In JNI, how do I cache the class, methodID, and fieldIDs per IBM's performance recommendations?

16,264

Solution 1

There is no built-in methodology to follow, however here is a somewhat standard, clean, and repeatable implementation showing how I practice IBM's recommendation.

I am going to assume you are calling your DLL from Java and you are referencing it multiple times throughout your application life-cycle.

The sample Native Java Class is named org.stackoverflow.jni.NativeClazz, which will implement the 2 built-in JNI methods JNI_OnLoad() and JNI_OnUnload().

void JNI_OnLoad(JavaVM *vm, void *reserved): This method will be used to register the Class IDs as global variables and assign the Method IDs and Field IDs to static variables. The method is automatically called when the driver is loaded by the Java VM; it is only called once during the driver life-cycle.

void JNI_OnUnload(JavaVM *vm, void *reserved): This method will be used to free any global variables registered by JNI_OnLoad(). The VM will automatically call JNI_OnUnload() immediately prior to application shutdown.

Rationale: It's my understanding the Class IDs must be registered as global references to maintain the viability of any associated Method ID / Field IDs. If this isn't done and the class is unloaded from the JVM, on class reload, the Method IDs / Field IDs may be different. If the Class ID is registered as a global reference, the associated Method IDs and Field IDs do not need to be registered as global references. Registering a Class ID as a global reference prevents the associated Java class from unloading, therefore stabilizing the Method ID / Field ID values. Global references, including the Class IDs should be removed in JNI_OnUnload().

Method IDs and Field IDs are not managed by the native code; they are managed by the virtual machine and are valid until the associated class is unloaded. Field IDs and Method IDs cannot be explicitly deleted before the virtual machine unloads the defining class; they can be left for the VM to handle after unload.

Sample Code

Comments in the following C++ code sections explain registering variables globally.

Here is the Java class BeanObject representing a data object:

package org.stackoverflow.data;

public class BeanObject {

    String foo = "";
    
    public String getFoo() {
     
        return foo;
    }
}

Here is a skeleton Java class NativeClazz:

package org.stackoverflow.jni;

import org.stackoverflow.data.BeanObject;

public class NativeClazz {

    // Static area for forced initialization
    static {

        // Load Native Library (C++); calls JNI_OnLoad()
        System.loadLibrary("Native_Library_File_Name");
    }       

    /**
     * A static native method you plan to call.
     */
    public static native void staticNativeMethod(BeanObject bean);

    /**
     * A non-static native method you plan to call, to show this also works with 
     * Java class instances.
     */
    public native void instanceNativeMethod(BeanObject bean);
}

Here is the C++ header file "org_stackoverflow_jni_NativeClazz.h" generated using javah on NativeClazz:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class org_stackoverflow_jni_NativeClazz */

#ifndef _Included_org_stackoverflow_jni_NativeClazz
#define _Included_org_stackoverflow_jni_NativeClazz
#ifdef __cplusplus
extern "C" {
#endif

/*
 * Class:     org_stackoverflow_jni_NativeClazz_staticNativeMethod
 * Method:    staticNativeMethod
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_org_stackoverflow_jni_NativeClazz_staticNativeMethod
  (JNIEnv *, jclass, jobject);

/*
 * Class:     org_stackoverflow_jni_NativeClazz_instanceNativeMethod
 * Method:    instanceNativeMethod
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_org_stackoverflow_jni_NativeClazz_instanceNativeMethod
  (JNIEnv *, jobject, jobject);

#ifdef __cplusplus
}
#endif
#endif

Here is the C++ .cpp file implementing the header file:

#include "org_stackoverflow_jni_NativeClazz.h"

using namespace std;

/**************************************************************
 * Static Global Variables to cache Java Class and Method IDs
 **************************************************************/
static jclass JC_BeanObject;              // declare for each class
static jmethodID JMID_BeanObject_getFoo;  // declare for each class method

/**************************************************************
 * Declare JNI_VERSION for use in JNI_Onload/JNI_OnUnLoad
 * Change value if a Java upgrade requires it (prior: JNI_VERSION_1_6)
 **************************************************************/
static jint JNI_VERSION = JNI_VERSION_1_8;

/**************************************************************
 * Initialize the static Class and Method Id variables
 **************************************************************/
jint JNI_OnLoad(JavaVM* vm, void* reserved) {

    // Obtain the JNIEnv from the VM and confirm JNI_VERSION
    JNIEnv* env;
    if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION) != JNI_OK) {

        return JNI_ERR;
    }

    // Temporary local reference holder
    jclass tempLocalClassRef;

    // STEP 1/3 : Load the class id
    tempLocalClassRef = env->FindClass("org/stackoverflow/data/BeanObject");

    // STEP 2/3 : Assign the ClassId as a Global Reference
    JC_BeanObject = (jclass) env->NewGlobalRef(tempLocalClassRef);

    // STEP 3/3 : Delete the no longer needed local reference
    env->DeleteLocalRef(tempLocalClassRef);
    
    // Load the method id
    JMID_BeanObject_getFoo = env->GetMethodID(JC_BeanObject, "getFoo", "(Ljava/lang/String;)V");

    // ... repeat prior line for any other methods of BeanObject

    // ... repeat STEPS 1-3 for any other classes; re-use tempLocalClassRef.

    // Return the JNI Version as required by method
    return JNI_VERSION;
}

