49

Android's JNI tips page mentions this FAQ: Why didn't FindClass find my class? They mention multiple solutions and the last option there is this one:

Cache a reference to the ClassLoader object somewhere handy, and issue loadClass calls directly. This requires some effort.

So, I tried to get it working and it seems that no matter what, this method simply does not work for me. Eventually, I figured how to use ClassLoader but it won't work if from a native thread I try to loadClass that hasn't been touched/loaded yet. Essentially, it's the identical to env->FindClass in behavior when called from a native thread, with the exception that it won't return 0 for classes that were already use in the app. Any idea if I didn't get it right, or it's impossible to access classes from a native thread that weren't used/loaded yet.






EDIT: I'll give more info to explain what exactly I mean. There is regular JNI env->FindClass(className), and another one that I wrote myFindClass(env, className) that uses cached ClassLoader->loadClass.

The class that I'm trying to access from native c/c++ is "com/noname/TestClient". Inside myFindClass I also use env->FindClass and log value that it returns:

jclass myFindClass(JNIEnv * env, const char* name)
{
    ...
    jclass c0 = env->FindClass(name);
    jclass c1 = (jclass)env->CallObjectMethod(ClassLoader,
        MID_loadClass, envNewStringUTF(name));
    dlog("myFindClass(\"%s\") => c0:%p, c1:%p, c0 and c1 are same: %d",
        name, c0, c1, env->IsSameObject(c0, c1));
    ...
}

Then, I have these 3 combinations to explain the issue.

1)

//inside JNI_OnLoad thread
myFindClass(env, "com/noname/TestClient");
...

//inside native thread created by pthread_create
myFindClass(env, "com/noname/TestClient");

I get this logcat:

myFindClass("com/noname/TestClent") => c0:0x41b64558, c1:0x41b64558, c0 and c1 are same: 1
...
myFindClass("com/noname/TestClent") => c0:0, c1:0x41b64558, c0 and c1 are same: 0

2)

//inside JNI_OnLoad thread
env->FindClass("com/noname/TestClient");
...

//inside native thread created by pthread_create
myFindClass("com/noname/TestClient");

I get this logcat:

myFindClass("com/noname/TestClent") => c0:0, c1:0x41b64558, c0 and c1 are same: 0

3)

//inside JNI_OnLoad thread
//"com/noname/TestClient" isn't touched from JNI_OnLoad.
...

//inside native thread created by pthread_create
myFindClass(env, "com/noname/TestClient");

I get this logcat:

myFindClass("com/noname/TestClent") => c0:0, c1:0, c0 and c1 are same: 1

Basically, my issue is that ClassLoader doesn't find my class in the 3rd case. Is it a bug? What can be done to fix the problem?

EDIT2: On top of that, it seems that ClassLoader::loadClass is plainly buggy. If I ask myFindClass("noname/TestClent") then it returns some garbage, and when I use that returned jclass in any way the app crashes.

Pavel P
  • 15,789
  • 11
  • 79
  • 128
  • Yes, that's normal, Android apps do not use the system class loader by default. Just cache all that you need in `JNI_OnLoad()` and that will take care of that. – Samuel Audet Nov 07 '12 at 13:24
  • 1
    Re: EDIT2: sounds like the method threw an exception, at which point the return value is undefined. `loadClass()` never returns null; it either returns the class reference or it throws an exception. – fadden May 09 '13 at 16:39
  • @fadden it could be. I don't remember exactly what was the issue, but I solved it eventually. – Pavel P May 09 '13 at 19:41

2 Answers2

79

After much trying and crashing of my app, a colleague and I managed to cache and succesfully use the class loader in another, native, thread. The code we used is shown below (C++11, but easily converted to C++2003), posted here since we couldn't find any examples of the aforementioned "Cache a reference to the ClassLoader object somewhere handy, and issue loadClass calls directly. This requires some effort.". Calling findClass worked perfectly when called from a thread different from the one of JNI_OnLoad. I hope this helps.

JavaVM* gJvm = nullptr;
static jobject gClassLoader;
static jmethodID gFindClassMethod;

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *pjvm, void *reserved) {
    gJvm = pjvm;  // cache the JavaVM pointer
    auto env = getEnv();
    //replace with one of your classes in the line below
    auto randomClass = env->FindClass("com/example/RandomClass");
    jclass classClass = env->GetObjectClass(randomClass);
    auto classLoaderClass = env->FindClass("java/lang/ClassLoader");
    auto getClassLoaderMethod = env->GetMethodID(classClass, "getClassLoader",
                                             "()Ljava/lang/ClassLoader;");
    gClassLoader = env->CallObjectMethod(randomClass, getClassLoaderMethod);
    gFindClassMethod = env->GetMethodID(classLoaderClass, "findClass",
                                    "(Ljava/lang/String;)Ljava/lang/Class;");

    return JNI_VERSION_1_6;
}

jclass findClass(const char* name) {
    return static_cast<jclass>(getEnv()->CallObjectMethod(gClassLoader, gFindClassMethod, getEnv()->NewStringUTF(name)));
}

JNIEnv* getEnv() {
    JNIEnv *env;
    int status = gJvm->GetEnv((void**)&env, JNI_VERSION_1_6);
    if(status < 0) {    
        status = gJvm->AttachCurrentThread(&env, NULL);
        if(status < 0) {        
            return nullptr;
        }
    }
    return env;
}
Jan Rüegg
  • 9,587
  • 8
  • 63
  • 105
