A part of the Java platform since Java 1.1, the Java Native Interface (JNI) standard allows for interoperation between Java code and code written in other languages. The JNI was primarily designed for natively compiled languages, specifically C and C++, but there isn’t really anything to prevent you from using other languages as long as the required calling conventions are supported. Let's look at some of the basics of employing JNI using a very simple example.
Get the code
Download the source code for the examples in this article here.
Using Java to interoperate with natively compiled code usually removes the portability benefits Java brings to the table. However, there are cases when doing so is acceptable, even required, such as when interfacing with legacy libraries, interfacing with hardware or the operating system, or even just improving performance. Writing to the JNI standard does at least guarantee that the native code will work with any JVM implementation.
Using the JNI isn't difficult once you get used to it. Since native methods will be implemented in another language, they don't have a method body. However, any native code must be declared with the native keyword and must be a member of a Java class. Listing A illustrates a simple class that declares a single, native, static method called sum.
After you have your Java class, you must next write the native code. The native method signature must provide a header file that satisfies some specific rules, so it's usually easier to generate it using the javah tool instead of building it by hand. You run javah against a compiled Java .class file, and it generates a suitable C/C++ header file for you. Listing B contains the generated header file for the Test1 found in Listing A. Notice that a single C/C++ function header has been created named Java_Test1_sum.
Implementing the native method
Once you have the header file, you just need to write your native method to match the generated function header, as I’ve done in Listing C. Notice that all native methods receive as the first parameter a pointer to the structure JNIEnv. This structure is used to make calls to JNI functions, which I’ll discuss further in another section. The meaning of the second parameter, jclass, depends on whether the method is static or instance. In the former case, jclass represents a reference to the class object, while in the latter, it’s a reference to the object for which the method was invoked. The final two jint arguments represent the int arguments for the Java method.
Return and parameter types are mapped to native C/C++ types according to some well-established rules of equivalency, as shown in Table A. Some types, such as the two jint arguments in Listing B, can be manipulated directly in the native code, while others may be manipulated only through JNI calls.
The final step is to compile the native method into a shared library (i.e., a .so file in UNIX systems or a .dll file in Windows). Before calling the method in Java, the shared library must be loaded using the System.loadLibrary method. The most common approach is to do this in the static initializer for the class.
Accessing JNI features from native code
The example I just walked through is very simple and doesn't serve any useful purpose except to illustrate how to write a JNI method. Now let’s look at something a bit more advanced, working with nonprimitive types through the JNIEnv structure.
JNI provides many features in the form of functions that your native code can call through the pointer to the JNIEnv structure; these are passed as the first parameter to every native method. Calls to JNI functions take the following format (assuming env is a pointer to JNIEnv):
(*env)-><jni function>( env, <parameters> )
env-><jni function>( < parameters> )
I’ll be presenting the rest of my examples in this article using the C++ format.
Working with arrays
One of the capabilities JNI provides through JNIEnv is the ability to manipulate Java arrays. Two sets of functions are provided: one for manipulating arrays of primitive Java types and another for arrays of objects.
For the sake of speed, arrays of primitive Java types are exposed to native code as pointers to native types, so they can be accessed as regular arrays. Depending on the implementation, the pointer can be a pointer to the actual Java array or to a copy. Either way, the layout of the array is guaranteed to match one of the native types.
To access a Java primitive type array, you use one of the GetXXXArrayElements functions found in Table B, where XXX corresponds to the type of the array. The function takes the Java array as parameter and returns a pointer to an array of the corresponding native type.
When you're done with the array, be sure to call the corresponding ReleaseXXXArrayElements function, passing as parameters the original Java array and the pointer returned by GetXXXArrayElements. If necessary, the release function will copy any changes you made (so they will be reflected in the Java array) and release all resources associated with the array.
To work with an array of Java objects, you’d use the GetObjectArrayElement and SetObjectArrayElement functions to respectively get and set an element of the array. The GetArrayLength function returns the length of any array.
Listing D contains a simple class illustrating the use of native code with Java arrays. The native implementation iterates over an int array and returns the sum of all the elements. For simplicity, this listing contains both the Java and native implementations; I’ve omitted the header file since this can be easily generated by javah.
Working with objects
Another feature JNI provides is the ability to work with Java objects from native code. Using appropriate JNI functions, you can create Java objects, get and set static and instance fields, and call both static and instance methods. JNI identifies fields and methods by ID, and a field or method ID is a required argument for any functions that deal with fields or methods.
Table C lists the JNI functions used to get the IDs of static and instance fields and methods. Each function receives (as arguments) the field or method’s class, its name, and signature, and it returns a corresponding jfieldID or jmethodID.
The class can be obtained through the method GetObjectClass if you have an object instance of that class, or through FindClass if you don’t. The signature is a string derived from the field’s type or method’s arguments and return type, as shown in Table D.
Once you have the class and method or field ID, you can save it for later use, so there’s no need to repeat the obtainment process again.
There are separate functions for accessing fields and methods. Instance fields are accessed using one of the GetXXXField variants appropriate for the field’s type. The GetStaticXXXField functions are used for static fields. To set field values, you use the SetXXXField and SetStaticXXXField functions. Table E contains a full listing of field access functions.
Method access, on the other hand, is granted via the functions CallXXXMethod and CallStaticXXXMethod, where XXX indicated the method’s return type. These functions have variations that allow the arguments to be passed as an array (CallXXXMethodA and CallStaticXXXMethodA) or as a variable size list (CallXXXMethodV and CallStaticXXXMethodV). See Table F for a complete list.
The code in Listing E illustrates calling methods from native code. The native method printRandom gets the method ID of the static method Math.random and calls it several times, printing the result. The same process applies for instance methods.
The JNI is a powerful tool that when used with care can extend the Java language without heavily compromising portability. I’ve touched just the surface here to give you a glimpse of its capabilities and possibilities. I encourage you to check out the JNI Specification, which is part of the JDK documentation, for the complete API. I also recommend the Java tutorial, which you can find here.