Andoid JNI – Summary

Introduction

In this article, I would like to make a summary for Android JNI programming by walking through a hands on JniExample project. You can pull a complete copy of this example project from my Github JniExample.

From this example project, you should be able to:

  • Understand how JNI_OnLoad works.
  • Understand typical hard wired JNI methods with style of Java_packagename_ClassName_jnimethodName(JNIEnv *, jobject).
  • Runtime registered JNI methods using RegisterNatives function.
  • Understand how to invoke JNI operations of class, object, string and array, and how to callback to Java layer from JNI native code.
  • Understand how to throw a Java exception using JNI function.
  • Understand how to manage AttachCurrentThread/DetachCurrentThread.

JniExample Project

Let’s create an Example project JniExample using Android Studio, remember to tick the “Include c++ support” option.

JNI_OnLoad

In the native-lib.cpp, there is one entry function 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)
{
    g_jvm = vm;

    g_uenv.venv = NULL;
    jint result = -1;
    JNIEnv* env = NULL;
    jclass aroStorageClazz = NULL;

    ALOGI("JNI_OnLoad started.");
    if (vm->GetEnv(&g_uenv.venv, JNI_VERSION_1_6) != JNI_OK) {
        ALOGE("ERROR: GetEnv failed");
        goto bail;
    }
    env = g_uenv.env;

    // Find the AroStorage class.
    aroStorageClazz = g_uenv.env->FindClass(classPathNameForAroStorage);
    // Make a global reference to aroStorageClazz for future usage.
    g_aroStorageClazz = (jclass)g_uenv.env->NewGlobalRef(aroStorageClazz);

    if (registerNatives(env) != JNI_TRUE) {
        ALOGE("ERROR: registerNatives failed");
        goto bail;
    }

    result = JNI_VERSION_1_6;

    bail:

    ALOGI("JNI_OnLoad finished.");
    return result;
}

The flow is as below:

JNI_OnLoad Sequence

Register JNI method

We can see one JNI method which has been created by Android Studio.

JNIEXPORT jstring JNICALL
Java_com_arophix_jniexample_MainActivity_stringFromJNI(JNIEnv *env, jobject thiz){
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
};

This is typical JNI method signature, it has below style

Java_some_package_name_ClassName_nativeMethodName(JNIEnv *env, jobject thiz, ...)

However, this JNI function declaration actually implicitly make the function name “exported” which may not be secure if this JNI method is trying to perform some sensitive operations. To make it more secure, the alternative is to make the JNI function be static and runtime register it to JNI environment. For example

/**
* Definition of the native method struct is as below:
typedef struct {
    const char* name; // the Java method name
    const char* signature; // the Java method signature
    void* fnPtr; // the C function pointer.
} JNINativeMethod;
*/
static JNINativeMethod aroMemoryMethods[] = {
    {"getNameFromNative", "()Ljava/lang/String;", (void*)getNameFromNative},
};

static JNINativeMethod aroStorageMethods[] = {
    {"getJniVersion", "()I", (void*)getJniVersion},
    {"computeStorageSignatureNative", "(Lcom/arophix/jniexample/jniobjects/AroMemory;[B)Ljava/lang/String;", (void*)computeStorageSignatureNative},
    {"downlaodImageNativeAsyncTask", "()I", (void*)downlaodImageNativeAsyncTask},
};
JNIEXPORT jstring JNICALL
Java_com_arophix_jniexample_MainActivity_stringFromJNI(JNIEnv *env, jobject thiz){
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
};

JNI operations

Class, object and string operations

This includes how to find a Java class, find and execute a method of a Java class, get and set a static or non-static field of a Java class. See below code snippet please.

