⬅️ Calling JVM code from Native
Calling back is easy too!
1. Create a contract interface in commonMain
@JniCallback
interface JvmCallback: AutoCloseable {
fun sayHello(): String
}
The @JniCallback annotation will tell KSP to generate relevant bindings on the native side.
package dev.datlag.nkommons
import com.dshatz.kni.binding.jobject
import com.dshatz.kni.utils.CallObjectMethodA
import com.dshatz.kni.utils.FindClass
import com.dshatz.kni.utils.GetMethodID
import com.dshatz.kni.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
lateinit var jvmCallback: JvmCallback
// commonMain
@JniCall expect fun init(callback: JvmCallback)
@JniCall expect fun dispose()
// Native
actual fun init(callback: JvmCallback) {
// Save the object for later.
jvmCallback = callback
}
actual fun dispose() {
jvmCallback.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: 23 April 2026