A small JNI layer for Kotlin ⇄ C++: a C++ static library that includes nothing but <jni.h>, a
Kotlin runtime of native-handle types, and a Kotlin compiler plugin that removes the JNI boilerplate
at compile time — no fbjni, no folly, no JSI.
external methods annotated with @NativeMethod are rewritten to take their object's C++ address
as the first argument, so the native side reads the peer straight off the call instead of doing an
env->GetLongField(thiz, ...); @AsNativePointer parameters cross the same way, each fenced so the
GC cannot free a peer the native code is still using. NativeObject and NativeCleaner own that
peer's lifetime. On the C++ side, expo::kolibri wraps the raw JNIEnv in typed classes, members
and refs, with a header-only expo::meta layer doing the compile-time work.
Kolibri is published to Maven Central. Apply the Gradle plugin next to the Kotlin plugin:
// build.gradle.kts
plugins {
kotlin("jvm") version "2.3.20" // or kotlin("android") — any release listed below
id("io.github.expo.kolibri") version "0.1.9"
}The plugin registers the compiler plugin and adds the runtime jar. On an Android project it also
adds the kolibri-android AAR — the same C++ tree prebuilt for all four ABIs — and turns on Prefab,
so CMake can link the static library into your own shared library:
find_package(kolibri-android REQUIRED CONFIG)
target_link_libraries(mylib PRIVATE kolibri-android::kolibri)Build with -DANDROID_STL=c++_shared; Prefab fails the configure on an STL mismatch.
The runtime jar carries its own R8 rules, which AGP applies to the app. They keep what
@CalledFromNative marks: put it on each class your native code finds by name, including each class
whose natives it registers (the class keeps all of them), and on each method, field or constructor
it resolves by name. Nothing else needs a rule.
@CalledFromNative(by = "mylib/Counter.cpp")
class Counter(pointer: NativePointer) : NativeObject(pointer) {
@NativeMethod
external fun increment(): Int
@CalledFromNative
fun onOverflow(limit: Int) = println("overflow at $limit")
}A Kotlin compiler plugin is bound to the exact compiler it was built against, so Kolibri builds and
publishes its compiler plugin once per Kotlin release, as
io.github.expo.kolibri:compiler-plugin:<kolibri>-<kotlin> (for example 0.1.9-2.3.20). The Gradle
plugin picks the build that matches your project's Kotlin, so nothing in your build refers to it
directly. The runtime jar, the kolibri-android AAR and the Gradle plugin itself are the same for
every release.
Every release in gradle/kotlin-versions.txt is built and tested on
CI: 2.2.0, 2.2.10, 2.2.20, 2.2.21, 2.3.0, 2.3.10, 2.3.20, 2.3.21, 2.4.0, 2.4.10 and 2.4.20. A Kotlin
release outside that list has no compiler-plugin artifact, and the build fails resolving it.
-PkotlinVersion=<version> makes the whole build use that Kotlin; without it the catalog's default,
the oldest supported release, applies. To try an unreleased Kolibri in a project, publish it into
your local Maven repository with the project's Kotlin:
./gradlew publishToMavenLocal -PkotlinVersion=2.3.20and add mavenLocal() to both pluginManagement.repositories in settings.gradle.kts and the
project's repositories.
scripts/test-all-versions.sh runs the Kotlin-dependent checks against every listed release, and
./gradlew :compiler-plugin:test -PkotlinVersion=<version> against one. To add a release, append it
to gradle/kotlin-versions.txt and run the script; if the release changed a compiler API the plugin
uses, or the golden dumps, see compiler-plugin/compat/README.md.
