Kni
1.0.4indexedEnables 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.
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 │
│ │ │ │ │ (IPC) │
│ {loader }│ │ impl + register│ └───────────────┘
└───────────────┘ └───────────────┘
Kni uses Kotlin Multiplatform's expect/actual mechanism, but each module has a more precise role:
┌─────────────────────────────────────────────────────────────┐
│ commonMain │
│ Declare all bridging classes, functions, props │
│ StringUtil │
│ : String │
└─────────────────────────────────────────────────────────────┘
│
┌─────────────────────┴─────────────────────┐
▼ ▼
┌───────────────────────┐ ┌───────────────────────────┐
│ jvmMain │ │ nativeMain │
│ │ │ │
│ : │ │ : │
│ - { loader() } │ │ - Implement all common │
│ -
│ = 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
}
})
::innerFunction.register(...)
OtherClass::method.register(...)
::myProperty.register(...)
getInt: KFunction1<, > = ::
getInt.register(staticCFunction { ... })
add: (, ) -> = ::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 = notImplemented()
{
::reverse.register(staticCFunction { _, _, str: jstring ->
kniResultJava {
str.asString.reversed().asJni
}
})
}
}
JVM implementation has two styles: normal implementation and inline implementation.
Logic is in actual fun, register callback just calls it:
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
}
{ JSON, XML, CSV }
| Function |
|---|
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 }
kotlinList: List<KniAny?> = javaListObj.asList
StringClass: KniClass = .toClass()
kotlinLambda: (KniAny) -> = jobject.asKniCallback()
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 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:
// 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)
Reflection uses JNI's GetMethodID / GetStaticMethodID — you must provide complete type information:
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( registers: ) {
registers.forEach { r -> with(r) { onRegister() } }
}
// Initialize in JNI_OnLoad
: jint {
KniVM.onLoad(vm) {
register(StringUtil)
register(DataFormatter)
}
JNI_VERSION_1_6
}
{
KniVM.onUnload()
}
KniVM.onLoad automatically registers two base components:
You don't need to register them manually—just register your custom components.
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:
// 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:
:
}
// Server - implement IPC interface
object CalculatorService : ICalculatorService {
: = a + b
: = a * b
{
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 : IBinder =
DreamSmartIPC.asServer(CalculatorService, ICalculatorService::)
}
: () {
: IBinder =
DreamSmartIPC.asServer(CalculatorService, ICalculatorService::)
}
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")
}
Auto-selection: Default isChunk = true, SDK 27+ uses SharedMemory, below 27 uses chunked JSON.
┌─────────────────────────────────────────────────────────────┐
│ commonMain │
│ │ │ │
└─────────────────────────────────────────────────────────────┘
│
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ jvmMain │ │ nativeMain │ │ androidMain │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
└───────────────┘ └───────────────┘ └───────────────┘
Apache License 2.0
| 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() |
// 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
}
fun kniOnLoad(vm: CPointer<JavaVMVar>, reserved: COpaquePointer): jint {
KniVM.onLoad(vm) {
initBridge()
}
return JNI_VERSION_1_6
}
fun kniOnUnload(vm: CPointer<JavaVMVar>, reserved: COpaquePointer): jint {
KniVM.onUnload()
return 0
}
// 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()
}
| 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 |
// 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)
| 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 |
| Shorthand | Java Return Type |
|---|
.string() | jstring |
.int() | jint |
.long() | jlong |
.boolean() | jboolean |
.byte() | jbyte |
.char() | jchar |
.short() | jshort |
.float() | jfloat |
.double() | jdouble |
.object() | jobject |
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 ^^ return
Method not found throws error directly
error: Cannot find method getName in com.example.User
Double-check class name, method name, and parameter types.
| 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 |
| 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!
}
// 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
}
}
| 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 |
Surfaced from shared tags and platforms — no rankings paid for.