Kotlin-JNI Help

⬅️ Calling JVM code from Native

Calling back is easy too!

1. Create a contract interface in commonMain

@CallableFromNative interface JvmCallback: AutoCloseable { fun sayHello(): String }

The @CallableFromNative annotation will tell KSP to generate relevant bindings on the native side.

package dev.datlag.nkommons import dev.datlag.nkommons.binding.jobject import dev.datlag.nkommons.utils.CallObjectMethodA import dev.datlag.nkommons.utils.FindClass import dev.datlag.nkommons.utils.GetMethodID import dev.datlag.nkommons.utils.toKString import kotlin.OptIn import kotlin.String import kotlinx.cinterop.CPointer import kotlinx.cinterop.ExperimentalForeignApi import kotlinx.cinterop.`get` import kotlinx.cinterop.allocArray import kotlinx.cinterop.memScoped public class _JvmCallbackNativeImpl( env: CPointer<JNIEnvVar>, instance: jobject, ) : BaseCallback(env, "dev/datlag/nkommons/JvmCallback", instance), JvmCallback { override fun sayHello(): String { val cls = jvmClass val methodId = env.GetMethodID(cls, "sayHello", "()Ljava/lang/String;")!! return memScoped { val args = allocArray<jvalue>(0) env.CallObjectMethodA(ref, methodId, args)?.toKString(env)!! } } }

2. Create the JVM implementation

// JVM / Android class JvmCallbackImpl: JvmCallback { override fun sayHello(): String = "Hello" override fun close() { // This will be automatically called when the // native side calls close(). // Clean up resources on the JVM side. } }

3. Pass your JVM object to Native

// JVM / Android fun main() = init(JvmCallbackImpl()) external fun init(callback: JvmCallback) external fun dispose()
lateinit var jvmCallback: JvmCallback // Native @JNIConnect( packageName = "com.example", className = "MainKt" ) fun init(callback: JvmCallback) { // Save the object for later. jvmCallback = callback } @JNIConnect( packageName = "com.example", className = "MainKt" ) fun dispose() { jvmCallback.dispose() }
import dev.datlag.nkommons.JNIEnvVar import dev.datlag.nkommons.binding.jobject import kotlin.OptIn import kotlin.experimental.ExperimentalNativeApi import kotlin.native.CName import kotlinx.cinterop.CPointer import kotlinx.cinterop.ExperimentalForeignApi @OptIn(ExperimentalForeignApi::class, ExperimentalNativeApi::class) @CName("Java_com_example_MainKt_init") public fun _initJNI( env: CPointer<JNIEnvVar>, clazz: jobject, p0: jobject, ) { // FORCING BODY return `init`(dev.datlag.nkommons._JvmCallbackNativeImpl(env, p0)) }
import dev.datlag.nkommons.JNIEnvVar import dev.datlag.nkommons.binding.jobject import kotlin.OptIn import kotlin.experimental.ExperimentalNativeApi import kotlin.native.CName import kotlinx.cinterop.CPointer import kotlinx.cinterop.ExperimentalForeignApi @OptIn(ExperimentalForeignApi::class, ExperimentalNativeApi::class) @CName("Java_com_example_MainKt_dispose") public fun _disposeJNI(env: CPointer<JNIEnvVar>, clazz: jobject) { // FORCING BODY return dispose() }

4. Call sayHello() from native!

// Native fun getGreetings() { val message = jvmCallback.sayHello() memScoped { fprintf(stderr, "Message from JVM: %s\n", message.cstr.ptr) } // Greetings received, dispose of the object jvmCallback.dispose() }
Last modified: 04 February 2026