FindClass from any thread in Android JNI - android

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.

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;
}

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

Related

Using AAssetManager_fromJava within plugin not directly called from Java VM (called from Unity)

I'm using Android NDK and need access to assets. A requirement for asset access seems to be obtaining an AssetManager reference.
Looking at the NDK samples (https://github.com/android/ndk-samples), the pattern seems to be:
A JNIEnv* is passed into the func when called directly from the JavaVM, along with some jobject
Use these to get AAssetManager* and then use this to open assets
That seems simple enough, except in my case, the functions are being called from Unity so I don't have access to either a JNIEnv* or jobject. Getting the JNIEnv* seems easy enough as I can make use of JNI_OnLoad to get access to a JavaVM* and then use that to get a JNIEnv* via vm->GetEnv. My questions about this are:
1) My understanding is that, an Android app can only have one instance of a Java VM. Am I safe to take the JavaVM* passed into JNI_OnLoad and save it for use in other function calls?
2) What about the JNIEnv*? Can I grab that once during JNI_OnLoad and save it, or should I grab a fresh one every time I need to use assets within a function? Is JNIEnv* something I need to explicitly free? (i.e. what's the lifetime/ownership situation with JNIEnv*?)
3) AAssetManager_fromJava also requires a jobject with the documentation (https://developer.android.com/ndk/reference/group/asset#group___asset_1gadfd6537af41577735bcaee52120127f4) saying: "Note that the caller is responsible for obtaining and holding a VM reference to the jobject to prevent its being garbage collected while the native object is in use.". I've seem some examples that simply pass in an empty (native) string like AAssetManager_fromJava(env, ""); - is that ok? I'd only be using the AssetManager for the lifetime of that call, and I could get a fresh one each time. (Again, is AAssetManager* a resource I need to manage, or am I just getting a reference to something owned elsewhere? The documentation seems to imply the latter.)
4) So given all the above, I'd probably do something like:
JavaVM* g_vm;
JNIEnv* g_env;
jint JNI_OnLoad(JavaVM* vm, void* reserved) {
g_vm = vm;
g_vm->GetEnv((void **)&g_env, JNI_VERSION_1_6); // TODO: error checking
return JNI_VERSION_1_6;
}
void do_asset_stuff() {
AAssetManager* mgr = AAssetManager_fromJava(g_env, "");
// do stuff...
}
Is that reasonable? No memory/resource leak issues? Any issues with multi-threading?
Thanks!
EDIT: Seems like there are some threading considerations with JNIEnv*. See: Unable to get JNIEnv* value in arbitrary context
Point-by point answer to your questions:
Yes, there can be only one VM in Android. You are allowed to store this pointer or use JNI_GetCreatedJavaVMs.
JNIEnv pointers are tightly coupled to the thread they were created on. In your situation you will first have to attach the thread to the VM using AttachCurrentThread. This will fill in a JNIEnv * for you. Don't forget to DetachCurrentThread when you're done.
Also note the caveat about FindClass: you need to look up classes from the main thread or via the classloader of a class you looked up in the main thread.
The implementation of AAssetmanager_fromJava is pretty clear: passing it anything other than an AssetManager object is undefined behavior. This answer shows one approach to grabbing the asset manager, another might be to call your own JNI function with a reference to the AssetManager object. Make sure to keep a global reference so it does not get GCed.
Given the above, it would probably look more like this:
JavaVM* g_vm;
jobject cached_assetmanager;
jint JNI_OnLoad(JavaVM* vm, void* reserved) {
g_vm = vm;
return JNI_VERSION_1_6;
}
void do_asset_stuff() {
JNIEnv *env;
JavaVMAttachArgs args = { JNI_VERSION_1_6, "my cool thread", NULL };
g_vm->AttachCurrentThread((void **)&env, &args);
AAssetManager* mgr = AAssetManager_fromJava(g_env, cached_assetmanager);
// do stuff...
}
// Assuming you call `com.shhhsecret.app.storeassetmanager(mgr)` somewhere.
void Java_com_shhhsecret_app_storeassetmanager(JNIEnv *env, jclass cls, jobject am) {
cached_assetmanager = env->NewGlobalRef(am);
}
I was able to read a json file from Unity c++ plugin.
I had to extend UnityPlayerActivity to get assetManager as jobject.
The tricky part also was to find the correct path to the asset in the plugin:
I placed it into StreamingAssets/data and was able to read using this path 'data/myfile'
see my comment with the code:
unity answers
Seems Botje's answer is precise (pity, I did not have it earlier)
Thought I'd post what I ended up doing in case it's of help to others...
#include <jni.h>
#include <android/asset_manager.h>
#include <android/asset_manager_jni.h>
JavaVM* g_JavaVM;
jobject g_JavaAssetManager;
bool g_Initialized = false;
jint JNI_OnLoad(JavaVM* vm, void* reserved) {
g_JavaVM = vm;
return JNI_VERSION_1_6;
}
// call this once from the main thread in C# land:
extern "C" void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API NativeInit() {
if (g_Initialized) { return; }
g_Initialized = true;
JNIEnv* env = nullptr;
jint get_env_result = g_JavaVM->GetEnv((void **)&env, JNI_VERSION_1_6);
if (get_env_result == JNI_EDETACHED) {
jint attach_thread_result = g_JavaVM->AttachCurrentThreadAsDaemon(&env, nullptr);
if (attach_thread_result != 0) {
return;
}
get_env_result = JNI_OK;
}
if (env == nullptr || get_env_result != JNI_OK) {
return;
}
jclass unity_player = env->FindClass("com/unity3d/player/UnityPlayer");
jfieldID static_activity_id = env->GetStaticFieldID(unity_player, "currentActivity","Landroid/app/Activity;");
jobject unity_activity = env->GetStaticObjectField(unity_player, static_activity_id);
jmethodID get_assets_id = env->GetMethodID(env->GetObjectClass(unity_activity), "getAssets", "()Landroid/content/res/AssetManager;");
jobject java_asset_manager = env->CallObjectMethod(unity_activity, get_assets_id);
g_JavaAssetManager = env->NewGlobalRef(java_asset_manager);
}
Now g_JavaAssetManager can be used in any thread to call AAssetManager_fromJava.