/**************************************************************
 * Destroy the global static Class Id variables
 **************************************************************/
void JNI_OnUnload(JavaVM *vm, void *reserved) {

    // Obtain the JNIEnv from the VM
    // NOTE: some re-do the JNI Version check here, but I find that redundant
    JNIEnv* env;
    vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION);

    // Destroy the global references
    env->DeleteGlobalRef(JC_BeanObject);
    
    // ... repeat for any other global references
}

/**************************************************************
 * A Static Native Method
 **************************************************************/
JNIEXPORT void JNICALL Java_org_stackoverflow_jni_NativeClazz_staticNativeMethod
               (JNIEnv * env, jclass clazz, jobject jBeanObject) {
    
    // Retrieve jstring from the Java Object
    jstring jFoo = (jstring)env->CallObjectMethod(jBeanObject, JMID_BeanObject_getFoo);

    // Make accessible to C++
    const char * cFoo = env->GetStringUTFChars(jFoo, NULL);             

    // Do something with cFoo...

    // Release Resources
    env->ReleaseStringUTFChars(jFoo, cFoo);
    env->DeleteLocalRef(jFoo);
}

/**************************************************************
 * Instance / Non-Static Native Method
 **************************************************************/
JNIEXPORT void JNICALL Java_org_stackoverflow_jni_NativeClazz_instanceNativeMethod
               (JNIEnv * env, jobject selfReference, jobject jBeanObject) {

    // Retrieve jstring from the Java Object
    jstring jFoo = (jstring)env->CallObjectMethod(jBeanObject, JMID_BeanObject_getFoo);               

    // Make accessible to C++
    const char * cFoo = env->GetStringUTFChars(jFoo, NULL);             

    // Do something with cFoo...

    // Release Resources
    env->ReleaseStringUTFChars(jFoo, cFoo);
    env->DeleteLocalRef(jFoo);
}

Solution 2

Here is how I practice IBM's recommendation. Considering demo java class like that:

public class SimpleClazz {

    public int value = 10;

    public native int getValue();

    static {
        // Load Native Library
        System.loadLibrary("The native library name");
    }
}

The corresponding jni header file like that:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class SimpleClazz */

#ifndef _Included_SimpleClazz
#define _Included_SimpleClazz
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     SimpleClazz
 * Method:    getValue
 * Signature: ()I
 */
JNIEXPORT jint JNICALL Java_SimpleClazz_getValue
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

According to IBM's recommendation, we need to cache the used class SimpleClazz and the field id of the object member value.

After learning this good article, I cache the SimpleClazz in the function JNI_OnLoad, which is called when the native library is loaded (for example, through System.loadLibrary). In the JNI_Onload, we do find class and store this jclass as a global field.

Furthermore, in the native implementation of getValue, we use static local variable to cache the field id of value. This design is to make sure this filed id can be in the better scope, rather than in the global scope. The drawback of this design is that we need to compare with NULL each time we call this function. I learnt this design from section 4.4.1 of the book The Java Native Interface: Programmer's Guide and Specification.

Finally, we also need to write the function JNI_OnUnload, which is called when the class loader containing the native library is garbage collected. In this function, we release the global reference of jclass.

My cpp implementation is shown as below:

#include <jni.h>
#include <SimpleClazz.h>

static jclass simpleCls;

// According to http://docs.oracle.com/javase/1.5.0/docs/guide/jni/spec/invocation.html#JNI_OnLoad
// The VM calls JNI_OnLoad when the native library is loaded (for example, through System.loadLibrary).
jint JNI_OnLoad(JavaVM* vm, void* reserved) {
    JNIEnv* env;
    if ((*vm)->GetEnv(vm, (void **) &env, JNI_VERSION_1_6) != JNI_OK) {
        return JNI_ERR;
    } else {
        jclass localSimpleCls = (*env)->FindClass("SimpleClazz");

        if (localSimpleCls == NULL) {
            return JNI_ERR;
        }
        simpleCls = (jclass) (*env)->NewGlobalRef(env, localSimpleCls);
    } 
    return JNI_VERSION_1_6;
}