Átila Neves
  • 1,351
  • 11
  • 14
  • 7
    `ClassLoader.loadClass()` does not return null when the class is not found. It throws an exception, which means the return value is undefined. You must check for exceptions after calling `CallObjectMethod`, and you must not use the return value if an exception has been raised. (This is true in general -- any `Call*Method` invocation should be followed by `ExceptionCheck` or `ExceptionOccurred`, and you can see the exception in the log with `ExceptionDescribe`.) – fadden May 09 '13 at 16:37
  • True, we didn't try to catch java exceptions, but it works for what we were doing and as much as I searched I didn't see any code online to solve this particular problem. It can and should be fleshed out but I posted to server as a guide for other people. It can be edited in the future. – Átila Neves May 13 '13 at 12:13
  • 6
    I understand; I just wanted to make sure that anybody who did copy it added the necessary checks to `JNI_OnLoad` and `findClass` (especially the latter -- you could convert an exception to NULL there, and clear the exception, to get the semantics you want). Note also you don't need to call `FindClass` to find `java.lang.Class`; you can just use `GetObjectClass` on the `jclass` you already have (it's faster and never fails on a valid object). Have you tried this on Android >= 4.0? Looks like you need a `NewGlobalRef` on `gFindClassMethod`. – fadden May 13 '13 at 16:35
  • Yeah. I ran it on a Galaxy Tab too with android 4.0.3. – Átila Neves May 21 '13 at 10:08
  • 1
    Cool. FWIW, I meant gClassLoader -- gFindClassMethod is a jmethodID. – fadden May 28 '13 at 16:32
  • 13
    Excelent answer... Just a comment, this didn't worked for me because of this : http://stackoverflow.com/questions/14765776/jni-error-app-bug-accessed-stale-local-reference-0xbc00021-index-8-in-a-tabl. After calling NewGlobalRef() on the gClassLoader object, this solved my problem. Thanks! – LarryPel Sep 24 '13 at 02:22
  • LarryPel, that seemed to be my issue as well, but it leaves me a bit confused; why can't you just load the specific class you actually need and then create a NewGlobalRef() to *that*, rather than going through this long procedure? Or am I misunderstanding how general this solution is? The fact that they need to instantiate randomClass made me think that it was a large number of steps just to load randomClass in future threads. – codetaku Jul 16 '14 at 18:45
  • Rather than getting the class loader from a random class, you may want to just get it from Thread.currentThread().getContextClassLoader() in JNI_OnLoad (via JNI of course), then cache it for later use. – Pete Blois Aug 14 '14 at 18:15
  • Thanks! I used a modified version of this approach to solve an issue with asynch callbacks coming from Google Play Games c++ sdk callbacks on non JVM threads that were then attempting to make calls over JNI to the Java side from within a cocos2d-x v2.2.5 project. (hopefully those key words in these comments will help someone get the answer quicker...) – Hunter-Orionnoir Nov 18 '14 at 01:15
  • 1
    Why is `classClass` needed? My code breaks if I instead pass `randomClass` into `GetMethodID`, but I don't know why, since it should be the same thing, right? Is it because the class in question is `com/*`? How did you know to do this, and are there any references or documentation you can point me to? Thanks! – bunkerdive Jun 19 '15 at 12:31
  • 7
    thanks for the solution. note you have to call `NewGlobalRef()` BEFORE this gets stored globally. calling `NewGlobalRef()` from a different thread isn't going to work. Do it like such: `gClassLoader = env->NewGlobalRef(env->CallObjectMethod(myClass, getClassLoaderMethod));` For more details check here: http://android-developers.blogspot.kr/2011/11/jni-local-reference-changes-in-ics.html – Andy Matteson Oct 14 '15 at 02:00
  • 1
    Pete, I was really hoping that calling Thread.currentThread().getContextClassLoader() in JNI_OnLoad would work (since it doesn't require knowing the name of some class in the non-system module, which is ugly) but sadly it does not work. That ClassLoader is NOT the one that knows about my non-system classes and fails when I later try to ClassLoader.findClass them (even if I do it from JNI_OnLoad itself). Oh well, good idea. As a double-check I did it both ways and confirmed that env->IsSameObject is FALSE for the two ClassLoaders. – Louis Semprini Feb 23 '16 at 16:49
  • 1
    Also, for people are implementing this, be aware that the ClassLoader you extract (the one that knows about your classes) does NOT know about certain Android classes like android.app.Activity. So you really have to try both in any general-purpose findClass routine. SIGH---could they make JNI any harder to use? – Louis Semprini Feb 26 '16 at 10:10
  • @bunkerdive: `GetObjectClass(randomClass)` is a simple way of obtaining a reference to `java.lang.Class`, which is needed for the `getMethodID` call since `getClassLoader` is a member of `java.lang.Class`. – Michael Feb 10 '17 at 09:41
  • Is the classloader thread-safe? Or can there be a race-condition if two threads call `findClass` on the same class loader reference at the same time? Is there a difference between `findClass` and `loadClass`? The string returned by NewStringUtf probably needs to be deleted with DeleteLocalRef? – BeniBela Jun 24 '18 at 14:37
5

Try attaching your native thread to the JVM first.

The pointer to jvm you can obtain first thing in JNI_OnLoad

env->GetJavaVM(&jvm);

Then from your native thread

JNIEnv *env;
jvm->AttachCurrentThread((void **)&env, NULL);

Then use that env for FindClass

Alexander Kulyakhtin
  • 47,782
  • 38
  • 107
  • 158