static jstring computeStorageSignatureNative(JNIEnv *env,
                                             jobject thiz /*thiz is referring to AroStorage instance*/,
                                             jobject aroMemory,
                                             jbyteArray deviceFingerprintData) {

    ...

    /** Example of accessing "jobject" of argument list **/
    // Find the AroStorage class
    jclass aroMemClazz = env->FindClass(classPathNameForAroMemory);
    // Get the methodID
    jmethodID jmethodGetId = env->GetMethodID(aroMemClazz, "getId", "()I");
    // Call int primitive method.
    jint idNo = env->CallIntMethod(aroMemory, jmethodGetId);
    ALOGI("idNo: %d", idNo);

    /** Example of calling "thiz" object method **/
    // Find the AroStorage class.
    jclass aroStorageClazz = g_aroStorageClazz;
    // Get the needed methodID.
    jmethodID getName = env->GetMethodID(aroStorageClazz, "getName", "()Ljava/lang/String;");
    // Call object method as String is an java object.
    jstring name = (jstring)env->CallObjectMethod(thiz, getName);
    // Get a const char * reference from jstring
    const char *nameUtf8 = env->GetStringUTFChars(name, 0);
    ALOGI("%s", nameUtf8);
    // Remember to release the const char * pointer` if it is no longer needed.
    env->ReleaseStringUTFChars(name, nameUtf8);
    env->DeleteLocalRef(name);

    /** Example of accessing static field **/
    jfieldID staticField = env->GetStaticFieldID(aroStorageClazz, "sAroStroageDescriptor", "Ljava/lang/String;");
    jstring descriptor = (jstring)env->GetStaticObjectField(aroStorageClazz, staticField);
    // Get a const char * reference from jstring
    const char *nameUtf8ForDescriptor = env->GetStringUTFChars(descriptor ,0);
    ALOGI("%s", nameUtf8ForDescriptor);
    // Remember to release the const char * pointer if it is no longer needed.
    env->ReleaseStringUTFChars(descriptor, nameUtf8ForDescriptor);
    env->DeleteLocalRef(descriptor);

    /** Example of accessing static method **/
    jmethodID staticMethod = env->GetStaticMethodID(aroStorageClazz, "getAroStroageDescriptor", "(IFLjava/lang/String;)Ljava/lang/String;");
    int arg1 = 100;
    float arg2 = 123.456;
    jstring arg3 = env->NewStringUTF("Native arg3");
    jstring descriptorFromStaticMethod = (jstring)env->CallStaticObjectMethod(aroStorageClazz, staticMethod, arg1, arg2, arg3);
    // Get a const char * reference from jstring
    const char *nameUtf8ForDescriptorFromStaticMethod = env->GetStringUTFChars(descriptorFromStaticMethod ,0);
    ALOGI("%s", nameUtf8ForDescriptorFromStaticMethod);
    // Remember to release the const char * pointer if it is no longer needed.
    env->ReleaseStringUTFChars(descriptorFromStaticMethod, nameUtf8ForDescriptorFromStaticMethod);
    env->DeleteLocalRef(descriptorFromStaticMethod);
    env->DeleteLocalRef(arg3);
    ......
}

One important point I want to mention is please do remember to release the local reference (DeleteLocalRef) to avoid JNI memory leak. The JNI local reference has its lifecycle only with the function call, you should never try to access a local reference out of its scope, or else, you will see an exception like below:

12-06 23:22:12.198 5345-5345/com.arophix.jniexample E/art: JNI ERROR (app bug): accessed stale local reference 0x10001d (index 7 in a table of size 7)

12-06 23:22:12.201 5345-5345/com.arophix.jniexample A/art: art/runtime/java_vm_ext.cc:470] JNI DETECTED ERROR IN APPLICATION: use of deleted local reference 0x10001d

12-06 23:22:12.202 5345-5345/com.arophix.jniexample A/art: art/runtime/java_vm_ext.cc:470]     from computeStorageSignatureNative(AroMemory, byte[])

Tips

To avoid JNI memory leak, take special care to all the JNI reference types, i.e. jstring, jobject etc. Thumb of rule is that you need to call “DeleteLocalRef” on all of these reference types. But, you don’t need to release the jclass, jfieldID and jmethodID types as well as the primitive types, because they are not Java reference type.

Array operations

Array operations usually include get length of array, modify array elements etc.


/** Example of accessing byte array and using array region API **/
jint arrayLength = env->GetArrayLength(deviceFingerprintData);
ALOGI("arrayLength: %d", arrayLength);
jbyte *jbyteBuffer = (jbyte*)malloc(8);
memset(jbyteBuffer, 0x01, 8);
env->SetByteArrayRegion(deviceFingerprintData, 0, 8, jbyteBuffer);
// Get the starting pointer of byte array
jbyte *byteArray1 = env->GetByteArrayElements(deviceFingerprintData, 0);
for (int i = 0; i < arrayLength; ++i) {
    // Print the array to verify the value is updated to 0x01.
    ALOGI("array[%d]: 0x%02x, ", i, byteArray1[i]&0x000000ff);
}
// Free the allocated memory
free(jbyteBuffer);
// Release the local reference of byte array
env->ReleaseByteArrayElements(deviceFingerprintData, byteArray1, 0);

Exception handling

It is possible to throw an Java exception from native layer.