JNIEXPORT jint JNICALL Java_SimpleClazz_getValue(JNIEnv * env, jobject thiz){
    static jfieldID valueID = NULL;
    if (valueID == NULL) {
        valueID = (*env)->GetFieldID(env, simpleCls, "value", "I");
        if (valueID == NULL){
            return JNI_ERR;         // Exception thrown
        }
    }
    jint value = (*env)->GetIntField(env, thiz, valueID);
    return value;
}

// According to http://docs.oracle.com/javase/1.5.0/docs/guide/jni/spec/invocation.html#JNI_OnUnload
// The VM calls JNI_OnUnload when the class loader containing the native library is garbage collected.
void JNI_OnUnload(JavaVM *vm, void *reserved) {
    JNIEnv* env;
    if ((*vm)->GetEnv(vm, (void **) &env, JNI_VERSION_1_6) != JNI_OK) {
        // Something is wrong but nothing we can do about this :(
        return;
    } else {
        if (0 != NULL){
            (*env)->DeleteGlobalRef(env, simpleCls);
        }
    }
}

Solution 3

You can have some utility structures like this:

typedef struct MYVARIANT_FID_CACHE {
    int cached;
    jclass clazz;
    jfieldID pAddress;
} MYVARIANT_FID_CACHE;

MYVARIANT_FID_CACHE VARIANTFc;

void cacheMYVARIANTFields(JNIEnv *env, jobject lpObject)
{
    if (VARIANTFc.cached) return;
    VARIANTFc.clazz = env->GetObjectClass(lpObject);
    VARIANTFc.pAddress = env->GetFieldID(VARIANTFc.clazz, "pAddress", "I");
    VARIANTFc.cached = 1;
}

VARIANT *getMYVARIANTFields(JNIEnv *env, jobject lpObject, VARIANT *lpStruct)
{
    if (!VARIANTFc.cached) cacheVARIANT2Fields(env, lpObject);

    lpStruct = (VARIANT*)(env->GetIntField(lpObject, VARIANTFc.pAddress));

    return lpStruct;
}

This is taken from my question: https://stackoverflow.com/questions/10617714/how-to-extend-swt-com-support

For some good examples look at the os_structs.c it is bundled with eclipse SWT implementation.

Note: The above code is just an example and could be adapted for different OS. Also it just shows "how to access java fields"; for methods you could follow the same approach.

Solution 4

Referring to the answer given by @JoshDM

The C interface for this is slightly different than the C++ one given above. You have to write it like this :-

/**************************************************************
 * Initialize the static Class and Method Id variables
 **************************************************************/
jint JNI_OnLoad(JavaVM* vm, void* reserved) {

    // Obtain the JNIEnv from the VM and confirm JNI_VERSION
    JNIEnv* env;
    if (*(vm)->GetEnv(vm, (void**)&env, JNI_VERSION) != JNI_OK) {

        return JNI_ERR;
    }

    // Temporary local reference holder
    jclass tempLocalClassRef;

    // STEP 1/3 : Load the class id
    tempLocalClassRef = (*env)->FindClass(env, "org/stackoverflow/data/BeanObject");

    // STEP 2/3 : Assign the ClassId as a Global Reference
    JC_BeanObject = (jclass) (*env)->NewGlobalRef(env, tempLocalClassRef);

    // STEP 3/3 : Delete the no longer needed local reference
    (*env)->DeleteLocalRef(env, tempLocalClassRef);

    // Load the method id
    JMID_BeanObject_getFoo = (*env)->GetMethodID(env, JC_BeanObject, "getFoo", "(Ljava/lang/String;)V");

    // ... repeat prior line for any other methods of BeanObject

    // ... repeat STEPS 1-3 for any other classes; re-use tempLocalClassRef.

    // Return the JNI Version as required by method
    return JNI_VERSION;
}
Share:
16,264

Related videos on Youtube

Suhail Gupta
Author by

Suhail Gupta

"There's nothing more permanent than a temporary hack." - Kyle Simpson "The strength of JavaScript is that you can do anything. The weakness is that you will." - Reg Braithwaite I am on internet Twitter @suhail3 E-mail [email protected]

