androidc++jvmjava-native-interfacejnienv

C++ Callback to Java -> Why can't I retrieve JNIEnv in callback class when coming from a different thread?


I'm trying to build a callback class to make a call to a Java method from different threads in my native code, on Android. I have read a lot about how to that, and as long as I'm on the same thread, it all works. But from a different thread I can't retrieve the JNIEnv properly, and I can't figure out what I'm doing wrong.

I'm not very experienced with C++ and the JNI so it's quite possible this is some beginner's problem...but I've spetd days on it and can't see what it is.

This is my Callback class, .h and .cpp file:

class AudioCallback {

public:
   explicit AudioCallback(JavaVM&, jobject);
    void playBackProgress(int progressPercentage);

private:
    JavaVM& g_jvm;
    jobject g_object;
};
jclass target = NULL;
jmethodID id = NULL;

AudioCallback::AudioCallback(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");

        //This is a test call to see if I can call my java method. It works.
        g_env->CallVoidMethod(g_object, id, (jint) 103);
    }
}

// this method is calles from other threads, so I want to attach to the current thread once I got my JNIEnv, but I can't since it's null...
void AudioCallback::playBackProgress(int progressPercentage) {
    JNIEnv *g_env;

    // This is null and I don't know why!
    int getEnvStat = g_jvm.GetEnv((void **) &g_env, JNI_VERSION_1_6);


    if (g_env == NULL) {
        LOGE("JNIEnv in callback method is null");
    } else {
        LOGD("Env Stat: %d", getEnvStat);
        JavaVMAttachArgs vmAttachArgs;
        if (getEnvStat == JNI_EDETACHED) {
            LOGD("GetEnv: not attached - attaching");
            if (g_jvm.AttachCurrentThread(&g_env, &vmAttachArgs) != 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) progressPercentage);
        //thread gets detached elsewhere
    }
}

This is my native_lib, where I get the JavaVM and instantiate the callback class:

std::unique_ptr<AudioEngine> audioEngine;
std::unique_ptr<AudioCallback> 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_appy_common_jni_JniBridge_playFromJNI(JNIEnv *env, jobject instance,jstring URI) {

    myJNIClass = env->NewGlobalRef(instance);

    callback = std::make_unique<AudioCallback>(*gJvm, myJNIClass);

    // this test call to my callback works
    callback->playBackProgress(104);

    const char *uri = env->GetStringUTFChars(URI, NULL);

    //... urelated code is left out here ...

    //audioEngine gets the callback and uses it from threads it creates
    audioEngine = std::make_unique<AudioEngine>(*extractor, *callback);
    audioEngine->setFileName(uri);
    audioEngine->start();
}

I've shortened the code and removed all unrelated/unnecessary parts. If something crucial is missing, please comment and I'll add it.

Solution: As per the suggestions @Michael made in his answer, I made these edits to the playbackProgressmethod in my callback class to make it work:

void AudioCallback::playBackProgress(int progressPercentage) {
    JNIEnv *g_env;
    int getEnvStat = g_jvm.GetEnv((void **) &g_env, JNI_VERSION_1_6);

    if (getEnvStat == JNI_EDETACHED) {
        LOGD("GetEnv: not attached - attaching");
        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) progressPercentage);
//    mJvm.DetachCurrentThread();
}

Now it checks for the value of getEnvStat directly, the null check of g_env before was wrong. I also had to replace the JavaVMAttachArgswith NULLto make it work.


Solution

  • Your logic in playBackProgress wrong.

    The only time your try to attach the current thread is when g_env is non-NULL. But if g_env is non-NULL then GetEnv probably succeeded (you should of course also check that getEnvStat == JNI_OK) and AttachCurrentThread isn't necessary.

    The case in which you need to call AttachCurrentThread is when g_env is NULL and getEnvStat is JNI_EDETACHED.

    You also need to keep track of whether you did call AttachCurrentThread, since you should call DetachCurrentThread at some point in those cases. See this answer for more info on that.