/**
* Throw an Exception from native by simply checking the length of fingerprint array.
*/
static bool validateDeviceFingerprint(JNIEnv *env,
                                      jobject thiz /*thiz is referring to AroMemory instance*/,
                                      jbyteArray deviceFingerprintData) {

    jint arrayLength = env->GetArrayLength(deviceFingerprintData);
    ALOGI("arrayLength: %d", arrayLength);
    if (arrayLength != 16) {

        jclass jcls = env->FindClass("java/lang/Exception");
        env->ThrowNew(jcls, "Invalid deviceFingerprint detected.");

        /**
         * The point worth mentioning here is that, on immediate encounter of a throw method,
         * control is *not* transferred to the Java code; instead, it waits until the return
         * statement is encountered. There can be lines of code in between the throw method
         * and return statement. Both the throw and JNI functions return zero on success
         * and a negative value otherwise.
        */

         return false;
    }

    return true;
}

Thread handling

For a native thread, it cannot directly access JNIEnv* as it has not been attach to current JNI environment, some steps need to do:

  1. Get reference to the JVM environment context using GetEnv
  2. Attach the context if necessary using AttachCurrentThread
  3. Call the JNI method as normal using CallObjectMethod
  4. Detach using DetachCurrentThread
/* this function is run by the start_backgroud_task thread */
void* download_image_func(void *arg)
{
    ALOGI("download_image started\n");

    ALOGI("threadFunction download_image_func is being attached.");
    JNIEnv *env = NULL;
    JavaVM *javaVM = g_jvm;
    jint res = javaVM->GetEnv((void**)&env, JNI_VERSION_1_6);
    if (res != JNI_OK) {
        res = javaVM->AttachCurrentThread(&env, NULL);
        if (JNI_OK != res) {
            ALOGI("Failed to AttachCurrentThread, ErrorCode = %d", res);
            return NULL;
        }

        ALOGI("AttachCurrentThread succeed ...");
    }

    // Get the needed methodID.
    jmethodID getImageUrl = env->GetMethodID(g_aroStorageClazz, "getImageUrl", "()Ljava/lang/String;");
    // Call object method as String is an java object.
    jstring imageUrl = (jstring)env->CallObjectMethod((jobject)g_aroStorageObj, getImageUrl);
    // Get a const char * reference from jstring
    const char *imageUrlChars = env->GetStringUTFChars(imageUrl, 0);
    ALOGI("%s", imageUrlChars);
    // Remember to release the const char * pointer` if it is no longer needed.
    env->ReleaseStringUTFChars(imageUrl, imageUrlChars);
    env->DeleteLocalRef(imageUrl);

    int counter = 0;
    /* sleep 100ms * 100 */
    while(++counter &lt;= 100) {
        usleep(100);
    }

    if (env->ExceptionCheck()) {
        env->ExceptionDescribe();
    }

    ALOGI("download_image finished\n");

    g_jvm->DetachCurrentThread();
    ALOGI("download_image thread is detached\n");

    /* the function must return something - NULL will do */
    return NULL;
}

/**
* using linux pthread APIs to start a new background thread and trying to access JNI functions.
*/
static void downlaodImageNativeAsyncTask(JNIEnv *env,
                                         jobject aroStorage /*thiz is referring to AroStorage instance*/)
{
    // Make a global reference to g_aroStorageObj for future usage.
    g_aroStorageObj = (jclass)g_uenv.env->NewGlobalRef(aroStorage);

    /* thread reference */
    pthread_t image_download_thread;

    /* create a thread which executes a image download task */
    if(pthread_create(&image_download_thread, NULL, download_image_func, aroStorage)) {
        fprintf(stderr, "Error creating thread\n");
    }
}

Caveats

If the thread is not attached to the JNI runtime environment, the runtime will abort and give below errors.

… A/art: art/runtime/scoped_thread_state_change.h:40] Check failed: runtime == NULL || !runtime->IsStarted() || runtime->IsShuttingDown()
… A/art: art/runtime/runtime.cc:203] Runtime aborting…
… A/art: art/runtime/runtime.cc:203] (Aborting thread was not attached to runtime!)
… A/art: art/runtime/runtime.cc:203] Dumping all threads without appropriate locks held: thread list lock mutator lock
… A/art: art/runtime/runtime.cc:203] All threads:
… A/art: art/runtime/runtime.cc:203] DALVIK THREADS (13):
… A/art: art/runtime/runtime.cc:203] “main” prio=5 tid=1 Runnable

Summary

  • JNI is officially part of JDK since JDK version 1.0
  • JNI provides bi-directional access between Java code and native code
  • JNI statically binds Java native method with native implementation
  • JNI_OnLoad method expose some security weakness. I.e. hooking of pointer JNIEnv.
  • JNI is tedious :)!!!

Source code

https://github.com/russell-shizhen/JniExample.git

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

w

Connecting to %s