Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the best way to save JNIEnv*

I have an Android project with JNI. In the CPP file which implements a listener class, there is a callback x() . When x() function is called, I want to call another function in a java class. However, in order to invoke that java function, I need to access JNIEnv*.

I know that in the same cpp file of the callback, there is a function:

static jboolean init (JNIEnv* env, jobject obj) {...}

Should I save in the cpp file JNIEnv* as member variable when init(..) is called? and use it later when the callback happens?

Sorry but I am a beginner in JNI.

like image 311
lyc001 Avatar asked May 04 '15 08:05

lyc001


People also ask

What is JNIEnv?

JNIEnv – a structure containing methods that we can use our native code to access Java elements. JavaVM – a structure that lets us manipulate a running JVM (or even start a new one) adding threads to it, destroying it, etc…

What is JNI and how it works?

JNI is the Java Native Interface. It defines a way for the bytecode that Android compiles from managed code (written in the Java or Kotlin programming languages) to interact with native code (written in C/C++).

What is Jclass in JNI?

The jclass instance is your object on which a method will be invoked; you'll need to look up the getName method ID on the Class class, then invoke it on the jclass instance using CallObjectMethod to obtain a jstring result. So in short yes, you just call the getName function and look at the jstring result.

How does Android APK execute compiled native code using the Java native interface?

Using Android Studio 2.2 and higher, you can use the NDK to compile C and C++ code into a native library and package it into your APK using Gradle, the IDE's integrated build system. Your Java code can then call functions in your native library through the Java Native Interface (JNI) framework.


2 Answers

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.

like image 112
Michael Avatar answered Oct 27 '22 22:10

Michael


@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;
}
like image 32
Gideon Gyabaah Avatar answered Oct 27 '22 23:10

Gideon Gyabaah