What is the best way to save JNIEnv*
Caching a JNIEnv*
is not a particularly good idea, since you can't use the same JNIEnv*
across multiple threads, and might not even be able to use it for multiple native calls on the same thread (see http://android-developers.blogspot.se/2011/11/jni-local-reference-changes-in-ics.html)
Writing a function that gets the JNIEnv*
and attaches the current thread to the VM if necessary isn't too difficult:
bool GetJniEnv(JavaVM *vm, JNIEnv **env) {
bool did_attach_thread = false;
*env = nullptr;
// Check if the current thread is attached to the VM
auto get_env_result = vm->GetEnv((void**)env, JNI_VERSION_1_6);
if (get_env_result == JNI_EDETACHED) {
if (vm->AttachCurrentThread(env, NULL) == JNI_OK) {
did_attach_thread = true;
} else {
// Failed to attach thread. Throw an exception if you want to.
}
} else if (get_env_result == JNI_EVERSION) {
// Unsupported JNI version. Throw an exception if you want to.
}
return did_attach_thread;
}
The way you'd use it is:
JNIEnv *env;
bool did_attach = GetJniEnv(vm, &env);
// Use env...
// ...
if (did_attach) {
vm->DetachCurrentThread();
}
You could wrap this in a class that attaches upon construction and detaches upon destruction, RAII-style:
class ScopedEnv {
public:
ScopedEnv() : attached_to_vm_(false) {
attached_to_vm_ = GetJniEnv(g_vm, &env_); // g_vm is a global
}
ScopedEnv(const ScopedEnv&) = delete;
ScopedEnv& operator=(const ScopedEnv&) = delete;
virtual ~ScopedEnv() {
if (attached_to_vm_) {
g_vm->DetachCurrentThread();
attached_to_vm_ = false;
}
}
JNIEnv *GetEnv() const { return env_; }
private:
bool attached_to_env_;
JNIEnv *env_;
};
// Usage:
{
ScopedEnv scoped_env;
scoped_env.GetEnv()->SomeJniFunction();
}
// scoped_env falls out of scope, the thread is automatically detached if necessary
Edit: Sometimes you might have a long-ish running native thread that will need a JNIEnv*
on multiple occasions. In such situations you may want to avoid constantly attaching and detaching the thread to/from the JVM, but you still need to make sure that you detach the thread upon thread destruction.
You can accomplish this by attaching the thread only once and then leaving it attached, and by setting up a thread destruction callback using pthread_key_create
and pthread_setspecific
that will take care of calling DetachCurrentThread
.
/**
* Get a JNIEnv* valid for this thread, regardless of whether
* we're on a native thread or a Java thread.
* If the calling thread is not currently attached to the JVM
* it will be attached, and then automatically detached when the
* thread is destroyed.
*/
JNIEnv *GetJniEnv() {
JNIEnv *env = nullptr;
// We still call GetEnv first to detect if the thread already
// is attached. This is done to avoid setting up a DetachCurrentThread
// call on a Java thread.
// g_vm is a global.
auto get_env_result = g_vm->GetEnv((void**)&env, JNI_VERSION_1_6);
if (get_env_result == JNI_EDETACHED) {
if (g_vm->AttachCurrentThread(&env, NULL) == JNI_OK) {
DeferThreadDetach(env);
} else {
// Failed to attach thread. Throw an exception if you want to.
}
} else if (get_env_result == JNI_EVERSION) {
// Unsupported JNI version. Throw an exception if you want to.
}
return env;
}
void DeferThreadDetach(JNIEnv *env) {
static pthread_key_t thread_key;
// Set up a Thread Specific Data key, and a callback that
// will be executed when a thread is destroyed.
// This is only done once, across all threads, and the value
// associated with the key for any given thread will initially
// be NULL.
static auto run_once = [] {
const auto err = pthread_key_create(&thread_key, [] (void *ts_env) {
if (ts_env) {
g_vm->DetachCurrentThread();
}
});
if (err) {
// Failed to create TSD key. Throw an exception if you want to.
}
return 0;
}();
// For the callback to actually be executed when a thread exits
// we need to associate a non-NULL value with the key on that thread.
// We can use the JNIEnv* as that value.
const auto ts_env = pthread_getspecific(thread_key);
if (!ts_env) {
if (pthread_setspecific(thread_key, env)) {
// Failed to set thread-specific value for key. Throw an exception if you want to.
}
}
}
If __cxa_thread_atexit
is available to you, you might be able to accomplish the same thing with some thread_local
object that calls DetachCurrentThread
in its destructor.
@Michael, gives a good overview of how best to retrieve the JNI by caching the JVM. For those that dont want to use pthread (or cant' because you are on Windows system), and you are using c++ 11 or highter, then thread_local storage is the way to go.
Bellow is rough example on how to implement a wrapper method that properly attaches to a thread and automatically cleans-up when the thread exits
JNIEnv* JNIThreadHelper::GetJniEnv() {
// This method might have been called from a different thread than the one that created
// this handler. Check to make sure that the JNI is attached and if not attach it to the
// new thread.
// double check it's all ok
int nEnvStat = m_pJvm->GetEnv(reinterpret_cast<void**>(&m_pJniEnv), JNI_VERSION_1_6);
if (nEnvStat == JNI_EDETACHED) {
std::cout << "GetEnv: not attached. Attempting to attach" << std::endl;
JavaVMAttachArgs args;
args.version = JNI_VERSION_1_6; // choose your JNI version
args.name = NULL; // you might want to give the java thread a name
args.group = NULL; // you might want to assign the java thread to a ThreadGroup
if (m_pJvm->AttachCurrentThread(&m_pJniEnv, &args) != 0) {
std::cout << "Failed to attach" << std::endl;
return nullptr;
}
thread_local struct DetachJniOnExit {
~DetachJniOnExit() {
m_pJvm->DetachCurrentThread();
}
};
m_bIsAttachedOnAThread = true;
}
else if (nEnvStat == JNI_OK) {
//
}
else if (nEnvStat == JNI_EVERSION) {
std::cout << "GetEnv: version not supported" << std::endl;
return nullptr;
}
return m_pJniEnv;
}