.. CLOSED .. JNI error 'java_class == null' Android Studio

This is closed. New problem will be addressed in a new question.
See edit for latest problem. I am trying to pass a Vector3 value from my cpp library to my java activity. I am able to do it vice versa, but cannot seem to find a way to go cpp to java. Anyone mine helping me out with this? I am receving this error: undefined reference to 'jni_createjavavm'
JavaVM *jvm; /* denotes a Java VM */
JNIEnv *env; /* pointer to native method interface */
JavaVMInitArgs vm_args; /* JDK/JRE 6 VM initialization arguments */
JavaVMOption* options = new JavaVMOption[1];
options[0].optionString = "-Djava.class.path=/usr/lib/java";
vm_args.version = JNI_VERSION_1_6;
vm_args.nOptions = 1;
vm_args.options = options;
vm_args.ignoreUnrecognized = false;
/* load and initialize a Java VM, return a JNI interface
* pointer in env */
JNI_CreateJavaVM(&jvm, &env, &vm_args);
delete options;
/* invoke the Main.test method using the JNI */
jclass cls = env->FindClass("MenuActivity");
jmethodID mid = env->GetStaticMethodID(cls, "Test", "(I)V");
env->CallStaticVoidMethod(cls, mid);
/* We are done. */
jvm->DestroyJavaVM();
Nov 11 2018 #2031 UTC+9 | EDIT: New Problem.. Crashes with java_class == NULL.
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
//Some Other Code Not Regarding JVM
JNIEnv *env;
vm->AttachCurrentThread(&env, NULL);
jclass cls = env->FindClass("MenuActivity");
jmethodID mid = env->GetStaticMethodID(cls, "Test", "(I)V");
env->CallStaticVoidMethod(cls, mid);
return JNI_VERSION_1_6;
}
On Android, there is no JNI_CreateJavaVM(). The apps run in JVM which is essential to access system APIs and services.
The callbacks from native code to the Java part of the app use the JNIEnv * that must belong to the current thread.
If this runs on a Java thread, the JNIEnv is received as the first parameter by the native method. You can call back to Java from a native thread, too. But then, you must attach the thread to JVM. AttachCurrentThread() accepts JavaVM * which can be stored as a global in your native code. You can obtain it in JNI_OnLoad() or derive it from JNIEnv with GetJavaVM().
Each native thread that is attached, must be detached on termination. The best practice is to use pthread_key_create() to define a destructor function that will be called before the thread exits.
You can read more explanations in the Android JNI tips article.

C++ signal handler can't notify Java side

