JNI is (Not) Your Friend

Introduction

So far we’ve scratched the surface of using JNI when we looked at wrapping a C library and Calling Java from C. Now we’re going to look into some more complex uses.

Java Class

Since we’re working with JNI, we’ll need a Java class to use what we’re going to expose from C.

DemoFuncs.java

class DemoFuncs {

    static {
        System.loadLibrary("demo_lib");
    }

    public enum Days {
        MON,
        TUE,
        WED,
        THU,
        FRI,
        SAT,
        SUN,
    }

    public static native int[] demo_array_return();
    public static native int[] demo_array_copy_inc(int[] in);
    public static native Days demo_enum_val();
    public static native Days demo_enum_field_val();
    public static native void demo_exception_1();
    public static native void demo_exception_2();
    public static native void demo_exception_3();
    public static native boolean demo_exception_4();
    public static native Name demo_name(String name);
}

Note: This class includes an `enum` because one of the later examples uses them. It was easier to have an `enum` in this class than to try and find a class in the Java standard library with one.

Also, for now ignore the `Name` class the `demo_name` function. We’ll go into detail about that later.

Arrays

JNI has some very handy functions to make working with arrays easier.

Creating Arrays

Let’s start with something simple. Like turning a C array into a Java array.

jintArray demo_array_return(JNIEnv *env, jobject obj)
{
    int       cia[] = { 1, 2, 3, 4, 5, 6, 7, 8 };
    jintArray jia;
    size_t    len;

    len = sizeof(cia)/sizeof(*cia);

    jia = (*env)->NewIntArray(env, len);
    if (jia == NULL)
        goto done;
    (*env)->SetIntArrayRegion(env, jia, 0, len, cia);

done:
    (*env)->ExceptionClear(env);

    return jia;
}

We have `cia` which is our super simple C array and some how we need to put it into our `jia` Java array. The first thing we do is create a Java array with `NewIntArray`. This example uses `int`s but there are equivalent functions for creating arrays of other types.

Now for the easy part. `SetIntArrayRegion` takes a C array and copies the contents into the Java array. Once we return the Java array the JVM handles its life cycle. It probably doesn’t need to be said but, if we were not returning it, then we need to destroy it ourselves.

Copy and Manipulate Arrays

When dealing with arrays, two common tasks are making a copy of the data and manipulating the data. I’ve combined this into one example where we first copy then manipulate. That said, you don’t need to copy the data to manipulate it. You can modify the array that was passed in.

jintArray demo_array_copy_inc(JNIEnv *env, jobject obj, jintArray jia_in)
{
    jintArray  jia = NULL;
    jint      *elms;
    jsize      len;

    len  = (*env)->GetArrayLength(env, jia_in);
    elms = (*env)->GetIntArrayElements(env, jia_in, NULL);
    if (elms == NULL)
        goto done;

    jia = (*env)->NewIntArray(env, len);
    if (jia == NULL)
        goto done;
    (*env)->SetIntArrayRegion(env, jia, 0, len, elms);
    (*env)->ReleaseIntArrayElements(env, jia_in, elms, 0);

    elms = (*env)->GetIntArrayElements(env, jia, NULL);
    if (len >= 4) {
        for (jsize i = 0; i < 4; i++) {
            elms[i] *= 2;
        }
    }
    (*env)->ReleaseIntArrayElements(env, jia, elms, 0);

done:
    (*env)->ExceptionClear(env);

    return jia;
}

The first thing we do is use `GetArrayLength` to get the length so we know how large an array we’ll need allocated. I really like that JNI function names are very clear. `GetIntArrayElements` is super important because it exposes the Java integer data as a C array of `int`s. We’ll get the data from the array passed in and use it with `SetIntArrayRegion` to copy the data into a new array.

When we copied the data into the new array we used `SetIntArrayRegion`. However, this isn’t strictly necessary. The four loop we used to change the data could have copied the each element from one array to the other. That said, use `SetIntArrayRegion` if you can because it’s cleaner.

When you call `GetIntArrayElements` it increases a reference count to the internal data held by the array. This will prevent the JVM from releasing the data. `ReleaseIntArrayElements` does not destroy the data, it decreases the reference count. You always need to have these paired, otherwise you’ll end up with memory leaks.

Once we have the array elements for the copied data we’ll loop though it and make some changes. We’re not doing anything useful by doubling the first four elements but it demonstrates changing the data.

Working with Enums

