The @JniCall annotation is the core of the KSP module. It generates the necessary JNI boilerplate so you can write clean, idiomatic Kotlin code.
1. Define Your Kotlin/Native Function
Write your function using standard Kotlin types and annotate it with @JNICall.
// commonMain
@JniCall
expect fun mixed(number: Long, value: CharArray, upper: Boolean, char: Char): String
// nativeMain or jniNativeMain
actual fun mixed(number: Long, value: CharArray, upper: Boolean, char: Char): String
return "$a, $b, $c, $d"
}
2. Let KSP generate the JNI Stub
When you build the project, KSP will automatically generate:
actual functions for JVM/Android.
native function to do the heavy JNI work.
public actual fun mixed(
number: Long,
`value`: CharArray,
upper: Boolean,
char: Char,
): String = mixedExternal(number, `value`, upper, char)
public external fun mixedExternal(
number: Long,
`value`: CharArray,
upper: Boolean,
char: Char,
): String
/**
* Calling user function kni.test.Bridge.mixed
* @param number kotlin.Long -> com.dshatz.kni.binding.jlong (via Simple)
* @param value kotlin.CharArray -> com.dshatz.kni.binding.jcharArray (via Convertable)
* @param upper kotlin.Boolean -> com.dshatz.kni.binding.jboolean (via Convertable)
* @param char kotlin.Char -> com.dshatz.kni.binding.jchar (via Convertable)
*
* @return kotlin.String <- com.dshatz.kni.binding.jstring
*/
@CName("Java_kni_test_Bridge_mixedExternal")
@OptIn(ExperimentalForeignApi::class, ExperimentalNativeApi::class)
public fun _mixedJNI(
env: CPointer<JNIEnvVar>,
clazz: jobject,
number: jlong,
`value`: jcharArray,
upper: jboolean,
char: jchar,
): jstring {
// generated by Kotlin-JNI
// this calls the actual fun you defined above in the native sourceset.
val env = env.GetAndAttach()!!
return (kni.test.Bridge.mixed(
number,
(`value`).toKCharArray(env)!!,
(upper).toKBoolean(env)!!,
(char).toKChar(env)!!
)).toJString(env)!!
}
3. Use in common code
Now you can call your mixed function from Android, JVM, Native or commonMain!
On non-native targets the call will be routed through JNI to your Native implementation.
fun main() {
mixed(Long.MAX_VALUE, " - max value".toCharArray(), false, 'x')
}
Expect/actual classes
Classes with @JniCall annotated work seamlessly across JVM/Native. This can be useful when having a long-lived native object is beneficial.
When an object on JVM is created, a corresponding object will be created on native. All future calls on JVM side will be routed to Native.
// common
expect class ImageRenderer(path: String) {
@JniCall
fun renderImage(width: Int, height: Int): ByteBuffer
}
// native
actual class Renderer actual constructor(path: String) {
actual fun renderImage(width: Int, height: Int): ByteBuffer {
// your implementation
}
}
// jvmMain is generated
Calling from JVM:
val renderer = ImageRenderer("~/photo.jpg")
val buffer = renderer.renderImage(1024, 768)
Under the hood
ImageRenderer("~/photo.jpg") makes a JNI call to Native to construct the actual class implementation with supplied parameters.
Long pointer is returned to JVM and stored in the JVM object.
renderer.renderImage passes the pointer, along with supplied parameters to native, where the native object is requested from the pointer.