I have the following CPP code. What I want to do is when an error occurs in my native side, I will notify Java about the error. I used How can I catch SIGSEGV (segmentation fault) and get a stack trace under JNI on Android? as reference.
static JavaVM* g_JVM = NULL;
static jobject g_thejavaobject = NULL;
void InitializeNativeSide(JNIEnv *env, jclass, jobject object)
{
env->GetJavaVM(&g_JVM);
g_thejavaobject = env->NewGlobalRef(object);
}
// this executes in another thread running in parallel with UI thread
void StartExecuting(JNIEnv *_env, jclass) {
struct sigaction sa;
memset(&sa, 0, sizeof(struct sigaction));
sigemptyset(&sa.sa_mask);
sa.sa_sigaction = SignalErrorHandler;
sa.sa_flags = SA_SIGINFO;
sigaction(SIGSEGV, &sa, NULL);
// native starts executing here. after a while, a SEGFAULT is encountered
// triggering SignalErrorHandler()
...
}
void SignalErrorHandler(int signal, siginfo_t *si, void *arg)
{
JNIEnv *env;
g_JVM->GetEnv((void**)&env, JNI_VERSION_1_6);
jclass myClass = env->FindClass("com/company/MyClass");
jmethodID myMethod = env->GetMethodID(myClass, "nativeCrashed", "()V" );
env->CallVoidMethod(g_thejavaobject, myMethod);
env->DeleteLocalRef(myClass);
}
Everything works fine but the call to myClass.nativeCrashed() does not work. What am I doing wrong?
You can't do this:
void SignalErrorHandler(int signal, siginfo_t *si, void *arg)
{
JNIEnv *env;
g_JVM->GetEnv((void**)&env, JNI_VERSION_1_6);
jclass myClass = env->FindClass("com/company/MyClass");
jmethodID myMethod = env->GetMethodID(myClass, "nativeCrashed", "()V" );
env->CallVoidMethod(g_thejavaobject, myMethod);
env->DeleteLocalRef(myClass);
}
That will not work for at least two fundamental reasons.
First, only async-signal-safe functions may be called from within a signal handler. The POSIX-specified list can be found at http://pubs.opengroup.org/onlinepubs/9699919799/functions/V2_chap02.html#tag_15_04_03.
No Java JNI call is async-signal-safe.
Second, the Java JVM uses SIGSEGV internally - getting a SIGSEGV is not necessarily fatal:
Signals Used in Oracle Solaris, Linux, and macOS
...
SIGSEGV, SIGBUS, SIGFPE, SIGPIPE, SIGILL These signals are used in
the implementation for implicit null check, and so forth.

Invalid indirect reference 0x416f68a0 in decodeIndirectRef

I've seen plenty of questions about exactly the same error, but none of them seems to be trying to do this simple thing and still fail.
I have in my class header, as private members:
static JNIEnv* env;
static jclass copterServiceClass;
static jmethodID mavlinkMsgMethod;
Then in the source for that class:
JNIEnv* JU_Calls::env = 0;
jclass JU_Calls::copterServiceClass = 0;
jmethodID JU_Calls::mavlinkMsgMethod = 0;
bool JU_Calls::setupJNICalls() {
if (cached_jvm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
__android_log_print(ANDROID_LOG_ERROR, TAG, "Unable to get Java Env from cached JavaVM");
return -1;
}
jclass dataClass = env->FindClass("eu/deye/copterdroidair/copterdroidair/Services/CopterService");
copterServiceClass = (jclass) env->NewGlobalRef(dataClass);
mavlinkMsgMethod = env->GetMethodID(copterServiceClass, "MavlinkMsg", "(Ljava/lang/String;)V");
jobject javaObjectRef = env->NewObject(copterServiceClass, mavlinkMsgMethod);
jstring msg = env->NewStringUTF("aaaa");
env->CallVoidMethod(javaObjectRef, mavlinkMsgMethod, msg);
return true;
}
Note: cached_jvm is assigned at JNI_OnLoad.
It fails with the aforementioned error when executing NewObject.
Invalid indirect reference 0x416f68a0 in decodeIndirectRef
I tried passing NULL to the CallVoidMethod as the message, as seen on other's questions, but as it's logical, the problem is before, so useless.
Hope you can help me, as always, great SO masters ;)
EDIT: While I think Bangyno answer is the right one, what I've eventually done to solve the problem as quickest as possible, was to declare the Java Methods that would be called from C++ as static. That way I don't have to call the constructor, and everything makes a lot more sense. Because the Java class to which I was calling, was an Android Service, thus calling the constructor was erroneous for sure.
Apart from declaring the Java method static, the resulting C++ code results as follows:
jclass copterServiceClass = env->FindClass("eu/deye/copterdroidair/copterdroidair/Services/CopterService");
jmethodID mavlinkMsgMethod = env->GetStaticMethodID(copterServiceClass, "MavlinkMsg", "(ILjava/lang/String;)V");
jstring msg = env->NewStringUTF(str);
env->CallStaticVoidMethod(copterServiceClass, mavlinkMsgMethod, severity, msg);
env->DeleteLocalRef(msg);
Is very important not to forget the last line, because otherwise it will fill your JNI table and crash.
Here I want to discuss is the error message:
Invalid indirect reference 0x416f68a0 in decodeIndirectRef
It means you give the wrong argument, the second one "mavlinkMsgMethod".
In my experience, if you change "mavlinkMsgMethod" to a number such as "5", the 0x416f68a0 will change to 0x5.
The right way to use newObject is use to invoke the constructor. It should look like this, just a sample:
jclass dataClass = env->FindClass("eu/deye/copterdroidair/copterdroidair/Services/CopterService");
mavlinkMsgMethod = env->GetMethodID(copterServiceClass, "<init>", "(Ljava/lang/String;)V");
jstring str = env->NewStringUTF("testing");
jobject javaObjectRef = env->NewObject(copterServiceClass, mavlinkMsgMethod, str);