Like most things in Java, an `enum` is an object. So, they have functions and these functions can help us get a member of the `enum`. In a larger application we may have a function that takes an `enum` value and we need to get the value in order to pass it as an argument to the function we care about

jobject demo_enum_val(JNIEnv *env, jobject obj)
{
    jclass    cls;
    jmethodID mid;
    jstring   name = NULL;
    jobject   eval = NULL;

    cls = (*env)->FindClass(env, "DemoFuncs$Days");
    if (cls == NULL)
        goto done;

    mid = (*env)->GetStaticMethodID(env, cls, "valueOf", "(Ljava/lang/String;)LDemoFuncs$Days;");
    if (mid == NULL)
        goto done;

    name = (*env)->NewStringUTF(env, "WED");
    if (name == NULL)
        goto done;

    eval = (*env)->CallStaticObjectMethod(env, cls, mid, name);

done:
    (*env)->ExceptionClear(env);
    if (name != NULL)
        (*env)->DeleteLocalRef(env, name);
    if (cls != NULL)
        (*env)->DeleteLocalRef(env, cls);

    return eval;
}

Like so many JNI patterns, we need to get the class for the `enum` within our class. When you reference packages and functions with JNI you use ‘/’ instead of ‘.’ and when we reference an `enum` within a class you use ‘$’.

Now that we have the class we get the id for the “valueOf” function. This function takes a string and returns the value associated with it. For example, we can can reference `Days.WED` using the string “WED”.

Now that we have the `valueOf` function id and the string name we put them together and we get the value we can pass along to whatever needs it. Keep in mind that since we’re calling static functions we’re passing the `cls`. If we had an object we would be using it instead of the `cls`.

Accessing Fields

We can get `enum` values by using functions but `enum` members are actually fields. Not surprisingly, there are a JNI functions specifically for accessing fields. Also not surprisingly, it’s much easier to access a field using this method.

jobject demo_enum_field_val(JNIEnv *env, jobject obj)
{
    jclass   cls;
    jfieldID fid;
    jobject  eval = NULL;

    cls = (*env)->FindClass(env, "DemoFuncs$Days");
    if (cls == NULL)
        goto done;

    fid = (*env)->GetStaticFieldID(env, cls, "THU", "LDemoFuncs$Days;");
    if (fid == NULL)
        goto done;

    eval = (*env)->GetStaticObjectField(env, cls, fid);

done:
    (*env)->ExceptionClear(env);
    if (cls != NULL)
        (*env)->DeleteLocalRef(env, cls);

    return eval;
}

Getting an `enum`’s field value starts off the same way by getting the class. However, instead of getting the id of the “valueOf” function you get the id of the field. Finally, you use the `GetStaticObjectField` function to get the value.

Exceptions

For the most part we’ve been ignoring exceptions. If they happen we clear them and go to a done label. We’re doing things in a very C way where we use the return value partly as an error indicator. This works fine for C but we’re dealing with Java and the Java stuff we’re doing with JNI does throw exceptions. We really should be doing a better job utilizing them.

The vast majority of JNI functions can throw exceptions and if you’re working with a Java object they can also cause exceptions to be thrown. When an exception it thrown JNI processing must stop until the exception is handled. Only a very small set of JNI functions can be called before the exception is cleared. Basically when there is an exception on the stack nothing that could cause another exception can be called. Once we’ve determined an exception was thrown, we need to clear it and deal with the implications.

There three main exception functions and they are `ExceptionOccurred`, `ExceptionCheck`, and `ExceptionClear`. `ExceptionOccurred` not only checks if there was an exception it will return an exception object. `ExceptionCheck` will only check if there was an exception. `ExceptionClear` clears it so we can continue processing.

You must be very aware of what JNI functions can throw an exception. Some, like FindClass, will throw and return NULL. You really only have to check the return value to know if there was an exception. However, some void functions will throw exceptions. Basically if a function can throw an exception it needs to be immediately followed by either `ExceptionOccurred` or `ExceptionCheck`. If you’re not going to let it bubble up don’t forget to call `ExceptionClear`.

It’s imperative exceptions are handled at some level. Either in JNI or in Java if we let it bubble up. If we don’t handle them a very bad thing will happen! The JVM will just stop! It really doesn’t like uncaught exceptions!

Exception in thread "main" java.lang.NoClassDefFoundError: I/Don't/Exist!
	at DemoFuncs.demo_exception_1(Native Method)
	at Main.main(Main.java:22)
