r/Kotlin • u/NoCodeNoBugs_Dev • 4h ago
How should I handle JNI type conversions in Kotlin/Native's nativeMain?
I'm working on a KMP project using Kotlin/Native and need to convert between Kotlin types (String, ByteArray, etc.) and JNI types represented by COpaquePointer in nativeMain.
Since nativeMain can't directly use JNI APIs through the usual JVM/JNI mechanisms, I'd like to avoid duplicating conversion logic across native targets such as Windows and macOS.
I tried manually accessing the JNI function table through COpaquePointer and invoking function pointers. For example, here's my implementation for converting between Kotlin String and jstring:
@OptIn(ExperimentalForeignApi::class)
fun String.kotlinStringToPointer(
env: COpaquePointer?,
): COpaquePointer? {
if (env == null) return null
val envPointer = env.reinterpret<COpaquePointerVar>()
val jniNativeInterfaceVar = envPointer.pointed.value ?: return null
val functionTable = jniNativeInterfaceVar.reinterpret<COpaquePointerVar>()
val newStringUTFPtr =
functionTable.get(JniFunctionIndex.NewStringUTF) ?: return null
val newStringUTFFunction = newStringUTFPtr.reinterpret<
CFunction<(COpaquePointer?, CPointer<ByteVar>?) -> COpaquePointer?>
>()
return memScoped {
val cString = this@kotlinStringToPointer.cstr
newStringUTFFunction.invoke(env, cString.ptr)
}
}
@OptIn(ExperimentalForeignApi::class)
fun COpaquePointer?.toKotlinString(
env: COpaquePointer?,
): String? {
if (this == null || env == null) return null
val envPointer = env.reinterpret<COpaquePointerVar>()
val jniNativeInterfaceVar = envPointer.pointed.value ?: return null
val functionTable = jniNativeInterfaceVar.reinterpret<COpaquePointerVar>()
val getStringUTFCharsPtr =
functionTable.get(JniFunctionIndex.GetStringUTFChars) ?: return null
val releaseStringUTFCharsPtr =
functionTable.get(JniFunctionIndex.ReleaseStringUTFChars) ?: return null
val getStringUTFChars = getStringUTFCharsPtr.reinterpret<
CFunction<
(COpaquePointer?, COpaquePointer?, CPointer<ByteVar>?) ->
CPointer<ByteVar>?
>
>()
val releaseStringUTFChars = releaseStringUTFCharsPtr.reinterpret<
CFunction<
(COpaquePointer?, COpaquePointer?, CPointer<ByteVar>?) -> Unit
>
>()
val cStringPtr = getStringUTFChars.invoke(env, this, null) ?: return null
return try {
cStringPtr.toKString()
} finally {
releaseStringUTFChars.invoke(env, this, cStringPtr)
}
}
The function table indices are defined in my JniFunctionIndex mapping, based on the JNI Functions specification.
Although this approach works in my current setup, manually maintaining the function indices and function pointer signatures feels fragile and error-prone.
I'd appreciate advice on the following:
- Is manually invoking JNI function pointers through
COpaquePointera reasonable approach for Kotlin/Native, or should it generally be avoided? - Would
expect/actualbe a better approach, with JNI-specific conversions implemented in platform-specific source sets while keeping the public Kotlin API shared? - Would using Kotlin/Native
cinteropwithjni.h, or introducing a C/C++ bridge, be a cleaner and more maintainable solution?
I'm particularly interested in approaches that let me share as much conversion logic as possible across Windows and macOS without introducing unnecessary platform-specific duplication.