Android JNI ReleaseByteArrayElements does not work

i have got a problem at using Android JNI. I call a java method from native C. Everything works fine, BUT after a few seconds the APP crashes, because the maximum of 512 entrys of JNI refs is full (or the memory is full).
Here is my code:
int jniBluetoothSend( TJNIAdapter *pAdapter, byte *data, int dwLength )
{
JNIEnv *pEnv;
JavaVM *pVm = NULL;
jclass cls;
jmethodID methodId;
int nRet;
jbyteArray aData;
if ( pAdapter )
{
pVm = pAdapter->pVm;
( *pVm )->AttachCurrentThread( pVm, &pEnv, NULL );
if ( pAdapter->pClasses != NULL && pAdapter->pClasses->hgs_bluetooth != NULL )
{
// get function
methodId = ( *pEnv )->GetMethodID( pEnv, pAdapter->pClasses->hgs_bluetooth, "write", "([BI)I" );
if ( methodId )
{
aData = ( *pEnv )->NewByteArray( pEnv, dwLength);
( *pEnv )->SetByteArrayRegion( pEnv, aData, 0, dwLength, data);
// write Data to device
nRet = ( *pEnv )->CallIntMethod( pEnv, g_hgsBthObject, methodId, aData, dwLength );
// and delete Reference -> so GC can cleanup
( *pEnv )->DeleteLocalRef( pEnv, aData );
}
}
//( *pVm )->DetachCurrentThread( pVm ); -> // crashes as soon as getting called
}
return nRet;
}
Everytime "ReleaseByteArrayElements()" gets called a warning shows up in Android Log from dalvik:
JNI: unpinPrimitiveArray(0x428e84a8) failed to find entry (valid=1)
So, i think, the problem is, that the created array is not getting freed. But I don't know, how to do this in the right way.
Can anyone help me with this?
Thanks!
EDIT
I have done a several tests.
If i add the DetachCurrentThread function to the bottom of the second if(), the APP crashes, if DetachCurrentThread is getting called.
But i have added DeleteLocalRef in an other function, and the APP does not crash anymore.
So my question is: Do i have to call DetachCurrentThread in every function i call AttachCurrentThread or is it enough if i call that once to the end of the APP?
Also updated the Code
Everytime ReleaseByteArrayElements() gets called a warning shows up in Android Log from dalvik:
JNI: unpinPrimitiveArray(0x428e84a8) failed to find entry (valid=1)
ReleaseByteArrayElements is meant to be paired with a previous call to GetByteArrayElements. From Oracle's documentation:
Release<PrimitiveType>ArrayElements Routines
void Release<PrimitiveType>ArrayElements(JNIEnv *env,
ArrayType array, NativeType *elems, jint mode);
A family of functions that informs the VM that the native code no longer needs access to elems. The elems argument is a pointer derived from array using the corresponding Get<PrimitiveType>ArrayElements() function.
Since you didn't call GetByteArrayElements you also shouldn't call ReleaseByteArrayElements.
Note that SetByteArrayRegion() function is not one of the functions that pin an array, therefore it does not require a release.

Categories

Resources