Android NDK: Calling Kotlin from Native Code

In our previous article, Android NDK: The Interaction Between Kotlin and C/C++, we shared the results of our research into the Android NDK (Native Development Kit). In that article, you can find out how to set C/C++ and Kotlin interaction. We showed how Kotlin can invoke C/C++ code with any data types. We also converted String instances and passed Kotlin objects to native code, called functions, and threw JDK (Java Development Kit) exceptions.

Now it’s time to move to the practical part working with the Android NDK. In this article, we’ll dive deeper into the topic of working with Kotlin in the Android NDK and will tell you how to call Kotlin from native code.

Why do you need to call Kotlin from native code?

The most obvious reason to call Kotlin from native code is for callbacks. When you perform an operation, you need to know when it’s done and receive the result of that operation. This is true whether you work with Kotlin or the JNI (Java Native Interface). In this article, we’ll give examples of how to find out when a new value is persisted to memory. We showed how a new value is created in one of our previous articles.

Read also: Android NDK: The Interaction Between Kotlin and C/C++

android-ndk-calling-kotlin-from-native-code-1

Calling static properties

The JNIEnv pointer contains many functions to deal with static fields. First, you have to get the ID of the static field using the jfieldID GetStaticFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig) function.

Kotlin doesn’t have static members, but each Kotlin class can have a companion object, so you can use companion objects’ fields and functions similarly. A companion object is initialized when the corresponding class is loaded, matching the semantics of the Java static initializer. Thus, you can use a companion object to load native libraries:

class Store {

    companion object {
        init {
            System.loadLibrary("Store")
        }
    }
}

You can create a field in the companion object:

companion object {

+        val static: Int = 3

        init {
            System.loadLibrary("Store")
        }

}

JNIEnv contains different functions to access static field values from native code:

android-ndk-calling-kotlin-from-native-code-2

[Image source: JavaSE documentation]


You can get the int value as follows:

jclass clazz = env->FindClass("com/ihorkucherenko/storage/Store");
jfieldID fieldId = env->GetStaticFieldID(clazz, "static", "I");
if (fieldId == NULL) {
    __android_log_print(ANDROID_LOG_INFO,  __FUNCTION__, "fieldId == null");
} else {
    jint fieldValue = env->GetStaticIntField(clazz, fieldId);
    __android_log_print(ANDROID_LOG_INFO,  __FUNCTION__, "Field value: %d ", fieldValue); //Field value: 3
}

To determine a method’s signature, use type codes. The following table summarizes the various types available in the JNI, along with their codes:

android-ndk-calling-kotlin-from-native-code-3

In the picture above, the compiler generates a private static field in the Store class. The Store class contains fields with the private modifier. You can’t access those fields from the Store class. But with the JNI, you can access the values of those fields even if the private modifier is set.

// access flags 0x31
public final class com/ihorkucherenko/storage/Store {
  // access flags 0xA
  private static I static
  
  ....
  
}

If you just create this field in a simple class, you get an exception: java.lang.NoSuchFieldError: no “I” field “static” in class “Lcom/ihorkucherenko/storage/Store;” or its superclasses. If you use the Int? type, you also receive this error. To avoid this error, follow the instructions below.

Calling static methods

To get the ID of a static method, you have to use jmethodID GetStaticMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig).

JNIEnv contains three types of functions that can call static methods:

1. NativeType CallStatic<type>Method(JNIEnv *env, jclass clazz, jmethodID methodID, ...)

2. NativeType CallStatic<type>MethodA(JNIEnv *env, jclass clazz, jmethodID methodID, jvalue *args)

3. NativeType CallStatic<type>MethodV(JNIEnv *env, jclass clazz, jmethodID methodID, va_list args)

These types of functions differ in their arguments. Therefore, we have the following set of functions:

android-ndk-calling-kotlin-from-native-code-4

[Image source: Java SE Documantation]

Let’s try to invoke static methods.

class Store {

    .........

    companion object {
        
        @JvmStatic
        fun staticMethod(int: Int) = int

        init {
            System.loadLibrary("Store")
        }
    }
}

You have to use @JvmStatic to generate an additional static method. You can see this static metod in bytecode:

// access flags 0x19
  public final static staticMethod(I)I
  @Lkotlin/jvm/JvmStatic;()
   L0
    GETSTATIC com/ihorkucherenko/storage/Store.Companion : Lcom/ihorkucherenko/storage/Store$Companion;
    ILOAD 0
    INVOKEVIRTUAL com/ihorkucherenko/storage/Store$Companion.staticMethod (I)I
    IRETURN
   L1
    LOCALVARIABLE int I L0 L1 0
    MAXSTACK = 2
    MAXLOCALS = 1

Pay attention to the (I)I. This is the JNI’s representation of the type signature.

android-ndk-calling-kotlin-from-native-code-5

Types of arguments are specified in parentheses. Here are several examples:

1. Kotlin:

@JvmStatic
fun staticMethod(int: Int, floaf: Float) = int

Native code:

public final staticMethod(IF)I

2. Kotlin:

@JvmStatic
fun staticMethod(int: Int, floaf: Float, string: String) = int

Native code:

public final staticMethod(IFLjava/lang/String;)I

You should point the return type after parentheses.

Here’s how we call a Kotlin function from native code:

