I have JNI native code that calls C++ and it starts background threads. In one of those threads, JNI is called but it ends with error:
JNI DETECTED ERROR IN APPLICATION: mid == null in call to CallVoidMethodV
Runtime aborting...
The code crashes on call:
lockedEnv->CallVoidMethod(cachedCaller,
javaCallback,
snapshotData);
Interesting is that the problem (crash) orrurs in the Release build of C++ with optimizations while debug mode works correctly. Also, I am using the JNIClass wrapper on other places and no problems occur.
My JNI method is:
JNIClass * snapshotDataClass;
#define RETURN(type) JNIEXPORT type JNICALL
#define PACKAGE(fn) Java_com_example_NativeAPI__##fn
jbyteArray convertToJavaByteArray(JNIEnv *env, const jbyte *array, int elementCount) {
if (elementCount == 0){
return env->NewByteArray(1);
}
if (array == nullptr){
return env->NewByteArray(elementCount);
}
jbyteArray res = env->NewByteArray(elementCount);
env->SetByteArrayRegion(res, 0, elementCount, array);
return res;
}
RETURN(void) PACKAGE(getData)(JNIEnv *env, jobject thiz, jobject caller) {
if (dataClass == nullptr) {
dataClass = new JNIClass(env, "com/example/Data");
dataClass ->AddMethod("<init>", "()V");
dataClass ->AddMethod("setData", "([B)V");
}
jclass callbackClass = (jclass) env->NewGlobalRef(env->FindClass("com/example/Data/DataListener"));
jobject cachedCaller = (jobject) env->NewGlobalRef(caller);
jmethodID javaCallback = env->GetMethodID(callbackClass, "onSnapshotRetrieved", "(Lcom/example/Data;)V");
CppGetData([=](Result res) {
//this is called from thread
JNIEnv *lockedEnv = dataClass->LockEnv();
dataClass->InitNewInstance();
dataClass->RunVoid("setData", convertToJavaByteArray(lockedEnv, reinterpret_cast<const jbyte *>(res.data), res.size));
lockedEnv->CallVoidMethod(cachedCaller,
javaCallback,
dataClass->instanceRef);
lockedEnv->DeleteGlobalRef(callbackClass);
lockedEnv->DeleteGlobalRef(cachedCaller);
dataClass->UnLockEnv();
});
}
And JNI class is a helper wrapper:
struct SimpleJNIClass {
JavaVM* vm = nullptr;
jobject instanceRef = nullptr;
JNIEnv *lockedEnv = nullptr;
SimpleJNIClass(JNIEnv *env, jobject instanceRef){
env->GetJavaVM(&this->vm);
this->instanceRef = (jobject)env->NewGlobalRef(instanceRef);
};
SimpleJNIClass(JNIEnv * env){
env->GetJavaVM(&this->vm);
};
SimpleJNIClass(const SimpleJNIClass & ) = delete;
virtual ~SimpleJNIClass() {
if (this->vm == nullptr) {
return;
}
if (JNIEnv * env = GetEnv()){
env->DeleteGlobalRef(this->instanceRef);
this->instanceRef = nullptr;
FreeEnv(env);
}
};
JNIEnv * LockEnv() {
this->envMutex.lock();
this->lockedEnv = this->GetEnv();
return this->lockedEnv;
};
void UnLockEnv() {
this->lockedEnv = nullptr;
this->FreeEnv(this->lockedEnv);
this->envMutex.unlock();
};
protected:
bool attachCurrentThread = false;
std::recursive_mutex envMutex;
JNIEnv * GetEnv(){
if (this->lockedEnv != nullptr){
return this->lockedEnv;
}
JNIEnv * env = nullptr;
int getEnvStat = vm->GetEnv((void **)&env, JNI_VERSION_1_4);
if (getEnvStat == JNI_EDETACHED){
if (vm->AttachCurrentThread(&env, NULL) != 0){
MY_LOG_ERROR("AttachCurrentThread was not successful. "
"This may be due to the thread being attached already to another JVM instance.");
}
else{
attachCurrentThread = true;
}
}
else if (getEnvStat == JNI_OK){
//no need to attach, already attached
}
else if (getEnvStat == JNI_EVERSION){
MY_LOG_ERROR("GetEnv: version not supported");
}
return env;
};
void FreeEnv(JNIEnv * env){
if (this->lockedEnv != nullptr){
return;
}
if (attachCurrentThread){
vm->DetachCurrentThread();
attachCurrentThread = false;
}
};
};
struct JNICallback : public SimpleJNIClass {
jmethodID callbackMethod = nullptr;
JNICallback(JNIEnv *env, jobject classRef,
const std::string & methodName, const std::string & methodSignature)
: SimpleJNIClass(env, classRef){
jclass callbackClass = (jclass)env->GetObjectClass(this->instanceRef);
this->callbackMethod = env->GetMethodID(callbackClass, methodName.c_str(), methodSignature.c_str());
};
JNICallback(const SimpleJNIClass & ) = delete;
virtual ~JNICallback() {
callbackMethod = nullptr;
};
};
struct JNIClass : public SimpleJNIClass {
jclass callbackClass;
std::unordered_map<std::string, jmethodID> callbacks;
JNIClass(JNIEnv *env, jobject classRef)
: SimpleJNIClass(env, classRef){
this->callbackClass = (jclass)env->NewGlobalRef((jclass)env->GetObjectClass(this->instanceRef));
};
JNIClass(JNIEnv *env, const std::string & classRefName)
: SimpleJNIClass(env){
this->callbackClass = (jclass)env->NewGlobalRef(env->FindClass(classRefName.c_str()));
};
virtual ~JNIClass() {
if (JNIEnv * env = this->GetEnv()){
env->DeleteGlobalRef(callbackClass);
callbackClass = nullptr;
this->FreeEnv(env);
}
callbacks.clear();
};
void AddMethod(const std::string & methodName, const std::string & methodSignature) {
if (this->lockedEnv != nullptr){
this->callbacks[methodName] = this->lockedEnv->GetMethodID(this->callbackClass, methodName.c_str(), methodSignature.c_str());
return;
}
JNIEnv *env = this->LockEnv();
this->callbacks[methodName] = env->GetMethodID(this->callbackClass, methodName.c_str(), methodSignature.c_str());
this->UnLockEnv();
};
template<typename... Args>
void InitNewInstance(Args... args){
if (this->lockedEnv != nullptr){
jobject inst = this->lockedEnv->NewObject(this->callbackClass, this->callbacks["<init>"], args...);
this->SetInstance(inst);
return;
}
JNIEnv *env = this->LockEnv();
jobject inst = env->NewObject(this->callbackClass, this->callbacks["<init>"], args...);
this->SetInstance(inst);
this->UnLockEnv();
};
template<typename... Args>
void RunVoid(const std::string & name, Args... args) {
if (this->lockedEnv != nullptr){
this->lockedEnv->CallVoidMethod(this->instanceRef, this->callbacks[name], args...);
return;
}
JNIEnv *env = this->LockEnv();
env->CallVoidMethod(this->instanceRef, this->callbacks[name], args...);
this->UnLockEnv();
};
};
I have tried to simplify code as much as possible, but with JNI its hard.