I'm trying to build a Kotlin/Native library and use it in an Android application. I actually want to use a prebuilt shared library and not a .jar because I want my code to be obfuscated.
I've managed to make my Kotlin/Native library work in my Android application using JNI and a lot of trial and error. The thing is I've only made a very simple "Hello world" function work with this Kotlin/Native code:
fun sayHello(): String {
return "Hello, Kotlin/Native!"
}
This C++ code for JNI:
extern "C" JNIEXPORT jstring JNICALL
Java_com_my_package_NativeLib_sayHello(
JNIEnv *env,
jobject /* this */) {
libshared_lib_ExportedSymbols *lib = libshared_lib_symbols();
return env->NewStringUTF(lib->kotlin.root.sayHello());
}
And finally this Kotlin code in my Android classes:
class NativeLib {
external fun sayHello(): String
companion object {
init {
System.loadLibrary("native-lib")
}
}
}
For the record, I'm building the Kotlin/Native with Gradle into a prebuilt shared library for android target. I then copy/paste the .so
file in my app's libs folder, and with some Gradle magic I'm getting it to work.
However it gets more complicated when I start writing a function taking a ByteArray as a parameter like so (the implementation isn't relevant):
fun readByteArray(byteArray: ByteArray): String {
val unsignedByteArray = byteArray.toUByteArray()
var i = 0
while (i < byteArray.size) {
// Access the array
val byte = unsignedByteArray[i].toUInt()
}
return "Done reading the array"
}
Then adding this to my NativeLib
class on Android side:
external fun readByteArray(byteArray: ByteArray): String
And what should be the JNI C++ equivalent:
extern "C" JNIEXPORT jstring JNICALL
Java_com_my_package_NativeLib_readByteArray(JNIEnv *env, jobject,
jbyteArray byte_array) {
libshared_lib_ExportedSymbols *lib = libshared_lib_symbols();
// What to do here?
return env->NewStringUTF(lib->kotlin.root.readByteArray(/* and here? */));
}
The thing is Kotlin/Native is generating this header file:
typedef void* libshared_lib_KNativePtr;
...
typedef struct {
libshared_lib_KNativePtr pinned;
} libshared_lib_kref_kotlin_Byte;
...
typedef struct {
libshared_lib_KNativePtr pinned;
} libshared_lib_kref_kotlin_ByteArray;
...
struct {
struct {
const char* (*readByteArray)(libshared_lib_kref_kotlin_ByteArray byteArray);
const char* (*sayHello)();
} root;
} kotlin;
...
That's where I get stuck, how exactly should I build a libshared_lib_kref_kotlin_ByteArray
? Here's what I've tried so far, only to get a crash when the code tries to read the array:
extern "C" JNIEXPORT jstring JNICALL
Java_com_my_package_NativeLib_readByteArray(JNIEnv *env, jobject,
jbyteArray byte_array) {
libshared_lib_ExportedSymbols *lib = libshared_lib_symbols();
libshared_lib_KNativePtr ptr = env->GetDirectBufferAddress(byte_array);
libshared_lib_kref_kotlin_ByteArray byteArray;
byteArray.pinned = ptr;
return env->NewStringUTF(lib->kotlin.root.readByteArray(byteArray));
}
I also tried using GetByteArrayElements()
without success.
Does anyone know how to proceed with those Kotlin generated types?
As Botje stated in the comments of my question, it looks like I needed to use C interop types in my Kotlin/Native function for it to be called correctly from the JNI C code.
So here's the Kotlin/Native code I had to write:
fun readByteArray(byteArray: CPointer<ByteVar>, length: Int): String {
// Convert the C interop array type to a standard Kotlin type
val ktByteArray = byteArray.readBytes(length)
// Here access the `ktByteArray` as we would any Kotlin array
return "Done reading the array"
}
And here's the C++ JNI glue to tie it with my Kotlin JVM code:
extern "C" JNIEXPORT jstring JNICALL
Java_com_my_package_NativeLib_readByteArray(JNIEnv *env, jobject,
jbyteArray byte_array) {
libshared_lib_ExportedSymbols *lib = libshared_lib_symbols();
jsize len = env->GetArrayLength(byte_array);
jbyte *nativeByteArray = env->GetByteArrayElements(byte_array, nullptr);
const char *response = lib->kotlin.root.readByteArray(nativeByteArray, len);
return env->NewStringUTF(response);
}