jclass clazz = env->FindClass("com/ihorkucherenko/storage/Store");
jmethodID methodId = env->GetStaticMethodID(clazz, "staticMethod", "(I)I");
if (methodId == NULL) {
    __android_log_print(ANDROID_LOG_INFO,  __FUNCTION__, "methodId == null");
} else {
    jint value = env->CallStaticIntMethod(clazz, methodId, 4);
    __android_log_print(ANDROID_LOG_INFO,  __FUNCTION__, "value: %d ", value); //value: 4
}

Calling an instance’s properties

Similarly to the previous code examples, you should use functions to retrieve the ID of the field and value: jfieldID GetFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig).

android-ndk-calling-kotlin-from-native-code-6

Let’s add private val property = 3, which is equal to private final I property = 3 in bytecode, to the Store class. After that, you have to get jobject, which refers to the instance of the Store class, in order to retrieve the value from property. For example, you can create a new jobject inside the native code and get the value:

jclass clazz = env->FindClass("com/ihorkucherenko/storage/Store");
jmethodID constructor = env->GetMethodID(clazz, "<init>", "()V");
jobject storeObject = env->NewObject(clazz, constructor);

jfieldID fieldId = env->GetFieldID(clazz, "property", "I");
jint value = env->GetIntField(storeObject, fieldId);
__android_log_print(ANDROID_LOG_INFO,  __FUNCTION__, "Property value: %d ", value); //Property value: 3 

Calling an instance’s methods

There is no significant difference between calling static and instance methods. Let’s look at an example:

jclass clazz = env->FindClass("com/ihorkucherenko/storage/Store");
jmethodID constructor = env->GetMethodID(clazz, "<init>", "()V");
jobject storeObject = env->NewObject(clazz, constructor);

jmethodID methodId = env->GetMethodID(clazz, "getSomething", "()I");
jint value = env->CallIntMethod(storeObject, methodId);
__android_log_print(ANDROID_LOG_INFO,  __FUNCTION__, "value: %d ", value); //value: 3

The difference between calling static and instance methods is that for instance method, you need to have jobject to pass it to CallIntMethod.

Callback example

Let’s add a simple callback to the Store:

interface StoreListener {

    fun onIntegerSet(key: String, value: Int)

    fun onStringSet(key: String, value: String)

}
class Store : StoreListener {

    val listeners = mutableListOf<StoreListener>()

    override fun onIntegerSet(key: String, value: Int) {
        listeners.forEach { it.onIntegerSet(key, value) }
    }

    override fun onStringSet(key: String, value: String) {
        listeners.forEach { it.onStringSet(key, value) }
    }
  
  .....
}

You can invoke these methods from proper native functions:

JNIEXPORT void JNICALL
Java_com_ihorkucherenko_storage_Store_setString(
        JNIEnv* pEnv,
        jobject pThis,
        jstring pKey,
        jstring pString) {
    // Turns the Java string into a temporary C string.
    StoreEntry* entry = allocateEntry(pEnv, &gStore, pKey);
    if (entry != NULL) {
        entry->mType = StoreType_String;
        // Copy the temporary C string into its dynamically allocated
        // final location. Then releases the temporary string.
        jsize stringLength = pEnv->GetStringUTFLength(pString);
        entry->mValue.mString = new char[stringLength + 1];
        // Directly copies the Java String into our new C buffer.
        pEnv->GetStringUTFRegion(pString, 0, stringLength, entry->mValue.mString);
        // Append the null character for string termination.
        entry->mValue.mString[stringLength] = '\0';

+        jclass clazz = pEnv->FindClass("com/ihorkucherenko/storage/Store");
+        jmethodID methodId = pEnv->GetMethodID(clazz, "onStringSet", "(Ljava/lang/String;Ljava/lang/String;)V");
+        pEnv->CallVoidMethod(pThis, methodId, pKey, pString);
    }
}

extern "C"
JNIEXPORT void JNICALL
Java_com_ihorkucherenko_storage_Store_setInteger(
        JNIEnv* pEnv,
        jobject pThis,
        jstring pKey,
        jint pInteger) {
    StoreEntry* entry = allocateEntry(pEnv, &gStore, pKey);
    if (entry != NULL) {
        entry->mType = StoreType_Integer;
        entry->mValue.mInteger = pInteger;

+        jclass clazz = pEnv->FindClass("com/ihorkucherenko/storage/Store");
+        jmethodID methodId = pEnv->GetMethodID(clazz, "onIntegerSet", "(Ljava/lang/String;I)V");
+        pEnv->CallVoidMethod(pThis, methodId, pKey, pInteger);
    }
}

Let's test it:

@Test
fun storeListener() {
  val valueInt = 3
  val keyInt = "integer"
  val valueString = "value"
  val keyString = "string"
  store.listeners.add(object : StoreListener {

      override fun onIntegerSet(key: String, value: Int) {
         assertEquals(keyInt, key)
         assertEquals(valueInt, value)
      }
      
      override fun onStringSet(key: String, value: String) {
          assertEquals(keyString, key)
          assertEquals(valueString, value)
      }
  })
  store.setInteger(keyInt, valueInt)
  store.setString(keyString, valueString)  
}

In this article, we told you how we extended the in-memory store example. You found out how to receive a value from a Kotlin instance and Kotlin static fields in native code, how to use calling Kotlin methods using the CallStatic<Type>Method and Call<Type>Method functions, and how to create a new jobject in C/C++. Now you can easily work with Android NDK and the JNI using Kotlin instead of Java for Android development.

4.9/ 5.0
Article rating
41
Reviews
Remember those Facebook reactions? Well, we aren't Facebook but we love reactions too. They can give us valuable insights on how to improve what we're doing. Whould you tell us how you feel about this article?