Introduction to Java Native Interface: Establishing a bridge between Java and C/C++
JNI (Java Native Interface) is a foreign function interface that allows code running on JVM to call (or be called by) native applications. Using JNI, one can call methods written in C/C++ or even access assembly language.
JNI comes in handy when you need to do something that cannot be accomplished by Java alone (low-level code may be needed to talk directly to OS/Hardware which cannot be accomplished by Java alone). It’s also possible that your company developed some software that is now considered legacy and you need to continue development using Java. In that case, you can create an interface between the legacy code and Java so you can build upon it.
How It Works
Let’s examine a simple application that demonstrates the process. You can find the full source code here.
We will start by creating a java class and declare some native methods.
- Native methods are declared using the native keyword.
- System.loadLibrary() is used to load the shared C/C++ library. This is going to be an so file on Linux or dll on Windows.
- As can be seen from lines 11 and 13, native methods are called just like regular ones.
TemperatureData is used as a simple data object and TemperatureScale is an enumeration.
Compile and create JNI headers
I created a Makefile to easily build and run the sample application. Let’s go over it:
First, we need to specify Java’s home directory (JAVA_HOME). Creating separate folders for everything is not necessary, but it’s a good idea to keep things organized, which saves a lot of time when cleaning a project.
At line 10, we use javac command to compile our Java code to bytecode (.class files) and create the JNI headers. -h option is given to specify where headers should go after building.
After performing this step, a header file will be created under MY_JNI_HEADERS folder (com_jni_example_TemperatureSampler.h).
You can see that our native getTemperature and getDetailedTemperature methods are present in this file. Naming of these methods follows this convention :
At this point, all you need to do is create another C/C++ file and implement these methods. After doing that, line 11 of Makefile creates an object file from your cpp source, and line 12 converts it into a dynamic library.
Native Side Implementation
Simply import the header file, copy the method signature and fill it in. Let’s start with the simpler getTemperature method:
Primitive types don’t need special conversions, so simply returning the number from getTemperature method is fine. For complex types, we need to do a lot more work:
If you’re familiar with Java’s Reflection API, you’ll notice this is very similar to it.
JNI provides all necessary methods to get/convert Java objects using the JNIEnv pointer, which is passed to all JNI method calls.
[Line 4–5] We get a hold of TemperatureData class by calling FindClass method and then create an instance of TemperatureData using AllocObject.
[Line 8] Same as TemperatureData, we grab the TemperatureScale class. We don’t instantiate it since it’s an Enumeration, we will access its values later.
[Line 11–12–13] We get fields of TemperatureData by providing the class, field name and field’s type to GetFieldID method.
[Line 16–17] Enumeration values are considered static fields, so we have to access them using GetStaticFieldID method. Its signature is the same as GetFieldID.
[Line 29–30–31–32] Remember the TemperatureData that we instantiated at the beginning? Now that we have all its fields, we can set them using Set<Type>Field functions. Based on our field types, we only need to use SetObjectField and SetFloatField methods. After setting these values, we simply return the object.
As you can see, Java classes are referenced by using their full package names, starting with L (like Ljava/lang/String; for String). If you want to access an array, your type simply starts with [ ( [Ljava/lang/String for a String array )
Primitive types are easier, as they have their own simple definitions. Here’s a list of all of them:
Z = boolean
B = byte
S = short
I = int
J = long
F = float
D = double
C = char
L = any non-primitive(Object)[Z = boolean array
[B = byte array
[S = short array
[I = int array
[J = long array
[F = float array
[D = double array
[C = char array
[L = any non-primitive(Object)array
How about calling a Java function from Native Side?
You might’ve noticed that I skipped lines 20–26 which demonstrates this. Now let’s take a look at it.
[Line 20–21–22] thisObject that is passed to JNI methods is actually the caller of native method. We can grab that object’s class using GetObjectClass method. After that, GetMethodID is used to grab one of its methods. Third parameter of GetMethodID describes the parameters and return value of the function. Since our getPreferredScale doesn’t have any parameters, inside parentheses are empty. We do have a return value however, which is TemperatureScale. Similar to how we grabbed field ids, we provide its full package name here.
If we had two parameters, let’s say (String, float), our method signature would look like this:
[Line 24–25] IsSameObject method is used to compare two enumeration values (it’s basically the equals method).
Things to Consider
Lastly, I want to talk a little bit about pitfalls of using JNI.
- Your application loses platform portability. If you need to support different platforms, you will need to compile the native layers for each of those platforms.
- Say goodbye to garbage collection once you step into native side. You need to handle everything manually here. If you forget to free things, memory leaks occur.
- Need to be extremely careful on native side, since a simple error can lead to full system crash (Segmentation Fault, we meet again!). Because of this, it’s difficult to debug runtime errors.