Updated on June 07, 2022

Comments

  • Suhail Gupta
    Suhail Gupta almost 2 years

    I read on on IBM that

    To access Java objects' fields and invoke their methods, native code must make calls to FindClass(), GetFieldID(), GetMethodId(), and GetStaticMethodID(). In the case of GetFieldID(), GetMethodID(), and GetStaticMethodID(), the IDs returned for a given class don't change for the lifetime of the JVM process. But the call to get the field or method can require significant work in the JVM, because fields and methods might have been inherited from superclasses, making the JVM walk up the class hierarchy to find them. Because the IDs are the same for a given class, you should look them up once and then reuse them. Similarly, looking up class objects can be expensive, so they should be cached as well.

    How does one cache the methodID, fieldID, and class objects in JNI? Are there built-in methods or a specific procedure that has to be followed?

  • user207421
    user207421 almost 12 years
    This code cannot possibly work. You can't cache jclasses or jobjects across different JNI invocations without using a GlobalReference. You can cache methodIDs and fieldIDs..
  • Favonius
    Favonius almost 12 years
    @EJP: Could you please explain why 'it can't cache jclasses or jobjects across different JNI'? The code snippet provided here is taken and modified from the eclipse SWT implementation for win32.
  • user207421
    user207421 almost 12 years
    @Favonious For the same reason that GlobalReferences exist. (1) "Local references are valid for the duration of a native method call", (2) "All Java objects returned by JNI functions are local references", and (3) "Local references are only valid in the thread in which they are created".
  • JoshDM
    JoshDM over 11 years
    The "How to extend SWT COM support" target no longer exists.
  • Julien
    Julien over 10 years
    What I didn't show is how to initialize the methodId field, but it's basic JNI. The caching is done automatically at class loading: initIDs is invoked and the methodID field is initialized
  • yano
    yano almost 9 years
    It is a good idea to do this caching in your JNI_OnLoad method, particularly in Android, because the ClassLoader is only available (by default) on the thread which is calling JNI_OnLoad, so that's why I have upvoted this answer!
  • Konstantin Berkov
    Konstantin Berkov about 6 years
    One of best answers about caching jclass/jmethodId/jfieldId! Still one question bugs me. What about jmethodID/jfieldID? They are opaque structs, how can we free/delete them, or should we? Or just pointing everything (all deleted global refs, jmethodIDs, jfieIDs) to NULL/nullptr is enough?
  • JoshDM
    JoshDM about 6 years
    @KonstantinBerkow - Method IDs and Field IDs are not managed by the native code; they are managed by the virtual machine and are valid until the associated class is unloaded. Field IDs and Method IDs cannot be explicitly deleted before the virtual machine unloads the defining class. Just let the VM handle them. If it makes you feel better to set them to NULL, do it after you release the associated class, but it is not necessary to do at all.
  • ceztko
    ceztko almost 5 years
    Question: is caching JC_BeanObject strictly necessary to safely use the cached JMID_BeanObject_getFoo?
  • JoshDM
    JoshDM almost 5 years
    See the Rationale portion, @ceztko. From what I read when it was written, and presuming it still holds true today, if the class (JC_) gets unloaded by the classloader, then re-loaded, it gets a new signature. The associated method signature (JMID_) would reference the original signature and no longer be valid. Caching the class prevents unloading. See the original article from the OP: ibm.com/developerworks/java/library/j-jni/index.html#notc
  • JoshDM
    JoshDM almost 5 years
    @ceztko TL;DR answer: not for the first call, but for subsequent calls to the same loaded JNI class, it provides a probably-never-to-be-used-in-99.999998%-of-scenarios safety net.
  • Konrad Rudolph
    Konrad Rudolph about 4 years
    Good answer, but there’s really no need to clear up all those local references. Let the Java VM do it — the waste of space is negligible, and it vastly decreases the risk of accidentally prematurely releasing a reference (it also unclutters the code: less code means less chance of bugs).
  • Raii
    Raii almost 3 years
    link "The Java Native Interface: Programmer's Guide and Specification." no response
  • Raii
    Raii almost 3 years
    How to find the JNI_VERSION we are using? I use Android Studio and cmake to make the cpp file. However, I cannot find any JNI_VERSION value in build.gradle or native-lib.cpp or CmakeLists.txt. I only find JavaVersion.VERSION_1_8 in build.gradle but JavaVERSION seems not the JNI_VERSION???
  • JoshDM
    JoshDM about 2 years
    @Raii - it is defined in jni.h which is included in the javah auto-generated header file: #include <jni.h>