Caused by: java.lang.ClassNotFoundException: I.Don't.Exist!
	at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	... 2 more

We get something that looks a lot like this. Let’s look at a few ways we can prevent this from happening.

Let Java Handle It

void demo_exception_1(JNIEnv *env, jobject obj)
{
    (*env)->FindClass(env, "I/Don't/Exist!");
}

`FindClass` throws an exception here and we don’t do anything in JNI. We’re depending on Java to have called this in a try block somewhere along the line.

try {
	DemoFuncs.demo_exception_1();
} catch (Throwable e) {
	System.out.println("demo_exception_1 caught throwable: " + e.getMessage());
}

What’s really interesting and took me an hour to figure out is, we have to catch a Throwable not an Exception. For what ever reason even though the text says, “Exception in thread “main” … ” the exception comes back up as a Throwable which is the parent class of an Exception. If you used `catch (Exception e)` the “exception” that bubbled up won’t be caught and the JVM will exit.

See if We Care About It

This is very similar to letting Java handle it because we’re still going to let Java handle it. That said, we’re going to use `ExceptionOccurred` so we can look at the exception and decide what we want to do with it.

void demo_exception_2(JNIEnv *env, jobject obj)
{
    jclass     cls;
    jthrowable e = NULL;

    cls = (*env)->FindClass(env, "I/Don't/Exist!");
    if (cls == NULL) {
        e = (*env)->ExceptionOccurred(env);
        (*env)->ExceptionClear(env);
        if (e != NULL) {
            (*env)->Throw(env, e);
            return;
        }
    }
}

Once we get the exception we can evaluate it and take appropriate action. If it’s not something we care to deal with we can call `Throw` and have it sent up the chain to be handled at a higher level. This example omits the whole check the exception but you get the idea.

Since this is throwing the original exception we’ll still need to catch a `Throwable`.

Roll Our Own

This isn’t so much about dealing with exceptions but we are going to kick everything off by generating one.

void demo_exception_3(JNIEnv *env, jobject obj)
{
    jclass cls;

    cls = (*env)->FindClass(env, "I/Don't/Exist!");
    if ((*env)->ExceptionCheck(env)) {
        (*env)->ExceptionClear(env);

        cls = (*env)->FindClass(env, "java/lang/Exception");
        (*env)->ThrowNew(env, cls, "Something bad happened...");
        goto done;
    }

done:
    if (cls != NULL)
        (*env)->DeleteLocalRef(env, cls);
}

Once we’ve encountered an error condition, either though another exception or due to some other indicator we’re ging to create our own exception. We use `ThrowNew` to create and send up an exception for us.

Even though we’re throwing an exception that still means there is an exception outstanding. So we can only do basic things before the function ends. Here we’re going to delete the reference to the `cls` if it exists. This is a simple example but we can assume it failed somewhere further along.

Unlike letting the exception bubble up or using `Throw`, `ThrowNew` actually throws an `Exception`!

try {
	DemoFuncs.demo_exception_3();
} catch (Exception e) {
	System.out.println("demo_exception_3 caught exception: " + e.getMessage());
}

Yep! If we use `ThrowNew` our try block can look like we’d expect. I have no idea why these ways of passing along exceptions act differently.

The C Way is the Only Way

We can go the C route and have our functions return a success or failure indicator. Basically, just return true or false if we hit an exception.

jboolean demo_exception_4(JNIEnv *env, jobject obj)
{
    jclass cls;

    cls = (*env)->FindClass(env, "I/Don't/Exist!");
    if (cls == NULL) {
        (*env)->ExceptionClear(env);
        if (cls != NULL)
            (*env)->DeleteLocalRef(env, cls);
        return JNI_FALSE;
    }

    if (cls != NULL)
        (*env)->DeleteLocalRef(env, cls);
    return JNI_TRUE;
}

There are two big issues here. First, you lose a lot of the power of Java exceptions. I’m not going to go into detail or debate the merits of exceptions, but suffice it to say, if you’re working with Java you can expect exceptions and using this style of programming will irk Java developers.

What I see as the bigger issue is JNI doesn’t allow returning a value through a reference. Well, it does but you can’t create the value. You can’t do something like this:

void func(char **out)
{
	*out = strdup("hi");
}

With JNI you have to create an object in Java and pass it in as a parameter. Then you can create members within that object. This gets really complex and you get really tight coupling between the Java and C code. Not to mention you end up with a bunch of classes that only serve to marshal data between C and Java. Returning the object you want to make and returning an exception on error alleviates this somewhat.

