Calling native code from our cosy JVM environment was and is possible. JVM comes with the magical JNI APIs layer to make that. In this post we show how to use the JNI from a Kotlin/JVM program and how to implement the native counter-part with Kotlin/Native.
The example project contains several parts:
- The JVM part (define a native method, load native library, call the API)
- The Native part (build as shared library, register callback in the JVM, have fun)
The JVM Side
Let’s implement the JVM side in Kotlin. It will be enough to have the following code:
package org.jonnyzzz.jni.java
class NativeHost {
external fun callInt(x: Int) : Int
}
The external
keyword in Kotlin is the same as the native
keyword in Java. Both mean
the implementation of the method comes from the native library.
The Kotlin/Native Shared Library
The experiment was to use the same programming language to implement the native part too. We use Kotlin/Native for that.
The second point to try Kotlin/Native is to use the Kotlin Multiplatform Programming to share some code between Native and JVM worlds. I will omit examples of that in the current post.
JVM looks for specific symbol names to resolve native methods. We may find
the full spec
in the documentation. The callInt
function has the following symbol name:
jint Java_org_jonnyzzz_jni_java_NativeHost_callInt(JNIEnv *env,
jobject obj,
jint i);
The first two parameters are added for all JNI calls. The JNIEnv
allows
accessing the JVM to say create an object or throw an exception. We do
not need these parameters for our example, but we have to keep them for
binary compatibility. The function name in our example is generated
as follows:
Java_<package name>_<class name>_<method_name>
That encoding does not work for overloaded functions (there is on support for overloads in C). The JNI specification defines how to create a longer names with mangling to overcome the limitation.
The following declaration in Kotlin/Native code will implement it:
@CName("Java_org_jonnyzzz_jni_java_NativeHost_callInt")
fun callInt(env: CPointer<JNIEnvVar>, clazz: jclass, it: jint): jint {
initRuntimeIfNeeded()
Platform.isMemoryLeakCheckerActive = false
println("Native function is executed with: $it")
return it + 1
}
Here we use the @CName
annotation to instruct
the cinterop
to export the function as a symbol of the shared library.
The C interop Setup
The example above requires a project setup to work.
We need to import the jni.h
header into Kotlin/Native.
The cinterop
tool helps us to generate Kotlin code from a C library definitions.
The Project Setup
Before we jump into the native world, let’s create a project. We’ll use Gradle project, written in Kotlin. You may see the code from my GitHub or create a new one from a scratch. It will the kotlin multiplatform plugin.
The initial project setup in a build.gradle.kts
file could look like that:
plugins {
kotlin("multiplatform") version "1.3.61"
}
repositories {
mavenCentral()
}
kotlin {
jvm()
macosX64("native") {..} // see below
sourceSets["jvmMain"].dependencies {
implementation(kotlin("stdlib-jdk8"))
}
}
The macosX64
block defined the macOS shared library target. Rename it to
mingwX64
for Windows, and linuxX64
for Linux. Find more explanations on that
in the tutorial
for Kotlin/Native. We use a script
in the example repository to avoid manual configuration need.
We use the Kotlin/Native’s cinterop
tool to import the jni.h
declarations to Kotlin,
e.g JNIEnv
or jint
symbols. The whole script looks like that:
macosX64("native") { // use linuxX64 or mingwX64 on other OS
binaries {
sharedLib()
}
compilations["main"].cinterops.create("jni") {
val javaHome = File(System.getProperty("java.home")!!)
packageName = "org.jonnyzzz.jni"
includeDirs(
Callable { File(javaHome, "include") },
Callable { File(javaHome, "include/darwin") },
Callable { File(javaHome, "include/linux") },
Callable { File(javaHome, "include/win32") }
)
}
}
The src/nativeInterop/cinterop/jni.def
file contains the definitions for the c interop,
it should container the only one line to instruct the interop tool what headers to use:
headers = jni.h
Putting all Together
The example is ready. We use Kotlin/JVM to talk to Kotlin/Native in the same project. The project sources are on my GitHub, try it, open in IntelliJ and have fun.
You’ll have the following code in the console:
➜ kotlin-jni-mix git:(master) ✗ ./gradlew run
> Configure project :
Kotlin Multiplatform Projects are an experimental feature.
> Task :linkDebugSharedNative
Produced library API in libkotlin_jni_mix_api.h
> Task :linkReleaseSharedNative
Produced library API in libkotlin_jni_mix_api.h
> Task :run
Native function is executed with: 42
ret from the native: 43