androidjvmjava-native-interfacekotlin-native

Use Kotlin/Native prebuilt shared library from an Android application using JNI


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?


Solution

  • 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);
    }