Tuesday, September 10, 2013

How to build a better Android JNI library using NDK

I've been using Android for a few years now and just recently had to build a fairly substantial library in C++.  I've always hated the typical structure of the JNI layer, at least the one I learned originally.  The new library that needed integrating was completely devoid of any JNI structure.  Given the nature of the project, I didn't want to hard code the paths.

I looked at a variety of jni code generators.  There weren't many restrictions, it did have to build on windows, mac, and pc.  After looking at them, I decided my project wasn't that big so it seemed overkill to put in a code generator that would need to be maintained.

In the end, I chose a simple method that gets the job done with minimal frustration.  The only real requirement is that you must specify the calling paramters and return code.  Once you have done a few, you get used to it and the rest are easy.  A code generator would eliminate that need.  (Checkout the bottom of this page for some possible generators: jni generator info.)

Typically, it looks something like this:

Java code:

public class FooUtil {
    try {
         System.loadLibrary("awesomeFooLib");
         native_init = true;
     } catch (UnsatisfiedLinkerError e) {
     }

     private native int fooMeN(int blah);

     public int fooMe(int blah) {
           if (native_init) {
                return fooMe(blah);
           }
           return -1;
     }
}

The C layer looks like:
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
    return JNI_VERSION_1_6;
}
JNIEXPORT jint JNICALL Java_com_mtterra_android_utils_fooMeN(
        JNIEnv *env, jobject self, jint blah)
{
     // do something interesting
     return 0;
}

The problem with the above is all the hardcoded paths and the general fuglyness of the whole thing.  You can use javah -jni <classfile> to generate a header file with the long method names.  As much as I would like it to read better, JNI coding doesn't lend itself well for that.  However, it can be better.  Note: if you are using JNI you should make sure to read JNI Tips.

The new method is to register your JNI functions when the library is loaded instead of hard coding the paths.  This allows for much better readability and somewhat better portability.  The Java class portion stays the same, only the JNI changes.  I used C calling convention above, using C++ (below) just requires wrapping the code with extern "C" and calling functions off of the env pointer is simpler.

#ifdef __cplusplus
extern "C" {
#endif

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
     JNIEnv *env;
     jint ok = vm->GetEnv((void **)& env, JNI_VERSION_1_6);

     // Add a function to register all of the jni calls
     if (ok == 0) {
         installJNIFunctions(env);
     }

     return JNI_VERSION_1_6;
}

   // Add your normal-looking functions
static int JNICALL fooMe(int blah) {
    return 42;
}

    // Define all the functions here that need to be registered
static JNINativeMethod fooLibraryMethods[] = {
    // Calling parameters are pretty easy, the end of this post has more info.
    // The first paramter is the name that your java defines as the native call.
    // Then the method signature then the local function to be called.
    { (char * const) "fooMeN", "(I)I", (void *)&fooMe) }
};


void installJNIFunctions(JNIEnv *env) {
     // This part defines the linkage to your app.  It is a hard coded path but it is also
     // very easy to change if you move the code to another app or put it in a library at some
     // point.
     // In your .h file you would add
     // #define FOO_LIBRARY "com/mtterra/android/utils"
     // substituting your package name obviously.
    jclass fooLibCls = env->FindClass(FOO_LIBRARY);

    // Make sure the class was found
    if (fooLibCls == NULL) {
        env->fatalError("Unable to find " FOO_LIBRARY);
        return;
    }

    // get the number of methods available
    int noofMethods= sizeof(fooLibraryMethods) / sizeof(fooLibraryMethods[0]);

    // finally, register the methods
    env->RegisterNatives(fooLibCls, fooLibraryMethods, noof Methods);

     //done....reap the rewards of unmangle names
}

#ifdef __cplusplus
} // extern "C"
#endif

The hardest part is writing the method signatures.  In the above example, I is standard for int.  So (I)I is an int argument to the method with a return type of int.  Looking at a more interesting example:

long foo(String s, int i, String t);  This would translate to a method signature of  (Ljava/lang/String;ILjava/lang/String)J

To see the full list of java types that can be used and more examples see this.  As far as the ndk build tools, there aren't any needed changes to compile the code.

Using the above method makes the code very readable and keeps from complicating the build environment.  Of course, if you have hundreds of jni calls then maybe a wrapper or jni code generator would be a better way to go.  In general, I have lots of libraries but there are few interfacing points where jni is involved.  JNI calls are rather expensive so cutting down the number of calls is a good thing.

---------------------------------------------------------------------------------------------------

QuickLogger Fitness - super quick fitness tracking
Android app on Google Play