Search code examples
androidc++kotlincallbackjava-native-interface

JNI on Android: Callback from native with Integer parameter works, with String or Void it doesn't. Why?


I have a callback class for doing callbacks from native C++ code to Kotlin (not sure if Kotlin/Java makes a difference here, if so, is there any documentation on that?). I have a working callback with an integer parameter, that I call from different native threads without a problem. Now I want to add a second one that sends a String, but for some reason that doesn't work.

My callback class implementation looks like this:

 jclass target;
 jmethodID id;

 Callback::Callback(JavaVM &jvm, jobject object) : g_jvm(jvm), g_object(object) {
     JNIEnv *g_env;
     int getEnvStat = g_jvm.GetEnv((void **) &g_env, JNI_VERSION_1_6);

     if (g_env != NULL) {
         target = g_env->GetObjectClass(g_object);
         id = g_env->GetMethodID(target, "integerCallback", "(I)V");
     }
 }

 void Callback::call(int integerValue, const char *stringValue) {
     JNIEnv *g_env;
     int getEnvStat = g_jvm.GetEnv((void **) &g_env, JNI_VERSION_1_6);

     if (getEnvStat == JNI_EDETACHED) {
         if (g_jvm.AttachCurrentThread(&g_env, NULL) != 0) {
             LOGD("GetEnv: Failed to attach");
         }
     } else if (getEnvStat == JNI_OK) {
        LOGD("GetEnv: JNI_OK");
     } else if (getEnvStat == JNI_EVERSION) {
        LOGD("GetEnv: version not supported");
     }

     g_env->CallVoidMethod(g_object, id, (jint) integerValue);
 }

It gets instantiated in my native-lib.cpplike this:

extern "C" {


std::unique_ptr<Callback> callback;

JavaVM *g_jvm = nullptr;

static jobject myJNIClass;


jint JNI_OnLoad(JavaVM *pJvm, void *reserved) {
    g_jvm = pJvm;
    return JNI_VERSION_1_6;
}

JNIEXPORT void JNICALL
Java_com_my_app_common_jni_JniBridge_loadNative(JNIEnv *env, jobject instance,
                                                          jstring URI, jboolean isWaitMode) {
    myJNIClass = env->NewGlobalRef(instance);

    if (callback == nullptr) {
        callback = std::make_unique<Callback>(*g_jvm, myJNIClass);
    }

}

The callback method it talks to is in my JniBridge.kt (the threading part is probbaly irrelevant to the problem):

object JniBridge {
    init {
        System.loadLibrary("native-lib")
        Timber.d("Native Lib loaded!")
    }

    fun load(fileName: String, isWaitMode: Boolean) {
        loadNative(fileName, isWaitMode)
    }

    fun integerCallback(value: Int) {
        someListener?.invoke(value)
    }

    private external fun loadNative(fileName: String, isWaitMode: Boolean)
}

So now if my native code triggers the call() method in my callback, my integerCallback() in JniBridge.kt gets called correctly with an integer.

But here's what I don't get: If I change my callback to send a String it doesn't work. If I change it like this:

// in JniBridge.kt
 fun stringCallback(value: String) {
        someListener?.invoke(value)
    }
// in Callback.cpp

//getting the method
id = g_env->GetMethodID(target, "stringCallback", "(Ljava/lang/String)V");

//making the call
g_env->CallVoidMethod(g_object, id, (jstring) stringValue);

Now my app crashes with this error:

JNI DETECTED ERROR IN APPLICATION: JNI GetStringUTFChars called with pending exception java.lang.NoSuchMethodError: no non-static method "Lcom/my/app/common/jni/JniBridge;.stringCallback(Ljava/lang/String)V"

The same happens if I try calling a void method (like this: id = g_env->GetMethodID(target, "voidCallback", "(V)V");or calling one that takes two integers (like this: id = g_env->GetMethodID(target, "twoIntegerCallback", "(I;I)V");, of course with having the corresponding methods in JniBridge.kt in place.

Why does this happen and how can I fix it?

Note: For clarity I have omitted all parts of my code that I believe are not related to the problem, if something crucial is missing, please let me know and I fix it.


Solution

  • You're missing a semi-colon in the method signature that you pass to GetMethodID.

    It should be:

    id = g_env->GetMethodID(target, "stringCallback", "(Ljava/lang/String;)V");
    

    Note the semi-colon after Ljava/lang/String.

    See the part about Type Signatures in Oracle's JNI documentation.


    Regarding your other variations:

    A Java void function() would have the signature ()V.
    A Java void function(int i, int j) would have the signature (II)V.


    As a side note, you really ought to verify the return value and check for exceptions after every JNI call.