
Enables seamless bidirectional communication between native-compiled binaries and managed runtimes, implementing the entire bridge in pure code with zero C/C++ glue, function registration and automatic conversions.
Kni is a Kotlin Multiplatform bridge library that enables seamless bidirectional communication between Kotlin/Native (compiled via Kotlin/Native) and Kotlin/JVM. Unlike traditional JNI bridging which requires manual C/C++ glue code, Kni allows you to implement the entire bridge in pure Kotlin while maintaining native-level performance.
// settings.gradle.kts
pluginManagement {
repositories {
mavenCentral()
gradlePluginPortal()
}
}Add dependency to your build.gradle.kts:
dependencies {
// Core API (required for all platforms)
implementation("io.github.dreammooncai:kni-api:1.0.4")
}// build.gradle.kts
plugins {
id("org.jetbrains.kotlin.multiplatform")
id("org.jetbrains.kotlin.plugin.serialization")
}
kotlin {
sourceSets {
commonMain.dependencies {
implementation("io.github.dreammooncai:kni-api:1.0.4")
}
}
}The library is published to Maven Central. No additional repositories are needed if mavenCentral() is already configured.
The core philosophy is simple: Define once in common, load in JVM, implement in Native, call anywhere.
┌─────────────────────────────────────────────────────────────┐
│ commonMain │
│ expect object StringUtil │
│ actual external fun reverse(str: String): String│
└─────────────────────────────────────────────────────────────┘
│
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ jvmMain │ │ nativeMain │ │ androidMain │
│ actual object │ │ actual object │ │ (IPC) │
│ init {loader }│ │ impl + register│ └───────────────┘
└───────────────┘ └───────────────┘
Kni uses Kotlin Multiplatform's expect/actual mechanism, but each module has a more precise role:
| Module | Role | Content |
|---|---|---|
commonMain |
Declaration | Declare all classes, functions, and properties needing JVM/Native bridging |
jvmMain |
Load & Declare | Load native dynamic library in init, use actual external fun for empty implementations |
nativeMain |
Implement | Implement IKniRegister interface, bind JNI callbacks in onRegister()
|
┌─────────────────────────────────────────────────────────────┐
│ commonMain │
│ Declare all bridging classes, functions, props │
│ expect object StringUtil │
│ actual external fun reverse(str: String): String │
└─────────────────────────────────────────────────────────────┘
│
┌─────────────────────┴─────────────────────┐
▼ ▼
┌───────────────────────┐ ┌───────────────────────────┐
│ jvmMain │ │ nativeMain │
│ │ │ │
│ actual object: │ │ actual object: │
│ - init { loader() } │ │ - Implement all common │
│ - actual external fun │ │ functions │
│ = empty impl │ │ - Implement IKniRegister │
│ │ │ - Bind JNI callbacks │
└───────────────────────┘ └───────────────────────────┘
:: is used to locate functions or properties in Kni. Kotlin infers which register overload to use based on types:
// Basic: reference current class's function
::reverse.register(staticCFunction { _, _, str: jstring ->
kniResultJava {
str.asString.reversed().asJni // jstring conversions need kniResultJava
}
})
// Specify this scope: reference outer function
this@OuterClass::innerFunction.register(...)
// Class name: reference other class's static or companion functions
OtherClass::method.register(...)
// Property reference
::myProperty.register(...)
// Overloaded functions: use variable to specify type
val getInt: KFunction1<Int, Int> = ::get // Only matches (Int) -> Int
getInt.register(staticCFunction { ... })
// Function reference variable: needs KFunction conversion
val add: (Int, Int) -> Int = ::add
add.asKFunction().register(staticCFunction { _, _, a: jint, b: jint ->
a + b
})For functions with generics, using :: directly fails due to type inference:
// ❌ Error: Cannot infer type for type parameter 'T'
::callOriginal.register(staticCFunction { ... })
// ✅ Solution: explicitly specify type
val callOriginal: (HookParam, Array<out ValueWrapper>, (Any) -> Unit) -> Unit = ::callOriginal
callOriginal.asKFunction().register(staticCFunction { _, _,
param: jobject,
args: jarray,
result: jobject ->
// ...
})JNI callbacks always have the first two parameters fixed as JNIEnv and jobject:
staticCFunction { env: CPointer<JNIEnvVar>, obj: jobject, ... ->
// env: JNI environment pointer
// obj: Java object that invoked this method (this)
// Remaining params: inferred from function signature
}If not needed, type annotations can be omitted: staticCFunction { _, _, param1, param2 -> ... }
// commonMain/kotlin/org/example/StringUtil.kt
package org.example
expect object StringUtil {
/**
* Reverse a string
* @param input input string
* @return reversed string
*/
fun reverse(input: String): String
}In jvmMain, load the native library and declare all functions with external:
// jvmMain/kotlin/org/example/StringUtil.kt
package org.example
actual object StringUtil {
init {
System.loadLibrary("native_tool") // Load native library
}
// Use external keyword declaration, JNI will find implementation in Native SO
actual external fun reverse(input: String): String
}In nativeMain, implement all functions and implement IKniRegister interface:
// nativeMain/kotlin/org/example/StringUtil.kt
package org.example
actual object StringUtil : IKniRegister {
// Use notImplemented() as placeholder, actual logic is in register callback
actual fun reverse(input: String): String = notImplemented()
override fun KniRegister.onRegister() {
// Register JNI callback
::reverse.register(staticCFunction { _, _, str: jstring ->
kniResultJava {
str.asString.reversed().asJni // jstring conversions need kniResultJava
}
})
}
}JVM implementation has two styles: normal implementation and inline implementation.
Logic is in actual fun, register callback just calls it:
// In nativeMain
override fun KniRegister.onRegister() {
::reverse.register(staticCFunction { _, _, str: jstring ->
reverse(str.asString).asJni // Call actual fun
})
}
actual fun reverse(input: String): String =
input.reversed() // Actual logic here
### 4. Export Register Function in Native SO
```kotlin
// nativeMain/kotlin/org/example/Bridge.kt
package org.example
fun KniRegister.initBridge() {
register(StringUtil) // Register all methods in StringUtil
}
@CName("JNI_OnLoad")
fun kniOnLoad(vm: CPointer<JavaVMVar>, reserved: COpaquePointer): jint {
KniVM.onLoad(vm) {
initBridge()
}
return JNI_VERSION_1_6
}
@CName("JNI_OnUnload")
fun kniOnUnload(vm: CPointer<JavaVMVar>, reserved: COpaquePointer): jint {
KniVM.onUnload()
return 0
}When function parameters contain complex types, conversion is needed in the callback:
// commonMain - Define a formatter
expect object DataFormatter {
fun format(
data: List<String>,
style: FormatStyle, // Enum parameter
callback: (String) -> Unit // Higher-order function callback
): String
}
enum class FormatStyle { JSON, XML, CSV }// nativeMain - Inline implementation
actual object DataFormatter: IKniRegister {
override fun KniRegister.onRegister() {
::format.register(staticCFunction { _, _,
data: jobject,
style: jobject,
callback: jobject ->
// Use kniResultJava for automatic return value conversion
kniResultJava {
// Java enum → Kotlin enum
val formatStyle = style.asEnum<FormatStyle>()
// Java List → Kotlin List
val items = data.asList.map { it!!.jObject.asString }
// Java callback → Kotlin Lambda
val onResult: (String) -> Unit = callback.asKniCallback()
// Business logic
val result = when (formatStyle) {
FormatStyle.JSON -> items.joinToString(",", "[", "]")
FormatStyle.XML -> items.joinToString("") { "<item>$it</item>" }
FormatStyle.CSV -> items.joinToString(",")
}
// Callback notification
onResult(result)
result.asJni
}
})
}
actual fun format(...): String = notImplemented()
}| Function | Purpose | Return Value Handling |
|---|---|---|
kni {} |
Only use asString etc., no asJni needed |
Return value will be Pop-cleaned |
kniResultJava {} |
Need to convert return value to Java | Auto asJni, jobject in return value is preserved |
Both are KniBridge extension functions, both use tryLocalFrame for local reference management.
// Use kni {}: only convert parameters, not return value
// Return value is Kotlin String, no need to convert to Java
::reverse.register(staticCFunction { _, _, str: jstring ->
kni {
str.asString.reversed() // Only use asString, no asJni needed
}
})
// Use kniResultJava {}: need to convert return value to Java
::greet.register(staticCFunction { _, _, name: jstring ->
kniResultJava {
"Hello, ${name.asString}!".asJni // Return value needs to be Java String
}
})Kni provides rich basic conversion methods:
// String conversion (ALL require kniResultJava context)
val kotlinStr: String = kniResultJava { jstring.jObject.asString }
val jstr: jstring = kniResultJava { kotlinStr.asJni }
// Enum conversion
val kotlinEnum: FormatStyle = kniResultJava { javaEnumObj.asEnum<FormatStyle>() }
val javaEnum: jobject = kniResultJava { kotlinEnum.asJni }
// List conversion
val kotlinList: List<KniAny?> = javaListObj.asList
// Class name string to Java Class
val StringClass: KniClass = "java.lang.String".toClass()
// Callback conversion: Java → Kotlin
val kotlinLambda: (KniAny) -> Unit = jobject.asKniCallback()
// Callback conversion: Kotlin → Java
val javaCallback: jobject = kotlinLambda.asJni()KniAny wraps Java objects and provides serialization/deserialization:
// Serialize: Kotlin object → Java object
val kotlinUser = User(name = "Alice", id = 1001)
val javaUser = User::class.java.serialize(kotlinUser)
// Deserialize: Java object → Kotlin object
val backToKotlin = User::class.java.deserialize<User>(javaUser)
// Convert to JSON
val json: String = javaUser.toJson()Kni provides a fluent reflection API using string class names:
// Get current time (equivalent to System.currentTimeMillis())
val time: Long = "java.lang.System".toClass().method {
name = "currentTimeMillis"
returnType = Long::class // Return type
}.long()
// Call instance method (equivalent to user.getName())
val name: String = "com.example.User".toClass().method {
name = "getName"
returnType = String::class // Return type
thisRef = userObj.asKni // This reference for instance method
}.string()
// Call method with parameters (no param type needed if parameters are empty)
val result: Boolean = "com.example.StringUtil".toClass().method {
name = "validate"
returnType = Boolean::class // Return type
param(String::class, Int::class) // Parameter types
}.boolean(param1, param2)// Get field value (equivalent to user.name)
val name: String = "com.example.User".toClass().field {
name = "name"
type = String::class // Field type
thisRef = userObj.asKni
}.string()
// Set field value (equivalent to user.name = "NewName")
"com.example.User".toClass().field {
name = "name"
type = String::class // Field type
thisRef = userObj.asKni
}.set("NewName".asJni)param() supports multiple types, Kni automatically converts them:
| Type | Example | Description |
|---|---|---|
| String class name | param("java.lang.String") |
Auto calls toClass()
|
| KClass | param(String::class) |
Native Class, auto detects primitives |
| KType | param(property.returnType) |
e.g. KProperty1.returnType
|
| KniClass | param(StringClass) |
Kni internal class type |
// Mix different parameter types
val result: Boolean = "com.example.Utils".toClass().method {
name = "process"
returnType = Boolean::class // Return type
// String class name
param("java.lang.String")
// KClass
param(Int::class)
// Existing variable
param(StringClass)
// KType
param(userNameProperty.returnType)
}.boolean()KClass Auto-conversion Rules:
Int::class → int (JNI primitive type)String::class → java.lang.String (fully qualified)// Direct method by descriptor
val result: String = "com.example.Utils".toClass().method {
descriptor = "calculate(Ljava/lang/String;I)Ljava/lang/String;"
}.string("param", 123)| Shorthand | Java Return Type |
|---|---|
.string() |
jstring |
.int() |
jint |
.long() |
jlong |
.boolean() |
jboolean |
.byte() |
jbyte |
.char() |
jchar |
.short() |
jshort |
.float() |
jfloat |
.double() |
jdouble |
.object() |
jobject |
Reflection uses JNI's GetMethodID / GetStaticMethodID — you must provide complete type information:
Class name must be fully qualified
// ✅ Correct: fully qualified name
"java.lang.System".toClass()
// ❌ Wrong: will not find the class
"System".toClass()Method name must match exactly
// ✅ Correct
name = "getName"
// ❌ Wrong: case-sensitive, no typos allowed
name = "GetName"Parameter types must be JNI signature format
// ✅ Correct: use class references
param(StringClass, IntClass)
param("java.lang.String".toClass(), "int".toClass())
// ❌ Wrong: shorthand or incomplete names won't work
param(String, Integer) // Will fail!Descriptors are more reliable (recommended for complex cases)
// Descriptor format: returnType(paramTypes...)
descriptor = "Ljava/lang/String;->substring(II)Ljava/lang/String;"
// fully.qualified.Class ^^^^^^^^ method ^^^^^^^^ params ^^ returnMethod not found throws error directly
error: Cannot find method getName in com.example.User
Double-check class name, method name, and parameter types.
Kni provides extensions to directly convert Kotlin KFunction / KProperty to JNI method/field calls:
// Convert Kotlin function to JNI method call
val method: KniMethod = ::myFunction.asKniMethod(thisRef = obj.asKni) {
// Additional configuration if needed
}
val result = method.string()
// Convert Kotlin property to JNI field access
val field: KniField = ::myProperty.asKniField(thisRef = obj.asKni)
val value = field.string()These extensions automatically extract:
declaringClass from the function/property's declaring classname from the function/property nameparam types from function parametersreturnType / type from return type// Automatic local reference management
bridge.tryLocalFrame {
val result = someMethod()
result
}
// When returning Java objects
bridge.tryLocalFrameResultJava {
createJavaObject()
}Classes implementing IKniRegister support dynamic JNI registration:
interface IKniRegister {
fun KniRegister.onRegister() {}
}
// Extension function: register single IKniRegister
fun IKniRegister.register() { onRegister() }
// Extension function: register multiple IKniRegisters
fun KniRegister.register(vararg registers: IKniRegister) {
registers.forEach { r -> with(r) { onRegister() } }
}// Initialize in JNI_OnLoad
@CName("JNI_OnLoad")
fun kniOnLoad(vm: CPointer<JavaVMVar>, reserved: COpaquePointer): jint {
// onLoad automatically registers KniCallbackProxy and KniLogger
KniVM.onLoad(vm) {
// KniRegister's this context is available here
register(StringUtil) // Register all methods in StringUtil
register(DataFormatter) // Register all methods in DataFormatter
}
return JNI_VERSION_1_6
}
// Cleanup in JNI_OnUnload
@CName("JNI_OnUnload")
fun kniOnUnload(vm: CPointer<JavaVMVar>, reserved: COpaquePointer) {
KniVM.onUnload()
}KniVM.onLoad automatically registers two base components:
You don't need to register them manually—just register your custom components.
| Platform | Status | Notes |
|---|---|---|
| Android | ✅ | Full JNI + IPC support |
| iOS | ✅ | Via Kotlin/Native CInterop |
| macOS | ✅ | Via Kotlin/Native CInterop |
| Windows | ✅ | Via Kotlin/Native CInterop |
| Desktop JVM | ✅ | Via KniLoader |
For Android cross-process communication, Kni provides DreamSmartIPC, enabling seamless IPC between apps.
┌─────────────────┐ Binder ┌─────────────────┐
│ Client App │ ◄──────────────────► │ Server App │
│ │ DreamSmartIPC │ │
│ asClient/ │ │ asServer() │
│ asClientAutoProxy │ │
└─────────────────┘ └─────────────────┘
IDreamIPC// ✅ Correct: interface extending IDreamIPC
interface ICalculatorService : IDreamIPC {
fun add(a: Int, b: Int): Int
}
// ❌ Wrong: regular class cannot be used for IPC
class CalculatorService { ... }IPC automatically selects the handling method based on parameter/return value types:
| Type | Handling | Example |
|---|---|---|
| Serializable types | JSON serialization |
Int, String, @Serializable data class
|
| Interface types | Proxy transmission |
ICallback, IUser
|
| Non-serializable without interface | ❌ Throws error | Custom regular class |
interface ICalculatorService : IDreamIPC {
// ✅ Int/String are serializable
fun add(a: Int, b: Int): Int
// ✅ @Serializable data class
fun getUser(id: Long): User
fun searchUsers(query: String): List<User>
// ✅ Higher-order function callbacks will be proxied
fun setCallback(callback: (String) -> Unit)
// ❌ Custom regular class cannot be transmitted
// fun getCustomObject(): CustomClass // Will throw error!
}// asClient: manual handling of return value proxy
val service = DreamSmartIPC.asClient<ICalculatorService>(binder, ICalculatorService::class)
// asClientAutoProxy: automatic proxy for all parameters and return values (recommended)
val service = DreamSmartIPC.asClientAutoProxy<ICalculatorService>(binder, ICalculatorService::class)// androidMain
interface ICalculatorService : IDreamIPC {
fun add(a: Int, b: Int): Int
fun multiply(a: Int, b: Int): Int
fun calculateAsync(a: Int, b: Int, callback: (Int) -> Unit)
}// Server - implement IPC interface
object CalculatorService : ICalculatorService {
actual fun add(a: Int, b: Int): Int = a + b
actual fun multiply(a: Int, b: Int): Int = a * b
actual fun calculateAsync(a: Int, b: Int, callback: (Int) -> Unit) {
callback(a * b)
}
}IPC supports multiple ways to expose services. RootService is just one option (for scenarios requiring root privileges):
// Option A: Regular Service (no root required)
class CalculatorService : Service() {
override fun onBind(intent: Intent): IBinder =
DreamSmartIPC.asServer(CalculatorService, ICalculatorService::class)
}
// Option B: Root Service (requires root privileges)
class CalculatorRootService : RootService() {
override fun onBind(intent: Intent): IBinder =
DreamSmartIPC.asServer(CalculatorService, ICalculatorService::class)
}// Client
object CalculatorClient : ServiceConnection {
var service: ICalculatorService? = null
fun start(context: Context) {
val intent = Intent(context, CalculatorService::class.java)
context.bindService(intent, this, Context.BIND_AUTO_CREATE)
}
override fun onServiceConnected(name: ComponentName?, binder: IBinder) {
// Auto proxy all complex types
service = DreamSmartIPC.asClientAutoProxy(binder, ICalculatorService::class)
}
override fun onServiceDisconnected(name: ComponentName?) {
service = null
}
}CalculatorClient.start(this)
// Call IPC methods as if they were local
val result = CalculatorClient.service?.add(10, 20)
CalculatorClient.service?.calculateAsync(5, 6) { r ->
println("Result: $r")
}| Mode | Description | Limit | Use Case |
|---|---|---|---|
DreamJsonBinder |
JSON serialization | ~1MB | Small data, simple objects |
DreamSharedMemoryBinder |
SharedMemory | ~4GB | Large data, high performance |
DreamJsonChunkBinder |
Chunked JSON | ~4GB | Medium data, good compatibility |
Auto-selection: Default isChunk = true, SDK 27+ uses SharedMemory, below 27 uses chunked JSON.
┌─────────────────────────────────────────────────────────────┐
│ commonMain │
│ IKniRegister │ KniExtension │ KniLogger │
└─────────────────────────────────────────────────────────────┘
│
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ jvmMain │ │ nativeMain │ │ androidMain │
│ │ │ │ │ │
│ KniLoader │ │ KniBridge │ │ DreamSmartIPC │
│ KniCallback │ │ KniVM │ │ Binder Series │
│ Proxy │ │ KniMethod │ │ │
│ │ │ KniField │ │ │
│ │ │ KniAny │ │ │
│ │ │ KniCallback │ │ │
│ │ │ Factory │ │ │
└───────────────┘ └───────────────┘ └───────────────┘
Apache License 2.0
Kni is a Kotlin Multiplatform bridge library that enables seamless bidirectional communication between Kotlin/Native (compiled via Kotlin/Native) and Kotlin/JVM. Unlike traditional JNI bridging which requires manual C/C++ glue code, Kni allows you to implement the entire bridge in pure Kotlin while maintaining native-level performance.
// settings.gradle.kts
pluginManagement {
repositories {
mavenCentral()
gradlePluginPortal()
}
}Add dependency to your build.gradle.kts:
dependencies {
// Core API (required for all platforms)
implementation("io.github.dreammooncai:kni-api:1.0.4")
}// build.gradle.kts
plugins {
id("org.jetbrains.kotlin.multiplatform")
id("org.jetbrains.kotlin.plugin.serialization")
}
kotlin {
sourceSets {
commonMain.dependencies {
implementation("io.github.dreammooncai:kni-api:1.0.4")
}
}
}The library is published to Maven Central. No additional repositories are needed if mavenCentral() is already configured.
The core philosophy is simple: Define once in common, load in JVM, implement in Native, call anywhere.
┌─────────────────────────────────────────────────────────────┐
│ commonMain │
│ expect object StringUtil │
│ actual external fun reverse(str: String): String│
└─────────────────────────────────────────────────────────────┘
│
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ jvmMain │ │ nativeMain │ │ androidMain │
│ actual object │ │ actual object │ │ (IPC) │
│ init {loader }│ │ impl + register│ └───────────────┘
└───────────────┘ └───────────────┘
Kni uses Kotlin Multiplatform's expect/actual mechanism, but each module has a more precise role:
| Module | Role | Content |
|---|---|---|
commonMain |
Declaration | Declare all classes, functions, and properties needing JVM/Native bridging |
jvmMain |
Load & Declare | Load native dynamic library in init, use actual external fun for empty implementations |
nativeMain |
Implement | Implement IKniRegister interface, bind JNI callbacks in onRegister()
|
┌─────────────────────────────────────────────────────────────┐
│ commonMain │
│ Declare all bridging classes, functions, props │
│ expect object StringUtil │
│ actual external fun reverse(str: String): String │
└─────────────────────────────────────────────────────────────┘
│
┌─────────────────────┴─────────────────────┐
▼ ▼
┌───────────────────────┐ ┌───────────────────────────┐
│ jvmMain │ │ nativeMain │
│ │ │ │
│ actual object: │ │ actual object: │
│ - init { loader() } │ │ - Implement all common │
│ - actual external fun │ │ functions │
│ = empty impl │ │ - Implement IKniRegister │
│ │ │ - Bind JNI callbacks │
└───────────────────────┘ └───────────────────────────┘
:: is used to locate functions or properties in Kni. Kotlin infers which register overload to use based on types:
// Basic: reference current class's function
::reverse.register(staticCFunction { _, _, str: jstring ->
kniResultJava {
str.asString.reversed().asJni // jstring conversions need kniResultJava
}
})
// Specify this scope: reference outer function
this@OuterClass::innerFunction.register(...)
// Class name: reference other class's static or companion functions
OtherClass::method.register(...)
// Property reference
::myProperty.register(...)
// Overloaded functions: use variable to specify type
val getInt: KFunction1<Int, Int> = ::get // Only matches (Int) -> Int
getInt.register(staticCFunction { ... })
// Function reference variable: needs KFunction conversion
val add: (Int, Int) -> Int = ::add
add.asKFunction().register(staticCFunction { _, _, a: jint, b: jint ->
a + b
})For functions with generics, using :: directly fails due to type inference:
// ❌ Error: Cannot infer type for type parameter 'T'
::callOriginal.register(staticCFunction { ... })
// ✅ Solution: explicitly specify type
val callOriginal: (HookParam, Array<out ValueWrapper>, (Any) -> Unit) -> Unit = ::callOriginal
callOriginal.asKFunction().register(staticCFunction { _, _,
param: jobject,
args: jarray,
result: jobject ->
// ...
})JNI callbacks always have the first two parameters fixed as JNIEnv and jobject:
staticCFunction { env: CPointer<JNIEnvVar>, obj: jobject, ... ->
// env: JNI environment pointer
// obj: Java object that invoked this method (this)
// Remaining params: inferred from function signature
}If not needed, type annotations can be omitted: staticCFunction { _, _, param1, param2 -> ... }
// commonMain/kotlin/org/example/StringUtil.kt
package org.example
expect object StringUtil {
/**
* Reverse a string
* @param input input string
* @return reversed string
*/
fun reverse(input: String): String
}In jvmMain, load the native library and declare all functions with external:
// jvmMain/kotlin/org/example/StringUtil.kt
package org.example
actual object StringUtil {
init {
System.loadLibrary("native_tool") // Load native library
}
// Use external keyword declaration, JNI will find implementation in Native SO
actual external fun reverse(input: String): String
}In nativeMain, implement all functions and implement IKniRegister interface:
// nativeMain/kotlin/org/example/StringUtil.kt
package org.example
actual object StringUtil : IKniRegister {
// Use notImplemented() as placeholder, actual logic is in register callback
actual fun reverse(input: String): String = notImplemented()
override fun KniRegister.onRegister() {
// Register JNI callback
::reverse.register(staticCFunction { _, _, str: jstring ->
kniResultJava {
str.asString.reversed().asJni // jstring conversions need kniResultJava
}
})
}
}JVM implementation has two styles: normal implementation and inline implementation.
Logic is in actual fun, register callback just calls it:
// In nativeMain
override fun KniRegister.onRegister() {
::reverse.register(staticCFunction { _, _, str: jstring ->
reverse(str.asString).asJni // Call actual fun
})
}
actual fun reverse(input: String): String =
input.reversed() // Actual logic here
### 4. Export Register Function in Native SO
```kotlin
// nativeMain/kotlin/org/example/Bridge.kt
package org.example
fun KniRegister.initBridge() {
register(StringUtil) // Register all methods in StringUtil
}
@CName("JNI_OnLoad")
fun kniOnLoad(vm: CPointer<JavaVMVar>, reserved: COpaquePointer): jint {
KniVM.onLoad(vm) {
initBridge()
}
return JNI_VERSION_1_6
}
@CName("JNI_OnUnload")
fun kniOnUnload(vm: CPointer<JavaVMVar>, reserved: COpaquePointer): jint {
KniVM.onUnload()
return 0
}When function parameters contain complex types, conversion is needed in the callback:
// commonMain - Define a formatter
expect object DataFormatter {
fun format(
data: List<String>,
style: FormatStyle, // Enum parameter
callback: (String) -> Unit // Higher-order function callback
): String
}
enum class FormatStyle { JSON, XML, CSV }// nativeMain - Inline implementation
actual object DataFormatter: IKniRegister {
override fun KniRegister.onRegister() {
::format.register(staticCFunction { _, _,
data: jobject,
style: jobject,
callback: jobject ->
// Use kniResultJava for automatic return value conversion
kniResultJava {
// Java enum → Kotlin enum
val formatStyle = style.asEnum<FormatStyle>()
// Java List → Kotlin List
val items = data.asList.map { it!!.jObject.asString }
// Java callback → Kotlin Lambda
val onResult: (String) -> Unit = callback.asKniCallback()
// Business logic
val result = when (formatStyle) {
FormatStyle.JSON -> items.joinToString(",", "[", "]")
FormatStyle.XML -> items.joinToString("") { "<item>$it</item>" }
FormatStyle.CSV -> items.joinToString(",")
}
// Callback notification
onResult(result)
result.asJni
}
})
}
actual fun format(...): String = notImplemented()
}| Function | Purpose | Return Value Handling |
|---|---|---|
kni {} |
Only use asString etc., no asJni needed |
Return value will be Pop-cleaned |
kniResultJava {} |
Need to convert return value to Java | Auto asJni, jobject in return value is preserved |
Both are KniBridge extension functions, both use tryLocalFrame for local reference management.
// Use kni {}: only convert parameters, not return value
// Return value is Kotlin String, no need to convert to Java
::reverse.register(staticCFunction { _, _, str: jstring ->
kni {
str.asString.reversed() // Only use asString, no asJni needed
}
})
// Use kniResultJava {}: need to convert return value to Java
::greet.register(staticCFunction { _, _, name: jstring ->
kniResultJava {
"Hello, ${name.asString}!".asJni // Return value needs to be Java String
}
})Kni provides rich basic conversion methods:
// String conversion (ALL require kniResultJava context)
val kotlinStr: String = kniResultJava { jstring.jObject.asString }
val jstr: jstring = kniResultJava { kotlinStr.asJni }
// Enum conversion
val kotlinEnum: FormatStyle = kniResultJava { javaEnumObj.asEnum<FormatStyle>() }
val javaEnum: jobject = kniResultJava { kotlinEnum.asJni }
// List conversion
val kotlinList: List<KniAny?> = javaListObj.asList
// Class name string to Java Class
val StringClass: KniClass = "java.lang.String".toClass()
// Callback conversion: Java → Kotlin
val kotlinLambda: (KniAny) -> Unit = jobject.asKniCallback()
// Callback conversion: Kotlin → Java
val javaCallback: jobject = kotlinLambda.asJni()KniAny wraps Java objects and provides serialization/deserialization:
// Serialize: Kotlin object → Java object
val kotlinUser = User(name = "Alice", id = 1001)
val javaUser = User::class.java.serialize(kotlinUser)
// Deserialize: Java object → Kotlin object
val backToKotlin = User::class.java.deserialize<User>(javaUser)
// Convert to JSON
val json: String = javaUser.toJson()Kni provides a fluent reflection API using string class names:
// Get current time (equivalent to System.currentTimeMillis())
val time: Long = "java.lang.System".toClass().method {
name = "currentTimeMillis"
returnType = Long::class // Return type
}.long()
// Call instance method (equivalent to user.getName())
val name: String = "com.example.User".toClass().method {
name = "getName"
returnType = String::class // Return type
thisRef = userObj.asKni // This reference for instance method
}.string()
// Call method with parameters (no param type needed if parameters are empty)
val result: Boolean = "com.example.StringUtil".toClass().method {
name = "validate"
returnType = Boolean::class // Return type
param(String::class, Int::class) // Parameter types
}.boolean(param1, param2)// Get field value (equivalent to user.name)
val name: String = "com.example.User".toClass().field {
name = "name"
type = String::class // Field type
thisRef = userObj.asKni
}.string()
// Set field value (equivalent to user.name = "NewName")
"com.example.User".toClass().field {
name = "name"
type = String::class // Field type
thisRef = userObj.asKni
}.set("NewName".asJni)param() supports multiple types, Kni automatically converts them:
| Type | Example | Description |
|---|---|---|
| String class name | param("java.lang.String") |
Auto calls toClass()
|
| KClass | param(String::class) |
Native Class, auto detects primitives |
| KType | param(property.returnType) |
e.g. KProperty1.returnType
|
| KniClass | param(StringClass) |
Kni internal class type |
// Mix different parameter types
val result: Boolean = "com.example.Utils".toClass().method {
name = "process"
returnType = Boolean::class // Return type
// String class name
param("java.lang.String")
// KClass
param(Int::class)
// Existing variable
param(StringClass)
// KType
param(userNameProperty.returnType)
}.boolean()KClass Auto-conversion Rules:
Int::class → int (JNI primitive type)String::class → java.lang.String (fully qualified)// Direct method by descriptor
val result: String = "com.example.Utils".toClass().method {
descriptor = "calculate(Ljava/lang/String;I)Ljava/lang/String;"
}.string("param", 123)| Shorthand | Java Return Type |
|---|---|
.string() |
jstring |
.int() |
jint |
.long() |
jlong |
.boolean() |
jboolean |
.byte() |
jbyte |
.char() |
jchar |
.short() |
jshort |
.float() |
jfloat |
.double() |
jdouble |
.object() |
jobject |
Reflection uses JNI's GetMethodID / GetStaticMethodID — you must provide complete type information:
Class name must be fully qualified
// ✅ Correct: fully qualified name
"java.lang.System".toClass()
// ❌ Wrong: will not find the class
"System".toClass()Method name must match exactly
// ✅ Correct
name = "getName"
// ❌ Wrong: case-sensitive, no typos allowed
name = "GetName"Parameter types must be JNI signature format
// ✅ Correct: use class references
param(StringClass, IntClass)
param("java.lang.String".toClass(), "int".toClass())
// ❌ Wrong: shorthand or incomplete names won't work
param(String, Integer) // Will fail!Descriptors are more reliable (recommended for complex cases)
// Descriptor format: returnType(paramTypes...)
descriptor = "Ljava/lang/String;->substring(II)Ljava/lang/String;"
// fully.qualified.Class ^^^^^^^^ method ^^^^^^^^ params ^^ returnMethod not found throws error directly
error: Cannot find method getName in com.example.User
Double-check class name, method name, and parameter types.
Kni provides extensions to directly convert Kotlin KFunction / KProperty to JNI method/field calls:
// Convert Kotlin function to JNI method call
val method: KniMethod = ::myFunction.asKniMethod(thisRef = obj.asKni) {
// Additional configuration if needed
}
val result = method.string()
// Convert Kotlin property to JNI field access
val field: KniField = ::myProperty.asKniField(thisRef = obj.asKni)
val value = field.string()These extensions automatically extract:
declaringClass from the function/property's declaring classname from the function/property nameparam types from function parametersreturnType / type from return type// Automatic local reference management
bridge.tryLocalFrame {
val result = someMethod()
result
}
// When returning Java objects
bridge.tryLocalFrameResultJava {
createJavaObject()
}Classes implementing IKniRegister support dynamic JNI registration:
interface IKniRegister {
fun KniRegister.onRegister() {}
}
// Extension function: register single IKniRegister
fun IKniRegister.register() { onRegister() }
// Extension function: register multiple IKniRegisters
fun KniRegister.register(vararg registers: IKniRegister) {
registers.forEach { r -> with(r) { onRegister() } }
}// Initialize in JNI_OnLoad
@CName("JNI_OnLoad")
fun kniOnLoad(vm: CPointer<JavaVMVar>, reserved: COpaquePointer): jint {
// onLoad automatically registers KniCallbackProxy and KniLogger
KniVM.onLoad(vm) {
// KniRegister's this context is available here
register(StringUtil) // Register all methods in StringUtil
register(DataFormatter) // Register all methods in DataFormatter
}
return JNI_VERSION_1_6
}
// Cleanup in JNI_OnUnload
@CName("JNI_OnUnload")
fun kniOnUnload(vm: CPointer<JavaVMVar>, reserved: COpaquePointer) {
KniVM.onUnload()
}KniVM.onLoad automatically registers two base components:
You don't need to register them manually—just register your custom components.
| Platform | Status | Notes |
|---|---|---|
| Android | ✅ | Full JNI + IPC support |
| iOS | ✅ | Via Kotlin/Native CInterop |
| macOS | ✅ | Via Kotlin/Native CInterop |
| Windows | ✅ | Via Kotlin/Native CInterop |
| Desktop JVM | ✅ | Via KniLoader |
For Android cross-process communication, Kni provides DreamSmartIPC, enabling seamless IPC between apps.
┌─────────────────┐ Binder ┌─────────────────┐
│ Client App │ ◄──────────────────► │ Server App │
│ │ DreamSmartIPC │ │
│ asClient/ │ │ asServer() │
│ asClientAutoProxy │ │
└─────────────────┘ └─────────────────┘
IDreamIPC// ✅ Correct: interface extending IDreamIPC
interface ICalculatorService : IDreamIPC {
fun add(a: Int, b: Int): Int
}
// ❌ Wrong: regular class cannot be used for IPC
class CalculatorService { ... }IPC automatically selects the handling method based on parameter/return value types:
| Type | Handling | Example |
|---|---|---|
| Serializable types | JSON serialization |
Int, String, @Serializable data class
|
| Interface types | Proxy transmission |
ICallback, IUser
|
| Non-serializable without interface | ❌ Throws error | Custom regular class |
interface ICalculatorService : IDreamIPC {
// ✅ Int/String are serializable
fun add(a: Int, b: Int): Int
// ✅ @Serializable data class
fun getUser(id: Long): User
fun searchUsers(query: String): List<User>
// ✅ Higher-order function callbacks will be proxied
fun setCallback(callback: (String) -> Unit)
// ❌ Custom regular class cannot be transmitted
// fun getCustomObject(): CustomClass // Will throw error!
}// asClient: manual handling of return value proxy
val service = DreamSmartIPC.asClient<ICalculatorService>(binder, ICalculatorService::class)
// asClientAutoProxy: automatic proxy for all parameters and return values (recommended)
val service = DreamSmartIPC.asClientAutoProxy<ICalculatorService>(binder, ICalculatorService::class)// androidMain
interface ICalculatorService : IDreamIPC {
fun add(a: Int, b: Int): Int
fun multiply(a: Int, b: Int): Int
fun calculateAsync(a: Int, b: Int, callback: (Int) -> Unit)
}// Server - implement IPC interface
object CalculatorService : ICalculatorService {
actual fun add(a: Int, b: Int): Int = a + b
actual fun multiply(a: Int, b: Int): Int = a * b
actual fun calculateAsync(a: Int, b: Int, callback: (Int) -> Unit) {
callback(a * b)
}
}IPC supports multiple ways to expose services. RootService is just one option (for scenarios requiring root privileges):
// Option A: Regular Service (no root required)
class CalculatorService : Service() {
override fun onBind(intent: Intent): IBinder =
DreamSmartIPC.asServer(CalculatorService, ICalculatorService::class)
}
// Option B: Root Service (requires root privileges)
class CalculatorRootService : RootService() {
override fun onBind(intent: Intent): IBinder =
DreamSmartIPC.asServer(CalculatorService, ICalculatorService::class)
}// Client
object CalculatorClient : ServiceConnection {
var service: ICalculatorService? = null
fun start(context: Context) {
val intent = Intent(context, CalculatorService::class.java)
context.bindService(intent, this, Context.BIND_AUTO_CREATE)
}
override fun onServiceConnected(name: ComponentName?, binder: IBinder) {
// Auto proxy all complex types
service = DreamSmartIPC.asClientAutoProxy(binder, ICalculatorService::class)
}
override fun onServiceDisconnected(name: ComponentName?) {
service = null
}
}CalculatorClient.start(this)
// Call IPC methods as if they were local
val result = CalculatorClient.service?.add(10, 20)
CalculatorClient.service?.calculateAsync(5, 6) { r ->
println("Result: $r")
}| Mode | Description | Limit | Use Case |
|---|---|---|---|
DreamJsonBinder |
JSON serialization | ~1MB | Small data, simple objects |
DreamSharedMemoryBinder |
SharedMemory | ~4GB | Large data, high performance |
DreamJsonChunkBinder |
Chunked JSON | ~4GB | Medium data, good compatibility |
Auto-selection: Default isChunk = true, SDK 27+ uses SharedMemory, below 27 uses chunked JSON.
┌─────────────────────────────────────────────────────────────┐
│ commonMain │
│ IKniRegister │ KniExtension │ KniLogger │
└─────────────────────────────────────────────────────────────┘
│
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ jvmMain │ │ nativeMain │ │ androidMain │
│ │ │ │ │ │
│ KniLoader │ │ KniBridge │ │ DreamSmartIPC │
│ KniCallback │ │ KniVM │ │ Binder Series │
│ Proxy │ │ KniMethod │ │ │
│ │ │ KniField │ │ │
│ │ │ KniAny │ │ │
│ │ │ KniCallback │ │ │
│ │ │ Factory │ │ │
└───────────────┘ └───────────────┘ └───────────────┘
Apache License 2.0