Printing Exceptions the Easy Way

(*env)->ExceptionDescribe(env);

There is a very powerful function (`ExceptionDescribe`) which will print an exception and backtrace to, “a system error-reporting channel”. In most cases this is stderr. This is really useful for debugging but not something that should be presented to the end user.

Objects

Java is an object oriented language and JNI doesn’t negate this. You can’t design and create Java objects with JNI because JNI is a glue layer. However, you can create and use Java objects within JNI.

Before we can do anything we need an object. Let’s create a simple class that we can operate on.

Name.java

public class Name
{

    private String name;
    private int num_dogs;
    private int num_cats;

    public Name(int num_dogs, int num_cats) {
        this.name = "?";
        this.num_dogs = num_dogs;
        this.num_cats = num_cats;
    }

    public boolean setName(String name) {
        this.name = name;
        return true;
    }

    public String toString() {
        return "My name is " + name + "." +
            " I have " + num_dogs + " dog" + ((num_dogs==1)?"":"s") +
            " and " + num_cats + " cat" + ((num_cats==1)?"":"s") +
            ".";
    }
}

Our object doesn’t really do much but it demonstrates all the various things we care about for a JNI example. There is a constructor, and non-void functions.

jobject demo_name(JNIEnv *env, jobject obj, jstring name)
{
    jclass    cls = NULL;
    jmethodID mid;
    jobject   myname;
    jboolean  ret;

    cls = (*env)->FindClass(env, "Name");
    mid = (*env)->GetMethodID(env, cls, "<init>", "(II)V");
    myname = (*env)->NewObject(env, cls, mid, 3, 0);

    mid = (*env)->GetMethodID(env, cls, "setName", "(Ljava/lang/String;)Z");
    ret = (*env)->CallBooleanMethod(env, myname, mid, name);
    if (ret == JNI_FALSE) {
        (*env)->DeleteLocalRef(env, myname);
        myname = NULL;
        goto done;
    }

done:
    if (cls != NULL)
        (*env)->DeleteLocalRef(env, cls);

    return myname;
}

To use the object we first, like everything else we do with JNI, we find the class. I’ve skipped all NULL checks and exception handling for brevity. You really should not do this and read the above information about exceptions if you skipped it.

Now that we have the class we call the magical `` function. This is actually the constructor. We provide the signature so if there are multiple overloaded functions the right one is called.

Now that we’ve created the class we can find and call the `setName` function.

Finally, we’ll return the object we created. Since this is being returned to the JVM we don’t have to worry about it any longer. The JVM now owns it and will take care of destroying it. If we were not returning it we would need to destroy it ourselves, before returning.

Exposing Our Demo Functions

Now that we have our Java class and our C functions we need to expose the C functions so the class can use them. This is the generic boilerplate JNI setup and tear down code that I’m sure you’re sick of seeing.

demo_funcs.c

#include <stdio.h>
#include <jni.h>

static const char *JNIT_CLASS = "DemoFuncs";

...

static JNINativeMethod funcs[] = {
    { "demo_array_return", "()[I", &demo_array_return       },
    { "demo_array_copy_inc", "([I)[I", &demo_array_copy_inc },
    { "demo_enum_field_val", "()LDemoFuncs$Days;", &demo_enum_field_val },
    { "demo_enum_val", "()LDemoFuncs$Days;", &demo_enum_val },
    { "demo_exception_1", "()V", &demo_exception_1 },
    { "demo_exception_2", "()V", &demo_exception_2 },
    { "demo_exception_3", "()V", &demo_exception_3 },
    { "demo_exception_4", "()Z", &demo_exception_4 },
    { "demo_name", "(Ljava/lang/String;)LName;", &demo_name },
};
 
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved)
{
    JNIEnv *env;
    jclass  cls;
    jint    res;
 
    (void)reserved;
 
    if ((*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_8) != JNI_OK)
        return -1;
 
    cls = (*env)->FindClass(env, JNIT_CLASS);
    if (cls == NULL)
        return -1;
 
    res = (*env)->RegisterNatives(env, cls, funcs, sizeof(funcs)/sizeof(*funcs));
    if (res != 0)
        return -1;
 
    return JNI_VERSION_1_8;
}
 
