Java Native Interface (JNI)
Overview
The Java Native Interface allows Java code to execute apps written in other languages, and allows one to embed the JVM into native applications.
Here is an example:
class HelloWorld { public native void displayHelloWorld(); static { System.loadLibrary("hello"); } public static void main(String[] args) { new HelloWorld().displayHelloWorld(); } }
Declare a Native Method
You must declare all methods, whether Java methods or native methods, within a class on the Java side. When you write a method implementation in a language other than Java, you must include the keyword native as part of the method's definition within the Java class. The native keyword signals to the Java compiler that the function is a native language function. It is easy to tell that the implementation for the HelloWorld class's displayHelloWorld method is written in another programming language because the native keyword appears as part of its method definition:This native method declaration in your Java class provides only the method signature for displayHelloWorld. It provides no implementation for the method. You must provide the implementation for displayHelloWorld in a separate native language source file.public native void displayHelloWorld();The method declaration for displayHelloWorld also indicates that the method is a public instance method, accepts no arguments, and returns no value.
Load the Library
You compile the native language code that implements displayHelloWorld into a shared library (you will do this in Step 5: Create a Shared Library). The runtime system later loads the shared library into the Java class that requires it. Loading the library into the Java class maps the implementation of the native method to its declaration.The HelloWorld class uses the System.loadLibrary method. The System.loadLibrary method loads the shared library that will be created when you compile the implementation code. Place this method within a static initializer. The argument to System.loadLibrary is the shared library name. This can be any name that you choose. The system uses a standard, but platform-specific, approach to convert the library name to a native library name. For example, the Solaris system converts the library name "hello" to libhello.so, while a Microsoft Windows system converts the same name to hello.dll.
The following static initializer from the HelloWorld class loads the appropriate library, named hello. The runtime system executes a class's static initializer when it loads the class.
static { System.loadLibrary("hello"); }
Write the Main Method
The HelloWorld class, because it is an application, also includes a main method to instantiate the class and call the native method. The main method instantiates HelloWorld and calls the displayHelloWorld native method.You can see from the code sample that you call a native method in the same manner as you call a regular method: just append the name of the method to the end of the object name, separated with a period ('.'). A matched set of parentheses, (), follow the method name and enclose any arguments to pass into the method. The displayHelloWorld method doesn't take any arguments.public static void main(String[] args) { new HelloWorld().displayHelloWorld(); }
Step 2: Compile the Java Code
Use the Java compiler to compile the class that you created in the previous step. Here's the command to use:javac HelloWorld.java
Step 3: Create the .h File
In this step, you use the javah utility program to generate a header file (a .h file) from the HelloWorld class. The header file provides a C function signature for the implementation of the native method displayHelloWorld defined in that class.Run javah now on the HelloWorld class that you created in the previous steps.
The name of the header file is the Java class name with a .h appended to the end. For example, the command shown above will generate a file named HelloWorld.h.
By default, javah places the new .h file in the same directory as the .class file. Use the -d option to instruct javah to place the header files in a different directory.
The Function Definition
Look at the header file HelloWorld.h.The Java_HelloWorld_displayHelloWorld function provides the implementation for the HelloWorld class's native method displayHelloWorld, which you will write in Step 4: Write the Native Method Implementation. You use the same function signature when you write the implementation for the native method./* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class HelloWorld */ #ifndef _Included_HelloWorld #define _Included_HelloWorld #ifdef __cplusplus extern "C" { #endif /* * Class: HelloWorld * Method: displayHelloWorld * Signature: ()V */ JNIEXPORT void JNICALL Java_HelloWorld_displayHelloWorld (JNIEnv *, jobject); #ifdef __cplusplus } #endif #endifIf HelloWorld contained any other native methods, their function signatures would appear here as well.
The name of the native language function that implements the native method consists of the prefix Java_, the package name, the class name, and the name of the native method. Between each name component is an underscore "_" separator.
The native method displayHelloWorld within the HelloWorld class becomes Java_HelloWorld_displayHelloWorld. No package name appears in our example because HelloWorld is in the default package, which has no name.
Notice that the implementation of the native function, as it appears in the header file, accepts two parameters even though, in its definition on the Java side, it accepts no parameters. The JNI requires every native method to have these two parameters.
The first parameter for every native method is a JNIEnv interface pointer. It is through this pointer that your native code accesses parameters and objects passed to it from the Java application. The second parameter is jobject, which references the current object itself. In a sense, you can think of the jobject parameter as the "this" variable in Java. For a native instance method, such as the displayHelloWorld method in our example, the jobject argument is a reference to the current instance of the object. For native class methods, this argument would be a reference to the method's Java class. Our example ignores both parameters. The next lesson,
Step 4: Write the Native Method Implementation
Now, you can finally write the implementation for the native method in a language other than Java. Back to that "real world," C and C++ programmers may already have existing implementations of native methods. For those "real" methods, you need only ensure that the native method signature matches the signature generated on the Java side.The function that you write must have the same function signature as the one generated by javah in the HelloWorld.h file in Step 3: Create the .h File. Recall that the function signature generated for the HelloWorld class's displayHelloWorld native method looks like this:
Here's the C language implementation for the native method Java_HelloWorld_displayHelloWorld. This implementation is in the file named HelloWorldImp.c.JNIEXPORT void JNICALL Java_HelloWorld_displayHelloWorld(JNIEnv *, jobject);The implementation for Java_HelloWorld_displayHelloWorld is straightforward. The function uses the printf function to display the string "Hello World!" and then returns.#include <jni.h> #include "HelloWorld.h" #include <stdio.h> JNIEXPORT void JNICALL Java_HelloWorld_displayHelloWorld(JNIEnv *env, jobject obj) { printf("Hello world!\n"); return; }The HelloWorldImp.c file includes three header files:
- jni.h - This header file provides information that the native language code requires to interact with the Java runtime system. When writing native methods, you must always include this file in your native language source files.
- HelloWorld.h - The .h file that you generated in Step 3: Create the .h File.
- stdio.h - The code snippet above includes stdio.h because it uses the printf function. The printf function is part of the stdio.h library.
Step 5: Create a Shared Library
Remember in Step 1: Write the Java Code you used the following method call to load a shared library named hello into your program at runtime:Now you are ready to create this shared library.System.loadLibrary("hello");In the previous step, Step 4: Write the Native Method Implementation, you created a C file in which you wrote the implementation for the displayHelloWorld native method. You saved the native method in the file HelloWorldImp.c. Now, you must compile HelloWorldImp.c into a shared library, which you name hello to match the library name used in the System.loadLibrary method.
Compile the native language code that you created in the previous two steps into a shared library. On Solaris, you'll create a shared library, while on Windows 95/NT you'll create a dynamic link library (DLL). Remember to specify the path or paths to all necessary header files. See Creating and Loading Shared Libraries for information on forming a shared library name.
On Solaris, the following command builds a shared library libhello.so:
cc -G -I/usr/local/java/include -I/usr/local/java/include/solaris \ HelloWorldImp.c -o libhello.soOn Microsoft Windows, the following command builds a dynamic link library hello.dll using Microsoft Visual C++ 4.0:
Of course, you need to specify the include path that corresponds to the setup on your own machine.cl -Ic:\java\include -Ic:\java\include\win32 -LD HelloWorldImp.c -Fehello.dll
Step 6: Run the Program
Now run the Java application (the HelloWorld class) with the Java interpreter, as follows:
java HelloWorldYou should see the following output:
If you see an exception like the following, then you don't have your library path set up correctly.Hello World!The library path is a list of directories that the Java runtime system searches when loading libraries. Set your library path now, and make sure that the name of the directory where the hello library lives is in it.java.lang.UnsatisfiedLinkError: no hello in shared library path at java.lang.Runtime.loadLibrary(Runtime.java) at java.lang.System.loadLibrary(System.java) at at java.lang.Thread.init(Thread.java)
Integrating Java and Native Programs
The Java Native Interface defines a standard naming and calling convention so that the Java Virtual Machine (VM) can locate and invoke your native methods. This section shows you how to follow the JNI naming and calling conventions so that you can use JNI functions from a native method. It also teaches you how to declare types so that they can be correctly recognized by both the Java program and the native method.
Declaring Native Methods
On the Java side, you declare a native method with the native keyword and an empty method body. On the native side, you provide an implementation for the native method. You must be careful when writing native methods to "match" the native function implementation with the method signature in the Java header file. The javah tool, which is explained in Step 3: Create the .h file, helps you to generate native function prototypes that match the Java-side native method declaration.
Mapping between Java and Native Types
The JNI defines a mapping of Java types and native language (C/C++) types. This section introduces the native types corresponding to both primitive Java types, such as int and double, and Java objects, including strings and arrays.
Declaring Native Methods
This section illustrates how to declare a native method in Java and how to generate the corresponding C/C++ function prototype.
The Java Side
Our first example, Prompt.java, contains a native method that accepts and prints a Java string. The program calls the native method, which waits for user input and then returns the line the user typed in.
The Prompt class contains a main method, which is used to invoke the program, and a native method named getLine, which is declared as follows:
private native String getLine(String prompt);Notice that the declarations for native methods are almost identical to the declarations for regular, non-native Java methods. However, you declare native methods differently, as follows:
- First, native methods must have the native keyword. The native keyword informs the Java compiler that the implementation for this method is provided in another language.
- Secondly, the native method declaration is terminated with a semicolon (the statement terminator symbol) because the Java class file does not include implementations for native methods.
The Native Language Side
You must declare and implement native methods in a native language, such as C or C++. Before you do this, it is helpful to generate the header file that contains the function prototype for the native method implementation.Compile the Prompt.java file and then generate the .h file. First, compile the Prompt.java file as follows:
javac Prompt.javaOnce you have successfully compiled Prompt.java and have created the Prompt.class file, you can generate a JNI-style header file by specifying a -jni option to javah:
javah -jni PromptExamine the Prompt.h file. Note the function prototype for the native method getLine that you declared in Prompt.java.
JNIEXPORT jstring JNICALL Java_Prompt_getLine(JNIEnv *, jobject, jstring);The native method function definition in the implementation code must match the generated function signature in the header file. Always include JNIEXPORT and JNICALL in your native method function signatures. JNIEXPORT and JNICALL ensure that the source code compiles on platforms such as Microsoft Windows that require special keywords for functions exported from dynamic link libraries.
Native method names are concatenated from the following components:
- the prefix Java_
- the fully qualified class name
- an underscore "_" separator
- the method name
Thus, the native code implementation for the Prompt.getLine method becomes Java_Prompt_getLine. (Remember that no package name component appears because the Prompt class is in the default package.)
Overloaded native method names, in addition to the above components, have an extra two underscores "__" appended to the method name followed by the argument signature. To illustrate, we created a second version of the Java program, Prompt2.java, and overloaded the getLine method by adding a second argument of type int. The two getLine method names in the Prompt2.h header file look as follows:
Java_Prompt2_getLine__Ljava_lang_String_2 Java_Prompt2_getLine__Ljava_lang_String_2IRecall from Step 3: Create the .h File, each native method has two parameters in addition to those parameters that you declared on the Java side. The first parameter, JNIEnv *, is the JNI interface pointer. This interface pointer is organized as a function table, with every JNI function at a known table entry point. Your native method invokes specific JNI functions to access Java objects through the JNIEnv * pointer. The jobject parameter is a reference to the object itself (it is like the this pointer in Java).
Lastly, notice that the JNI has a set of type names, such as jobject and jstring, and each type corresponds to Java types. This is covered in the next section.
Mapping Between Java and Native Types
In this section, you will learn how to reference Java types in your native method. You need to do this when you want to:
- Access arguments passed in to a native method from a Java application.
- Create Java objects in your native method.
- Have your native method return results to the caller.
Java Primitive Types
Your native method can directly access Java primitive types such as booleans, integers, floats, and so on, that are passed from programs written in Java. For example, the Java type boolean maps to the native language type jboolean (represented as unsigned 8 bits), while the Java type float maps to the native language type jfloat (represented by 32 bits). The following table describes the mapping of Java primitive types to native types.
Primitive Types and Native Equivalents
Java Type Native Type Size in bits boolean jboolean 8, unsigned byte jbyte 8 char jchar 16, unsigned short jshort 16 int jint 32 long jlong 64 float jfloat 32 double jdouble 64 void void n/a
Java Object Types
Java objects are passed by reference. All references to Java objects have the type jobject. For convenience and to avoid programming errors, the JNI implements a set of types that are conceptually all subclassed from (or are "subtypes" of) jobject, as follows:
In our Prompt.java example, the native method getLine takes a Java string as an argument and returns a Java string:
Its corresponding native implementation has type jstring for both the argument and the return value:private native String getLine(String prompt);JNIEXPORT jstring JNICALL Java_Prompt_getLine(JNIEnv *, jobject, jstring);As mentioned above, jstring corresponds to the Java type String. Notice that the second argument to Java_Prompt_getLine, which is the reference to the object itself, has type jobject.
Accessing Java Strings
Strings are a particularly useful kind of object. The JNI provides a set of string manipulation functions to ease the task of handling Java strings in native code. Using these functions, the programmer can translate between Java strings and native strings in Unicode and UTF-8 formats.
Accessing Java Arrays
Arrays are another kind of frequently-used Java object. You can use JNI array manipulation functions to create arrays and access array elements.
Calling Java Methods
The JNI supports a complete set of "callback" operations that allow you to invoke a Java method from the native code. You locate the method using its name and signature. You can also invoke both class and instance methods. Use the javap tool to generate JNI-style method signatures from class files.
Accessing Java Member Variables
The JNI allows you to locate a Java member variable using the member variable's name and type signature. You can locate and access both class and instance member variables. The javap tool helps you to generate JNI-style member variable signatures from class files.
Catching and Throwing Exceptions
This section teaches you how to deal with exceptions from within a native method implementation. Your native method can catch, throw, and clear exceptions.
Local and Global References
Native code can refer to Java objects using either local or global references. Local references are only valid within a native method invocation. Local references are freed automatically after the native method returns. Global references remain valid throughout an application. You must explicitly allocate and free global references.
Threads and Native Methods
This section describes the implications of running native methods in the multi-threaded Java platform. The JNI offers basic synchronization constructs for native methods.
JNI Programming in C++
In C++, the JNI presents a slightly cleaner interface and performs additional static type checking.
Accessing Java Strings in Native Methods
When a Java application passes a string to a native method, it passes the string as a jstring type. This jstring type is different from the regular C string type (char *). If your code tries to print a jstring directly, it will likely result in a VM crash. For example, the following code segment incorrectly tries to print a jstring and may result in a VM crash:/* DO NOT USE jstring THIS WAY !!! */ JNIEXPORT jstring JNICALL Java_Prompt_getLine(JNIEnv *env, jobject obj, jstring prompt) { printf("%s", prompt); ...Your native method code must use JNI functions to convert Java strings to native strings. The JNI supports the conversion to and from native Unicode and UTF-8 strings. In particular, UTF-8 strings use the highest bit-to-signal multibyte characters; they are therefore upwards-compatible with 7-bit ASCII. In Java, UTF-8 strings are always 0-terminated.
Accessing Java Strings
Your native method needs to call GetStringUTFChars to correctly print the string passed to it from a Java application. GetStringUTFChars converts the built-in Unicode representation of a Java string into a UTF-8 string. Once you are certain that the string only contains 7-bit ASCII characters, you can directly pass the string to regular C language functions, such as printf, as is shown in Prompt.c:When your native code is finished using the UTF-8 string, it must call ReleaseStringUTFChars. ReleaseStringUTFChars informs the VM that the native method is finished with the string so that the VM can free the memory taken by the UTF-8 string. Failing to call ReleaseStringUTFChars results in a memory leak. This will ultimately lead to system memory exhaustion.JNIEXPORT jstring JNICALL Java_Prompt_getLine(JNIEnv *env, jobject obj, jstring prompt) { char buf[128]; const char *str = (*env)->GetStringUTFChars(env, prompt, 0); printf("%s", str); (*env)->ReleaseStringUTFChars(env, prompt, str); ...The native method can also construct a new string using the JNI function NewStringUTF. The following lines of code from Java_Prompt_getLine show this:
... scanf("%s", buf); return (*env)->NewStringUTF(env, buf); }
Using the JNIEnv Interface Pointer
Native methods must access and manipulate Java objects, such as strings, through the env interface pointer. In C, this requires using the env pointer to reference the JNI function. Notice how the native method uses the env interface pointer to reference the two functions, GetStringUTFChars and ReleaseStringUTFChars, that it calls. Not only does the native method use env as an interface pointer, env is passed as the first parameter to these functions.
Other JNI Functions for Accessing Java Strings
The JNI also provides functions to obtain the Unicode representation of Java strings. This is useful, for example, on those operating systems that support Unicode as the native format. There are also utility functions to obtain both the UTF-8 and Unicode length of Java strings.
- GetStringChars takes the Java string and returns a pointer to an array of Unicode characters that comprise the string.
- ReleaseStringChars releases the pointer to the array of Unicode characters.
- NewString constructs a new java.lang.String object from an array of Unicode characters.
- GetStringLength returns the length of a string that is comprised of an array of Unicode characters.
- GetStringUTFLength returns the length of a string if it is represented in the UTF-8 format.
Working With Java Arrays in Native Methods
The JNI uses the jarray type to represent references to Java arrays. Similar to jstring, you cannot directly access jarray types in your native method C code. Instead, you use functions provided by the JNI that allow you to obtain pointers to elements of integer arrays.Our second example, IntArray.java, contains a native method that totals up the contents of an integer array passed to it from a Java application. You cannot implement the native method by directly addressing the array elements. The following code snippet incorrectly tries to access the array elements directly:
/* This program is illegal! */ JNIEXPORT jint JNICALL Java_IntArray_sumArray(JNIEnv *env, jobject obj, jintArray arr) { int i, sum = 0; for (i=0; i<10; i++) { sum += arr[i]; } ...The C program IntArray.c shows the correct way to implement the above function Java_IntArray_sumArray. In this example, you use one JNI function to get the length of the array. Use another JNI function to obtain a pointer to the individual elements of the array. Then, you can retrieve the elements. Lastly, use a third JNI function to release the array memory.
Accessing Arrays of Primitive Elements
First, obtain the length of the array by calling the JNI function GetArrayLength. Unlike C language arrays, Java arrays carry length information.
JNIEXPORT jint JNICALL Java_IntArray_sumArray(JNIEnv *env, jobject obj, jintArray arr) { int i, sum = 0; jsize len = (*env)->GetArrayLength(env, arr); ...Next, obtain a pointer to the elements of the array. Our example contains an integer array so we use the JNI function GetIntArrayElements to obtain this pointer. Once you obtain the pointer, you can use normal C language operations on the resulting integer array.
{ ... jint *body = (*env)->GetIntArrayElements(env, arr, 0); for (i=0; i<len; i++) { sum += body[i]; } ...The JNI provides a set of functions to obtain array element pointers; use the function that corresponds to the primitive type of the array. Had our example contained a float array arr, for example, we would have used the JNI function GetFloatArrayElements to obtain a pointer to its elements, as follows:
In general, the garbage collector may move Java arrays. However, the Java Virtual Machine guarantees that the result of GetIntArrayElements points to a nonmovable array of integers. The JNI will either "pin" down the array or it will make a copy of the array into nonmovable memory. Because of this, the native code must call ReleaseIntArrayElements when it has finished using the array, as follows:... int i; float sum = 0; ... jfloat *body = (*env)->GetFloatArrayElements(env, arr, 0); for (i=0; i<len; i++) { sum += body[i]; } ...Similar to the Get<type>ArrayElements functions, the JNI provides a set of Release<type>ArrayElements functions. Do not forget to call the appropriate Release<type>ArrayElements function, such as ReleaseIntArrayElements. If you forget to make this call, the array stays pinned for an extended period of time. Or, the Java Virtual Machine is unable to reclaim the memory used to store the nonmovable copy of the array.... (*env)->ReleaseIntArrayElements(env, arr, body, 0); return sum; }The JNI provides a set of functions to access arrays of every primitive type, including boolean, byte, char, short, int, long, float, and double. Each function in the following table accesses elements in the specified Java type of array:
JNI Functions for Accessing Arrays
Function Array Type GetBooleanArrayElements boolean GetByteArrayElements byte GetCharArrayElements char GetShortArrayElements short GetIntArrayElements int GetLongArrayElements long GetFloatArrayElements float GetDoubleArrayElements double The JNI also provides a set of functions to release arrays of different primitive types. The following table summarizes these functions.
JNI Functions for Releasing Arrays
Function Array Type ReleaseBooleanArrayElements boolean ReleaseByteArrayElements byte ReleaseCharArrayElements char ReleaseShortArrayElements short ReleaseIntArrayElements int ReleaseLongArrayElements long ReleaseFloatArrayElements float ReleaseDoubleArrayElements double
Accessing a Small Number of Elements
Note that the Get<type>ArrayElements function might potentially copy the entire array. You may want to limit the number of elements that are copied, especially if your array is large. If you are only interested in a small number of elements in a (potentially) large array, you should instead use the Get/Set<type>ArrayRegion functions. These functions allow you to access, via copying, a small set of elements in an array.
Accessing Arrays of Objects
The JNI provides a separate set of functions to access elements of object arrays. You can use these functions to get and set individual object array elements. You cannot get all the object array elements at once.
- GetObjectArrayElement returns the object element at a given index.
- SetObjectArrayElement updates the object element at a given index.
Calling Java Methods
This section illustrates how to call Java methods from native language methods. Our example program, Callbacks.java, invokes a native method. The native method then makes a call back to a Java method. To make things a little more interesting, the Java method again (recursively) calls the native method. This process continues until the recursion is five levels deep, at which time the Java method returns without making any more calls to the native method. To help you see this, the Java method and the native method print a sequence of tracing information.
Calling a Java Method from Native Code
To see how native code calls a Java method, let us focus on the implementation of Callbacks_nativeMethod, which is implemented in Callbacks.c. This native method contains a call back to the Java method Callbacks.callback.You can call an instance method by following these three steps:JNIEXPORT void JNICALL Java_Callbacks_nativeMethod(JNIEnv *env, jobject obj, jint depth) { jclass cls = (*env)->GetObjectClass(env, obj); jmethodID mid = (*env)->GetMethodID(env, cls, "callback", "(I)V"); if (mid == 0) { return; } printf("In C, depth = %d, about to enter Java\n", depth); (*env)->CallVoidMethod(env, obj, mid, depth); printf("In C, depth = %d, back from Java\n", depth); }
- Your native method calls the JNI function GetObjectClass, which returns the Java class object that is the type of the Java object.
- Your native method then calls the JNI function GetMethodID, which performs a lookup for the Java method in a given class. The lookup is based on the name of the method as well as the method signature. If the method does not exist, GetMethodID returns zero (0). An immediate return from the native method at that point causes a NoSuchMethodError to be thrown in the Java application code.
- Lastly, your native method calls the JNI function CallVoidMethod. The CallVoidMethod function invokes an instance method that has void return type. You pass the object, method ID, and the actual arguments to CallVoidMethod.
Forming the Method Name and Method Signature
The JNI performs a symbolic lookup based on the method's name and type signature. This ensures that the same native method will work even after new methods have been added to the corresponding Java class.
The method name is the Java method name in UTF-8 form. Specify the constructor of a class by enclosing the word init within angle brackets (this appears as "<init>").
Note that the JNI uses the method signature to denote the return type of a Java method. The signature (I)V, for example, denotes a Java method that takes one argument of type int and has a return type void. The general form of a method signature argument is:
"(argument-types)return-type"The following table summarizes the encoding for the Java type signatures:
Java VM Type Signatures
Signature Java Programming Language Type Z boolean B byte C char S short I int J long F float D double L FQC; fully-qualified-class [ type type[] ( arg-types ) ret-type method type For example, the Prompt.getLine method has the signature:
Prompt.getLine takes one parameter, a Java String object, and the method type is also String.(Ljava/lang/String;)Ljava/lang/String;The Callbacks.main method has the signature:
The signature indicates that the Callbacks.main method takes one parameter, a Java String object, and the method type is void.([Ljava/lang/String;)VArray types are indicated by a leading square bracket ([) followed by the type of the array elements.
Using javap to Generate Method Signatures
The Java class file disassembler tool, javap, helps you to eliminate the mistakes that can occur when deriving method signatures by hand. You can use the javap tool to print out member variables and method signatures for specified classes. Run the javap tool with the options -s and -p and give it the name of a Java class, as follows:
javap -s -p PromptThis gives you the following output:
Compiled from Prompt.java class Prompt extends java.lang.Object /* ACC_SUPER bit set */ { private native getLine (Ljava/lang/String;)Ljava/lang/String; public static main ([Ljava/lang/String;)V <init> ()V static <clinit> ()V }The "-s" flag informs javap to output signatures rather than normal Java types. The "-p" flag instructs javap to include private members.
Calling Java Methods Using Method IDs
When you invoke a method in the JNI, you pass the method ID to the actual method invocation function. Obtaining a method ID is a relatively expensive operation. Because you obtain the method ID separately from the method invocation, you need only perform this operation once. Thus, it is possible to first obtain the method ID one time and then use the method ID many times at later points to invoke the same method.
It is important to keep in mind that a method ID is valid only for as long as the class from which it is derived is not unloaded. Once the class is unloaded, the method ID becomes invalid. As a result, if you want to cache the method ID, be sure to keep a live reference to the Java class from which the method ID is derived. As long as the reference to the Java class (the jclass value) exists, the native code keeps a live reference to the class. The section Local and Global References explains how to keep a live reference even after the native method returns and the jclass value goes out of scope.
Passing Arguments to Java Methods
The JNI provides several ways to pass arguments to a Java method. Most often, you pass the arguments following the method ID. There are also two variations of method invocation functions that take arguments in an alternative format. For example, the CallVoidMethodV function receives all its arguments in one va_list type argument. A va_list type is a special C type that allows a C function to accept a variable number of arguments. The CallVoidMethodA function expects the arguments in an array of jvalue union types. The array of jvalue union types are as follows:
typedef union jvalue { jboolean z; jbyte b; jchar c; jshort s; jint i; jlong j; jfloat f; jdouble d; jobject l; } jvalue;In addition to the CallVoidMethod function, the JNI also supports instance method invocation functions with other return types, such as CallBooleanMethod, CallIntMethod, and so on. The return type of the method invocation function must match with the type of the Java method you wish to invoke.
Calling Class Methods
You can call a Java class method from your native code in a similar manner to calling an instance method. Call a class method by following these steps:If you compare instance method invocation functions to class method invocation functions, you will notice that instance method invocation functions receive the object, rather than the class, as the second argument following the JNIEnv argument. For example, suppose we add a incDepth class method into Callback.java.
- Obtain the method ID using the JNI function GetStaticMethodID rather than the function GetMethodID.
- Pass the class, method ID, and arguments to the family of class method invocation functions: CallStaticVoidMethod, CallStaticBooleanMethod, and so on.
We can call this class method incDepth from Java_Callback_nativeMethod using the following JNI functions:static int incDepth(int depth) {return depth + 1};JNIEXPORT void JNICALL Java_Callbacks_nativeMethod(JNIEnv *env, jobject obj, jint depth) { jclass cls = (*env)->GetObjectClass(env, obj); jmethodID mid = (*env)->GetStaticMethodID(env, cls, "incDepth", "(I)I"); if (mid == 0) { return; } depth = (*env)->CallStaticIntMethod(env, cls, mid, depth); ...
Calling Instance Methods of a Superclass
You can call instance methods defined in a superclass that have been overridden in the class to which the object belongs. The JNI provides a set of CallNonvirtual<type>Method functions for this purpose. To call instance methods from the superclass that defined them, you do the following:It is rare that you will need to invoke the instance methods of a superclass. This facility is similar to calling a superclass method, such as f, in Java using the following construct:
- Obtain the method ID from the superclass using GetMethodID rather than GetStaticMethodID.
- Pass the object, superclass, method Id, and arguments to the family of nonvirtual invocation functions: CallNonvirtualVoidMethod, CallNonvirtualBooleanMethod, and so on.
super.f();
Accessing Java Member Variables
The JNI provides functions that native methods use to get and set Java member variables. You can get and set both instance and class member variables. Similar to accessing methods, you use one set of JNI functions to access instance member variables and another set of JNI functions to access class member variables.
Our example program, FieldAccess.java, contains a class with one class integer member variable si and an instance string member variable s. The example program calls the native method accessFields, which prints out the value of these two member variables and then sets the member variables to new values. To verify the member variables have indeed changed, we print out their values again in the Java application after returning from the native method.
Procedure for Accessing a Java Member Variable
To get and set Java member variables from a native language method, you must do the following:
Just as we did when calling a Java method, we factor out the cost of member variable lookup using a two-step process. First we obtain the member variable ID, then use the member variable ID to access the member variable itself. The member variable ID uniquely identifies a member variable in a given class. Similar to method IDs, a member variable ID remains valid until the class from which it is derived is unloaded.
- Obtain the identifier for that member variable from its class, name, and type signature. For example, in FieldAccess.c, we get the identifier for the class integer member variable si as follows:
and we get the identifier for the instance string member variable s as follows:fid = (*env)->GetStaticFieldID(env, cls, "si", "I");fid = (*env)->GetFieldID(env, cls, "s", "Ljava/lang/String;");- Use one of several JNI functions to either get or set the member variable specified by the member variable identifier. To get the value of a class member variable, pass the class to one of the appropriate class member variable access functions. To get the value of an instance member variable, pass the object to the appropriate instance member variable access function. For example, in FieldAccess.c, we use GetStaticIntField to get the value of the class integer member variable si, as follows:
We use the function GetObjectField to get the value of the instance string member variable s:si = (*env)->GetStaticIntField(env, cls, fid);jstr = (*env)->GetObjectField(env, obj, fid);
Member Variable Signatures
Specify member variable signatures following the same encoding scheme as method signatures. The general form of a member variable signature is:
"member variable type"The member variable signature is the encoded symbol for the type of the member variable, enclosed in double quotes (""). The member variable symbols are the same as the argument symbols in the method signature. That is, you represent an integer member variable with "I", a float member variable with "F", a double member variable with "D", a boolean member variable with "Z", and so on.
The signature for a Java object, such as a String, begins with the letter L, followed by the fully-qualified class for the object, and terminated by a semicolon (;). Thus, you form the member variable signature for a String variable, such as c.s in FieldAccess.java, as follows:
"Ljava/lang/String;"To indicate an array, use a leading square bracket ([) followed by the type of the array. For example, you designate an integer array as follows:
"[I"Refer to the table in the previous section that summarizes the encoding for the Java type signatures and their matching Java types.
You can use the Java class disassembler tool javap with option "-s" to generate the member variable signatures from class files. For example, run:
This gives you output containing the following two member variable signatures:javap -s -p FieldAccess... static si I s Ljava/lang/String; ...
Handling Java Errors from Native Methods
When an exception is thrown in Java, the Java Virtual Machine automatically looks for the nearest enclosing exception handler and unwinds the stack if necessary. This style of exception handling frees the programmer from caring about and handling unusual error cases at every operation in the program. Instead, the Java Virtual Machine propagates the error conditions automatically to a location (the catch clause in Java) that can handle the same class of error conditions in a centralized way.
While other languages such as C++ support a similar notion of exception handling, there is no uniform and general way to throw and catch exceptions in native languages. The JNI therefore requires you to check for possible exceptions after calling JNI functions. The JNI also provides functions that allow your native methods to throw Java exceptions. These exceptions can then be handled either by other parts of your native code or by the Java Virtual Machine. After the native code catches and handles an exception, it can either clear the pending exception so that the computation may continue, or it can throw another exception for an outer exception handler.
Many JNI functions may cause an exception to be thrown. For example, the GetFieldID function described in the previous section throws a NoSuchFieldError if the specified field does not exist. To simplify error checking, most JNI functions use a combination of error codes and Java exceptions to report error conditions. For example, you may check if the jfieldID returned from GetFieldID is zero (0) instead of calling the JNI function ExceptionOccurred. When the result of GetFieldID is not zero (0), you are guaranteed that there is no pending exception.
The remainder of this section illustrates how to catch and throw exceptions in native code. The example code is in CatchThrow.java.
The CatchThrow.main method calls the native method. The native method, defined in CatchThrow.c, first invokes the Java method CatchThrow.callback, as follows:
... jclass cls = (*env)->GetObjectClass(env, obj); jmethodID mid = (*env)->GetMethodID(env, cls, "callback", "()V"); jthrowable exc; if (mid == 0) { return; } (*env)->CallVoidMethod(env, obj, mid); ...Because CallVoidMethod throws a NullPointerException, the native code can detect this exception after CallVoidMethod returns by calling the method ExceptionOccurred:
... exc = (*env)->ExceptionOccurred(env); if (exc) { ...You can see that it is a simple matter to catch and handle an exception. In our example, we do not do much about the exception in CatchThrow.c except to use the method ExceptionDescribe to output a debugging message. The native method then throws an IllegalArgumentException. It is this IllegalArgumentException that the Java code which invoked the native method will see.
... (*env)->ExceptionDescribe(env); (*env)->ExceptionClear(env); newExcCls = (*env)->FindClass(env, "java/lang/IllegalArgumentException"); if (newExcCls == 0) { /* Unable to find the new exception class, give up. */ return; } (*env)->ThrowNew(env, newExcCls, "thrown from C code"); ...The ThrowNew function constructs an exception object from the given exception class and message string and posts the exception in the current thread.
It is extremely important to check, handle, and clear the pending exception before calling subsequent JNI functions. Calling arbitrary JNI functions with a pending exception may lead to unexpected results. You can safely call only a small number of JNI functions when there is a pending exception. These functions are ExceptionOccurred, ExceptionDescribe, and ExceptionClear.
Local and Global References
So far, we have used data types such as jobject, jclass, and jstring to denote references to Java objects. However, the JNI creates references for all object arguments passed to native methods, as well as all objects returned from JNI functions.
References serve to keep the Java objects from being garbage collected. By default, the JNI creates local references because local references ensure that the Java Virtual Machine can eventually free the Java objects. Local references become invalid when program execution returns from the native method in which the local reference is created. Therefore, a native method must not store away a local reference and expect to reuse it in subsequent invocations.
For example, the following program, which is a variation of the native method in FieldAccess.c, mistakenly caches the Java class for the member variable ID so that it does not have to repeatedly search for the member variable ID based on the member variable name and signature at each invocation:
This program is illegal because the local reference returned from GetObjectClass is valid only until the native method returns. When the Java application calls the native method Java_FieldAccess_accessFields a second time, the native method tries to use an invalid local reference. This leads to either the wrong results or to a VM crash./* This code is illegal */ static jclass cls = 0; static jfieldID fld; JNIEXPORT void JNICALL Java_FieldAccess_accessFields(JNIEnv *env, jobject obj) { ... if (cls == 0) { cls = (*env)->GetObjectClass(env, obj); if (cls == 0) { ... /* error */ } fid = (*env)->GetStaticFieldID(env, cls, "si", "I"); } /* access the member variable using cls and fid */ ... }You can overcome this problem by creating a global reference. A global reference remains valid until it is explicitly freed. The following code rewrites the previous program and correctly uses a global reference to cache the class for the member variable ID:
/* This code is correct. */ static jclass cls = 0; static jfieldID fld; JNIEXPORT void JNICALL Java_FieldAccess_accessFields(JNIEnv *env, jobject obj) { ... if (cls == 0) { jclass cls1 = (*env)->GetObjectClass(env, obj); if (cls1 == 0) { ... /* error */ } cls = (*env)->NewGlobalRef(env, cls1); if (cls == 0) { ... /* error */ } fid = (*env)->GetStaticFieldID(env, cls, "si", "I"); } /* access the member variable using cls and fid */ ... }A global reference keeps the Java Virtual Machine from unloading the Java class, and therefore also ensures that the member variable ID remains valid, as discussed in Accessing Java Member Variables. However, the native code must call DeleteGlobalRef when it no longer needs access to the global reference. Otherwise, the Java Virtual Machine will never unload the corresponding Java object, the Java class referenced by cls above.
In most cases, the native programmer should rely on the Java Virtual Machine to free all local references after the native method returns. In certain situations, however, the native code may need to call the DeleteLocalRef function to explicitly delete a local reference. These situations are:
- You may know that you are holding the only reference to a large Java object and you do not want to wait until the current native method returns before the garbage collector can reclaim the object. For example, in the following program segment, the garbage collector may be able to free the Java object referred to by lref when it is running inside lengthyComputation:
... lref = ... /* a large Java object */ ... /* last use of lref */ (*env)->DeleteLocalRef(env, lref); lengthyComputation(); /* may take some time */ return; /* all local refs will now be freed */ }- You may need to create a large number of local references in a single native method invocation. This may result in an overflow of the internal JNI local reference table. It is a good idea to delete those local references that will not be needed. For example, in the following program segment, the native code iterates through a potentially large array arr consisting of Java strings. After each iteration, the program can free the local reference to the string element:
... for(i = 0; i < len; i++) { jstring jstr = (*env)->GetObjectArrayElement(env, arr, i); ... /* processes jstr */ (*env)->DeleteLocalRef(env, jstr); /* no longer needs jstr */ } ...
Threads and Native Methods
The Java platform is a multithreaded system. Because of this, native methods must be thread-safe programs. Unless you have knowledge to the contrary, such as knowing that the native method is synchronized, you must assume that there can be multiple threads of control executing a native method at any given time. Native methods therefore must not modify sensitive global variables in unprotected ways. That is, they must share and coordinate their access to variables in certain critical sections of code.
Threads and JNI
- The JNI interface pointer (JNIEnv *) is only valid in the current thread. You must not pass the interface pointer from one thread to another, or cache an interface pointer and use it in multiple threads. The Java Virtual Machine will pass you the same interface pointer in consecutive invocations of a native method from the same thread. However, different threads pass different interface pointers to native methods.
You must not pass local references from one thread to another. In particular, a local reference may become invalid before the other thread has had a chance to use it. You should always convert local references to global references in situations where different threads may be using the same reference to a Java object.
Check the use of global variables carefully. Multiple threads might be accessing these global variables at the same time. Make sure you put in appropriate locks to ensure safety.
Thread Synchronization in Native Methods
The JNI provides two synchronization functions that allow you to implement synchronized blocks. In Java, you implement synchronized blocks using the synchronized statement. For example:synchronized (obj) { ... /* synchronized block */ ... }The Java Virtual Machine guarantees that a thread must acquire the monitor associated with a Java object obj before it can execute the statements in the block. Therefore, at any given time, there can be at most one thread running inside the synchronized block.
Native code can perform equivalent synchronization on objects using the JNI functions MonitorEnter and MonitorExit. For example:
A thread must enter the monitor associated with obj before it can continue its execution. A thread is allowed to enter a monitor multiple times. The monitor contains a counter signaling how many times it has been entered by a given thread. MonitorEnter increments the counter when the thread enters a monitor it has already entered. MonitorExit decrements the counter. Other threads can enter the monitor when the counter reaches zero (0).... (*env)->MonitorEnter(env, obj); ... /* synchronized block */ (*env)->MonitorExit(env, obj); ...
Wait and Notify
The functions Object.wait, Object.notify, and Object.notifyAll provide another useful thread synchronization mechanism. While the JNI does not directly support these functions, a native method can always follow the JNI method call mechanism to invoke them.Invoking the Java Virtual Machine
The JDK1.1 ships the Java Virtual Machine as a shared library (or dynamic link library on Microsoft Windows). You can embed the Java Virtual Machine into your native application by linking the native application with the shared library.The JNI supports an Invocation API that allows you to load, initialize, and invoke the Java Virtual Machine. Indeed, the normal way of starting the Java interpreter,
java, is no more than a simple C program that parses the command line arguments and invokes the Java Virtual Machine through the Invocation API.Invoking the Java Virtual Machine
To illustrate, we will write a C program that invokes the Java Virtual Machine and calls the
Prog.mainmethod defined inProg.java:The C code inpublic class Prog { public static void main(String[] args) { System.out.println("Hello World" + args[0]); } }invoke.cbegins with a call toJNI_GetDefaultJavaVMInitArgsto obtain the default initialization settings, such as heap size, stack size, and so on. It then callsJNI_CreateJavaVMto load and initialize the Virtual Machine.JNI_CreateJavaVMfills in two return values:Note that after
jvmrefers to the created Java Virtual Machine. You can use this to destroy the Virtual Machine at a later time, for example.- env is a JNI interface pointer that the current thread can use to access Java features, such as calling a Java method.
JNI_CreateJavaVMsuccessfully returns, the current native thread has bootstrapped itself into the Java Virtual Machine and is therefore running just like a native method. The only difference is that there is no concept of returning to the Java Virtual Machine. Therefore, any local references that you subsequently create will not be freed until you callDestroyJavaVM.Once you have created the Java Virtual Machine, you can issue regular JNI calls to invoke, for example,
Prog.main.DestroyJavaVMattempts to unload the Java Virtual Machine. (The JDK 1.1 Java Virtual Machine cannot be unloaded; therefore DestroyJavaVM always returns an error code.)You need to compile and link
invoke.cwith the Java libraries shipped with JDK1.1. On Solaris, you can use the following command to compile and linkinvoke.c:On Microsoft Windows with Microsoft Visual C++ 4.0, the command line is:cc -I<where jni.h is> -L<where libjava.so is> -ljava invoke.ccl -I<where jni.h is> -MT invoke.c -link <where javai.lib is>\javai.libThis error message indicates that you have set the wrong value for theUnable to initialize threads: cannot find class java.lang.Thread Can't create Java VMvm_args.classpathvariable.You might get a different system error indicating that it cannot find either
libjava.so(on Solaris) orjavai.dll(on Microsoft Windows). If this is the case, addlibjava.sointo yourLD_LIBRARY_PATHon Solaris, or addjavai.dllinto your executable path on Microsoft Windows.The program may report an error that it cannot find the class
Prog. If so, make sure the directory containingProg.classis in thevm_args.classpathvariable as well.Attaching Native Threads
The Invocation API also allows you to attach native threads to a running Java Virtual Machine and have the threads bootstrap themselves into Java threads. This requires that the Java Virtual Machine internally uses native threads. In JDK 1.1, this feature only works on Microsoft Windows. The Solaris version of the Java Virtual Machine uses user-level thread support and is therefore incapable of attaching native threads. In the future, the JDK on Solaris will support native threads.Our example program,
attach.c, therefore, will only work on Microsoft Windows. This example program is a variation ofinvoke.c. Instead of callingProg.mainin the main thread, the native code spawns five threads and then waits for them to finish before it destroys the Java Virtual Machine. Each thread attaches itself to the Java Virtual Machine, invokes theProg.mainmethod, and finally detaches itself from the Virtual Machine before it terminates. Note that the third argument toAttachCurrentThreadis reserved and should be set toNULL.When you call
DetachCurrentThread, you free all local references belonging to the current thread.Limitations of the Invocation API in JDK1.1
As mentioned above, there are a number of limitations of the Invocation API implementation in JDK1.1.
- The user-level Java thread implementation on Solaris requires the Java Virtual Machine to redirect certain Solaris system calls. The set of redirected system calls currently includes
read,readv,write,writev,getmsg,putmsg,poll,open,close,pipe,fcntl,dup,create,accept,recv,send, and so on. This may cause undesirable effects on a hosting native application that also depends on these system calls.- You cannot attach a native thread to the user-thread based Java Virtual Machine on Solaris.
AttachCurrentThreadsimply fails on Solaris (unless your code calls it from the main thread that created the Virtual Machine).- You cannot unload the Java Virtual Machine without terminating the process. The
DestroyJavaVMcall simply returns an error code.