Search code examples
c++android-ndkjvmjava-native-interfacejnienv

How to solve "Didn't find class "..." on path: DexPathList in my native callback to Java


I have trouble calling back to Java(Kotlin) from the native part of my Android application. It's an Audio app, so you'll see the word "Audio a lot", but I don't think the problem is related to that. I've read a lot of articles and experimented for a number of days on this, but I can't solve my problem. I can trigger my java callback from my native_lib.cpp, so I have tested all that, and the method references between my JniBridge and my native code should be correct. But I want to do it from a separate class and I also have to be able to do it from different threads and many times over. So I want to store my reference to the JVM callback class as permanent as I can, but I don't get it right.

My current code fails with this error:

A: java_vm_ext.cc:542] JNI DETECTED ERROR IN APPLICATION: JNI GetMethodID called with pending exception java.lang.ClassNotFoundException: Didn't find class "com.my.app.common.jni.JniBridge" on path: DexPathList[[directory "."],nativeLibraryDirectories=[/system/lib64, /system/vendor/lib64, /system/lib64, /system/vendor/lib64]]
A: java_vm_ext.cc:542]   at java.lang.Class dalvik.system.BaseDexClassLoader.findClass(java.lang.String) (BaseDexClassLoader.java:134)
A: java_vm_ext.cc:542]   at java.lang.Class java.lang.ClassLoader.loadClass(java.lang.String, boolean) (ClassLoader.java:379)
A: java_vm_ext.cc:542]   at java.lang.Class java.lang.ClassLoader.loadClass(java.lang.String) (ClassLoader.java:312)
A: java_vm_ext.cc:542] 
A: java_vm_ext.cc:542]     in call to GetMethodID
A: java_vm_ext.cc:542] "Thread-6" prio=10 tid=17 Runnable
A: java_vm_ext.cc:542]   | group="main" sCount=0 dsCount=0 flags=0 obj=0x13080000 self=0x7ca400e800
A: java_vm_ext.cc:542]   | sysTid=30175 nice=-16 cgrp=default sched=1073741825/2 handle=0x7c911014f0
A: java_vm_ext.cc:542]   | state=R schedstat=( 5738847 149269 9 ) utm=0 stm=0 core=1 HZ=100
A: java_vm_ext.cc:542]   | stack=0x7c91006000-0x7c91008000 stackSize=1009KB
A: java_vm_ext.cc:542]   | held mutexes= "mutator lock"(shared held)
A: java_vm_ext.cc:542]   native: #00 pc 00000000003cb654  /system/lib64/libart.so (art::DumpNativeStack(std::__1::basic_ostream<char, std::__1::char_traits<char>>&, int, BacktraceMap*, char const*, art::ArtMethod*, void*, bool)+220)

Here is the description of what I built:

I have built this callback class: AudioCallback.h:

#include <jni.h>

class AudioCallback {

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

private:
    JavaVM& mJvm;
    jobject& mObject;
};

AudioCallback.cpp:

#include <jni.h>>
#include <utils/logging.h>
#include "AudioCallback.h"

AudioCallback::AudioCallback(JavaVM &jvm, jobject &object) : mJvm(jvm), mObject(object) {
}

void AudioCallback::playBackProgress(int progressPercentage) {


    JNIEnv *g_env = NULL;

    int getEnvStat = mJvm.GetEnv((void **) &g_env, JNI_VERSION_1_6);
    JavaVMAttachArgs vmAttachArgs;
    vmAttachArgs.version = JNI_VERSION_1_6;
    vmAttachArgs.name = NULL;
    vmAttachArgs.group = NULL;

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

    if (g_env != NULL) {
        jclass target = g_env->FindClass("com/my/app/common/jni/JniBridge");
        jmethodID id = g_env->GetMethodID(target, "integerCallback", "(I)V");
        g_env->CallVoidMethod(mObject, id, (jint) progressPercentage);
    } else {
        LOGE("JNIEnv is null!");
    mJvm.DetachCurrentThread();
    }
}

In my native_lib.cpp, I get ahold of the JavaVM in JNI_OnLoad like this:

JavaVM *jvm;

jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    JNIEnv jvm_env;

    int getEnvStatus = vm->GetEnv((void **) &jvm_env, JNI_VERSION_1_6);

    if (getEnvStatus != JNI_OK) {
        LOGE("JNI_ONLOAD Failed to get the environment using GetEnv()");
        return -1;
    }
    jvm = vm;
    if (jvm == NULL) {
        LOGE("JNI_ONLOAD: globabl jvm is NULL");
    } else {
        LOGD("JNI_ONLOAD: global jvm is NOT NULL");
    }
    LOGD("Onload done");
    return JNI_VERSION_1_6;
}

After that, I have a method which builds the callback and the AudioEngine class that uses it:

JNIEXPORT void JNICALL
Java_com_my_app_common_jni_JniBridge_playFromJNI(JNIEnv *env, jobject instance,jstring URI) {

    // Here I instantiate my callback
    callback = std::make_unique<AudioCallback>(*jvm, instance);

    // below code sets up my audio engine class, which calls the AudioCallback during playback.
    // This works without problems.
    const char *uri = env->GetStringUTFChars(URI, NULL);

    AMediaExtractor *extractor = AMediaExtractor_new();
    if (extractor == nullptr) {
        LOGE("Could not obtain AMediaExtractor");
        return;
    }
    media_status_t amresult = AMediaExtractor_setDataSource(extractor, uri);
    if (amresult != AMEDIA_OK) {
        LOGE("Error setting extractor data source, error %d", amresult);
    }
    audioEngine = std::make_unique<AudioEngine>(*extractor, *callback);
    audioEngine->setFileName(uri);
    audioEngine->start();
}

audioEngine makes use of the oboe library for audio processing, so I have no control over possible thread creation in there, hence the AttachCurrentThread() in my callback class. Attaching and detaching works, the only thing that fails is the callback to Java.


Solution

  • I solved the problem mostly myself (Reading through @Michael's suggestion in the comment and a few other places mentioned there definitely helped!):

    First, I separated the "setup" part from the actual callback function (This was the plan all along, but not doing it first stood in the way of seeing the problem).

    Second, in my native_lib.cpp I created a global reference to my jobject (the "instance" object in playFromJNI from the example in the question) before passing it into AudioCallback's constructor like this:

        myJNIClass = env->NewGlobalRef(instance);
        callback = std::make_unique<AudioCallback>(*g_jvm, myJNIClass);
    
    

    Third, in the actual callback method playbackProgress I only handle the ÀttachToCurrentThread` if necessary, and use the class members, that I have initialized beforehand with the correct data.

    This is what my AudioCallback implementation looks like now:

    #include <jni.h>>
    #include <utils/logging.h>
    #include "AudioCallback.h"
    
    
    jclass target;
    jmethodID id;
    
    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);
        LOGD("Env Stat: %d", getEnvStat);
    
        if (g_env != NULL) {
            target = g_env->GetObjectClass(g_object);
            id = g_env->GetMethodID(target, "integerCallback", "(I)V");
        }
    }
    
    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);
    }
    

    This now works for me as desired, but since I'm not a C++/JNI expert, there might still be problems in my code. Feel free to point them out!