JNIEXPORT void JNICALL JNI_OnUnload(JavaVM *vm, void *reserved)
{
    JNIEnv *env;
    jclass  cls;
 
    (void)reserved;
 
    if ((*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_8) != JNI_OK)
        return;
 
    cls = (*env)->FindClass(env, JNIT_CLASS);
    if (cls == NULL)
        return;
 
    (*env)->UnregisterNatives(env, cls);
}

If you don’t know what this does read over Wrapping a C library in Java which explains it.

Putting it All Together

Now that we have all the pieces we can do something useful. Useful in terms of an example app demonstrating everything working.

Main.java

import java.util.Arrays;

class Main {

    public static void main(String args[]) {
        int[]          da;
        DemoFuncs.Days day;
        boolean        ret;
        Name           myname;

        da = DemoFuncs.demo_array_return();
        System.out.println("demo_array_return: " + Arrays.toString(da));

        da = DemoFuncs.demo_array_copy_inc(da);
        System.out.println("demo_array_copy_inc: " + Arrays.toString(da));

        day = DemoFuncs.demo_enum_val();
        System.out.println("demo_enum_val: " + day);
        
        day = DemoFuncs.demo_enum_field_val();
        System.out.println("demo_enum_field_val: " + day);

        try {
            DemoFuncs.demo_exception_1();
        } catch (Throwable e) {
            System.out.println("demo_exception_1 caught throwable: " + e.getMessage());
        }

        try {
            DemoFuncs.demo_exception_2();
            System.out.println("demo_exception_2 OK");
        } catch (Throwable e) {
            System.out.println("demo_exception_2 caught throwable: " + e.getMessage());
        }

        try {
            DemoFuncs.demo_exception_3();
        } catch (Exception e) {
            System.out.println("demo_exception_3 caught exception: " + e.getMessage());
        }

        ret = DemoFuncs.demo_exception_4();
        if (ret) {
            System.out.println("demo_exception_4: SUCCESS!");
        } else {
            System.out.println("demo_exception_4: FAILURE!");
        }

        myname = new Name(1, 2);
        myname.setName("John Doe");
        System.out.println("Name from Java: " + myname);

        myname = DemoFuncs.demo_name("Jane Doe");
        System.out.println("Name from JNI: " + myname);
    }
}

There isn’t all that much to see here. It just calls all of our examples to demonstrate they work. The exception is `Name` examples. First, we create a `Name` object in java (the normal way) to show that works. Then we have one created by the JNI C code to show that works. Also, that the object from JNI is different than the first one.

Build

To make building easy I’m going to use CMake. This will build the C library and a .jar file with our Java application.

This is the same processes we used to wrap a C library.

CMakeLists.txt

cmake_minimum_required(VERSION 3.0)
project(demo)

find_package(Java REQUIRED)
find_package(JNI REQUIRED)
include(UseJava)

add_library (demo_lib SHARED
    demo_funcs.c
)
target_link_libraries (demo_lib ${JAVA_JVM_LIBRARY})
target_include_directories(demo_lib PRIVATE
    ${CMAKE_CURRENT_BINARY_DIR}
    ${CMAKE_CURRENT_SOURCE_DIR}
    ${JNI_INCLUDE_DIRS}
)

add_jar(${PROJECT_NAME}
        DemoFuncs.java 
        Main.java
        Name.java
    ENTRY_POINT Main
)

And now we can build.

$ mkdir build && cd build
$ JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_72.jdk/Contents/Home/ cmake ..
$ make
$ java -jar demo.jar

Output

All that’s left is running the test app and verifying everything works.

demo_array_return: [1, 2, 3, 4, 5, 6, 7, 8]
demo_array_copy_inc: [2, 4, 6, 8, 5, 6, 7, 8]
demo_enum_val: WED
demo_enum_field_val: THU
demo_exception_1 caught throwable: I/Don't/Exist!
demo_exception_2 caught throwable: I/Don't/Exist!
demo_exception_3 caught exception: Something bad happened...
demo_exception_4: FAILURE!
Name from Java: My name is John Doe. I have 1 dog and 2 cats.
Name from JNI: My name is Jane Doe. I have 3 dogs and 0 cats.

Everything looks right to me.

Conclusion

JNI is very tricky and at times a bit obtuse. It’s so complex that this barely scratches the surface of working with it. What we’ve looked at are the basic things pretty much anyone using JNI needs to know about. Hopefully, you don’t have to work with JNI very often, if ever.

Leave a Reply

Your email address will not be published. Required fields are marked *