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 <init>
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.