⬅️ 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