When deciding on how to approach the problem, Sun
The communication between Java and native code can be bidirectional, that is, you can access native code from the Java side and access Java code and objects from the native side. More
From the Java side you can call native functions and access memory in the C heap outside the Java heap. From a Java developer’s perspective, calling a native function is no different from calling a Java method. The idea is that a dummy Java method is declared so that other Java code can call it as if it were a typical method. When you declare a
class Sample{ Sample(){ System.loadLibrary("MyNativeLibrary"); } // dummy method private static native void MyNativeMethod(); public static void main(String args[]) { Sample sample = new Sample(); sample.MyNativeFunction(); } }
Notice that when the argument is passed to
loadLibrary
, it does not specify a platform-dependent extension such as a
dynamic link library
(DLL) or Unix
shared object
(SO). Also, note that the library is loaded from the constructor of the class. What do you think would happen if we invoke the static native method before making an instance of the class? Because the native library is not loaded until the constructor is called, there would be an
UnsatisfiedLinkError
exception because the VM would not be able to link the method to a corresponding native function. It is
class Sample{ static{ System.loadLibrary("MyNativeLibrary"); } private static native void MyNativeMethod(); public static void main(String args[]) { Sample sample = new Sample(); sample.MyNativeFunction(); } }
Preparing the native function and library is a little more work. First, we must write a function prototype that matches the arguments and the return type of the dummy method. Furthermore, we must
JDK ships with a tool that autogenerates a header file, which contains the
Sample.h
that has the prototype for the
MyNativeMethod
method.
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h: #ifdef __cplusplus extern "C" { #endif /* * Class: Sample * Method: MyNativeFunction * Signature: ()V */ NIEXPORT void JNICALL Java_Sample_MyNativeMethod(JNIEnv *,jclass); #ifdef __cplusplus } #endif
Let’s go through some of the details of
Sample.h
. The included
jni.h
contains the necessary JNI declarations.
jni.h
includes the only additional external header file,
jni_md.h
, which declares some
#include "Sample.h" #include <stdio.h> JNIEXPORT void JNICALL Java_Sample_MyNativeMethod(JNIEnv *env, jclass clazz){ printf("Java_Sample_MyNativeMethod was called\n"); }
We
Sample.class
,
Java_Sample_MyNativeMethod
will be written to the console window. Even though a class file that has a few native methods can do many useful
To interface two languages, a mapping of different types must be established. If you want to pass some data from language A to language B, you have to make sure that the data is preserved properly. Table 11.1 shows the mapping of Java primitive types to their C/C++ equivalents. Note that some C/C++ types such as short , int , and long are compiler dependent, and the table uses int16 , int32 , and int64 correspondingly.
|
Java |
C/C++ |
JNI |
Bytes |
|---|---|---|---|
|
boolean |
unsigned char |
jboolean |
1 |
|
byte |
signed char |
jbyte |
1 |
|
char |
unsigned short |
jchar |
2 |
|
double |
double |
jdouble |
8 |
|
float |
float |
jfloat |
4 |
|
int |
int32 |
jint |
4 |
|
long |
int64 |
jlong |
8 |
|
short |
int16 |
jshort |
2 |
For nonprimitive types, the solution is not as simple. Passing objects by value is not a
JNIEnv
is the JNI environment that is basically a table of function pointers. It is passed to every native function as its first parameter. If a native function chooses to communicate to the Java side, it has to use this interface.
JNIEnv
contains more than 230 function pointers, and as
Struct { void *functionName1(JNIEnv *env) jint *functionName2(JNIEnv *env, ... ) jobject *functionName3(JNIEnv *env, ... ) ... }
In C,
env
is a pointer to another pointer that points to the function table. The reason for an extra level of indirection is that the JNIEnv not only points to a function table but also stores additional data local to the thread that is managed internally. You can simply dereference the double pointer and call one of its functions. The syntax is a bit more
// When using C compiler you call the functions as: (*env)->FunctionName(env, ...) // But when using C++ compiler you write: env->FunctionName(...)
The functions in the table can be broken down to different categories. Some deal with Java references, which are used to point to Java objects. Class functions are those that deal with instances of
Java.lang.Class
. Object functions are those that can be used for typical Java objects. Field and method functions are those that allow you to access the fields of an object or invoke its methods. Array functions deal with arrays, and string functions deal with instances of
java.lang.String
. Exception functions allow you to deal with Java exceptions from the native side. Direct ByteBuffer functions allow you to make arbitrary
In Java, objects are referred to by references, not direct pointers. Even though newer VMs use direct pointers internally, the pointers are nondirect from the developer’s view. A reference contains a direct pointer to an object in the Java heap. The following Java segment shows three references, two of which are of type ClassA , but the reference3 is of type java.lang.Object :
Object ReferenceTest(){ ClassA reference1 = new ClassA(); ClassA reference2 = reference1; Object reference3 = reference1; return reference1; }
Note that when this method returns,
reference1
,
reference2
, and
reference3
are
When manipulating Java objects from native code, we cannot use direct pointers. That is, you cannot simply use the memory address to manipulate a Java object. Instead, you must have some
class ClassA{ int x, y, z; } class ClassB{ String name; } Object ReferenceTest(){ ClassA referenceA = new ClassA(); ClassB referenceB = referenceA; return referenceB; }
When dealing with Java objects on the native side, however, all references are of type java.lang.Object , so you do not have as much compile-time type checking as you do in Java. When using a C compiler, all references to Java objects are simply of type jobject , which is the equivalent for java.lang.Object . However, some extra dummy types are defined when a C++ compiler is used so that some compile-time type checking can be performed for common types. Figure 11.1 shows the types defined when using a C++ compiler.
Figure 11.1:
JNI reference types.
Because the garbage collector collects objects that do not have a
Local references are used most commonly. Local references live only during the execution of a native function, just as typical Java references are destroyed when they go out of scope. In other words, when a native function returns, any local references created during its invocation are automatically released. This is convenient and more than often
JNIEXPORT void JNICALL Java_Sample_MyNativeFunction(JNIEnv *env, jclass clazz){ jobject reference2 = env->NewLocalRef(clazz); }
The local reference clazz , which is of type jclass , has already been created when the following function is called. In the function, another local reference of type jobject is created to refer to the object referred to by the clazz reference. Both references will automatically be deleted when this function returns.
Even though this behavior is appropriate for most cases, there are times when a reference should persist, even when the native function returns. For example, what if you want to store a reference in native code for use across calls? Alternatively, what if the object is created from the native side and only relevant to the native code?
Unlike local references, global references are not released when a native function returns. The only way to release them is to explicitly call the DeleteGlobalRef function. To create a global reference you must do so explicitly using the NewGlobalRef function. Unless explicitly released, global references can result in objects not getting collected during the lifetime of the VM. If you use a global reference you must make sure to call a corresponding delete at the appropriate time. Weak global references relax this idea a bit.
If you want to have a global reference but want the reference not to prevent the garbage collector from reclaiming the object, you can use a weak global reference. This type of reference is similar to the
java.lang.ref.WeakReference
object. This
Frames are used to manage the scope of local references. When a native function is called, a new frame is created and then destroyed when the function returns. If you want to manage local references yourself, you can explicitly push and pop
In Java, instances of
java.lang.Class
are used to represent classes and interfaces in a running Java application. The
java.lang.Class
class has no public constructor and is
ClassA.class
file and an object of type
ClassA
that is created when executing:
ClassA ref = new ClassA
Only one instance of java.lang.Class is created to represent ClassA during the life of the VM. On the other hand, many objects of type ClassA can be created using the new operator. When using JNI with a C compiler (as opposed to C++), you should be careful not to confuse them with typical Java objects because you will not have compile-time type checking. When using a C compiler, an instance of java.lang.Class is referred to by jobject . If using a C++ compiler, they are referred to be jclass .
To load a class file through JNI you can call the FindClass function. FindClass takes a descriptor that identifies the class you want to load. The descriptor is simply a string that represents the class name and its package.
jclass classMyClass = env->FindClass("MyClass");
Because arrays are special objects, to describe an array object the type of the array is preceded by “[”. Table 11.2 shows the descriptors for different types, and Table 11.3 shows how to describe primitive types.
|
java.lang.Object |
“java/lang/Object” |
|
Array of java.lang.Object |
“[java/lang/Object” |
|
Array of bytes |
“[B” |
|
2D array of Integers |
“[[I” |
|
boolean |
Z |
|
byte |
B |
|
char |
C |
|
double |
D |
|
float |
F |
|
int |
I |
|
long |
J |
|
short |
S |
You can also load a class by passing its raw bytes to DefineClass . The GetSuperClass function returns a java.lang.class reference to the super class of a class, and IsAssignableFrom determines if an object can be safely cast from one type to another. Two of the rarely needed but interesting calls are RegisterNatives and UnregisterNatives . They can be used to manually link a Java native method to a native function. This is the alternative to calling System.loadLibrary() and allowing the VM to link the functions to native methods based on the names of the functions. The following segment registers the functions function1 and function2 with the class referred to by clazz so that they correspond to methods createUnit and destroyUnit :
JNINativeMethod methods[2] = {0}; methods[0].name = "createUnit"; methods[0].signature = "(I)LUnit;"; methods[0].fnPtr = function1; methods[1].name = "destroyUnit"; methods[1].signature = "(LUnit;)V"; methods[1].fnPtr = function2; g_env->RegisterNatives(clazz, methods, nMethods);
These calls are
If you look through jni.h you will see that jobject is defined as _jobject pointer. jobject is really a pointer that points to a reference. Comparing two jobjects using the == operator can return false, even if they both refer to the same object. Doing so results in the references, as opposed to the objects referred to by the references, being compared. To compare two Java objects referred to from the native side, you should use the IsSameObject function. You can also use IsSameObject to test whether the object referred to by a weak global reference has been garbage collected. To do so, you can pass in NULL or as one of its parameters.
jobject reference1 = env->NewLocalRef(myObject); jobject reference2 = env->NewLocalRef(myObject); jobject reference3 = reference1; // this will evaluate to **false** if (reference1 == reference2){...} // this will evaluate to true if (env->IsSameObject(reference1, reference2)){...} // obviously this
evaluates
to true if (reference1 == reference3){...} if (env->IsSameObject(reference1, reference3)){...} // this will evaluate to true if the object referred // to by myWeakGlobalReferece is not live if (env->IsSameObject(myWeakGlobalReferece, NULL)){...}
There are two different ways to create objects from native code. The
NewObject
function creates an object in much the same way that the
new
operator does in Java. It can be used to allocate an object and call a constructor. If you want to create array objects, you should use
New
<
type
>
Array
instead. Unlike
NewObject
,
AllocObject
To access the fields of an object, we must first obtain a field identifier or jfieldID . The GetFieldID and GetStaticFieldID functions take a reference to a class, the field’s name, and a field descriptor and returns the field ID. The descriptor is simply the signature of the field, as previously presented in Table 11.3. References are described by L + class descriptor + ;. As with class descriptors, array fields are preceded with “[”. Table 11.4 provides several examples.
|
float |
“F” |
|
int[] |
“[I” |
|
Integer |
“Ljava/lang/Integer;” |
|
Object array |
“[Ljava/lang/Object;” |
Once you have a field ID, it is a good idea to cache it so you do not have to look it up every time you need to use it. After resolving the field IDs, you can use the set/get functions to access the fields of an object. Note that there are separate accessor functions for static fields. The functions are of the following form where < type > can be “Object,” “Boolean,” “Byte,” “Char,” “Short,” “Int,” “Long,” “Float,” or “Double.”
GetFieldID GetStaticFieldID Get<type>Field Set<type>Field SetStatic<type>Field GetStatic<type>Field
The following line looks up the ID of the field named health , which is of type integer and is a member of a java.lang.Class instance referred to by clazz .
jfieldID field = env->GetFieldID(clazz, "health", "I");
There are also two functions that allow you to convert field IDs into instances of java.lang.reflect.Field and vice versa. These functions are useful if you want to take advantage of reflection from the native side. The functions are FromReflectedField and ToReflectedField .
Calling functions through JNI is similar to accessing fields. To call methods we first must obtain a method identifier (jmethodID), and then invoke the method using one of the following:
Call<type>Method CallStatic<type>Method CallNonvirtual<type>Method
A method ID can be retrieved by using
GetMethodID
or
GetStaticMethodID
. These functions take a class reference, method name, and the method descriptor as parameter. A method descriptor or signature is
jmethodID method1 = env->GetMethodID(clazz, "run", "()V"); jmethodID method2 = env->GetMethodID(clazz, "<init>", "()V");
The previous segment retrieves the ID of the run method and the default constructor of the class referred to by clazz . Table 11.5 contains several examples of method descriptors.
|
void method(int a) |
“(I)V” |
|
boolean method(int a, byte b) |
“(IB)Z” |
|
char method(String s, int a) |
“(Ljava/lang/String;I)C” |
|
Object[] method() |
“()[Ljava/lang/Object;” |
Unlike C++, Java methods are virtual by default. That is, if class B extends class A and class B overwrites the foo method, if we call the foo method on an instance of class B that has been cast to A , the foo method in class B will be called. If you want to call the foo method in A through JNI, you should use the CallNonvirtual < type > Method family of functions.
Java arrays are unlike arrays in C. Java arrays are special objects. You can use the New < primitiveType > Array functions to create an array and GetArrayLength to get its length. Three sets of functions can be used to retrieve elements of primitive arrays, and it is crucial to understand their differences.
Get<primitiveType>ArrayRegion Get<primitiveType>ArrayElements GetPrimitiveArrayCritical
Get < primitiveType > ArrayRegion simply copies a region of the array into a provided buffer. On the other hand, Get < primitiveType > ArrayElements attempts to retrieve a direct pointer to the first element of the array, and if the attempt fails, it will return a pointer to a copy of the array. The attempt will fail if the array cannot be pinned or if the VM implementation does not store arrays as would typically be expected. For example, a VM implementation may not represent arrays as contiguous memory. A VM implementation may represent, for example, boolean arrays as a collection of bits. Because pinning an object so that the garbage collector does not move it may not be supported due to implementation complications, the VM may return a copy. In addition, even if pinning is supported by a VM, it may just happen that the call will result in too much fragmentation of the heap. The isCopy parameter informs you whether the returned pointer is a pointer to a copy of the array. Either way, you must call Release < primitiveType > ArrayElements when you no longer need the elements.
Unlike
Get
<
type
>
ArrayElements
,
GetPrimitiveArrayCritical
makes every effort to return a pointer to the body of the array. If possible, it will even temporarily disable the garbage collector. Every call to
GetPrimitiveArrayCritical
needs to be paired with
ReleasePrimitiveArrayCritical
. Be careful not to perform blocking operations or even call arbitrary JNI functions because such operations can result in a deadlock. In addition, disabling and enabling the garbage collector has some overhead that may undo the benefit of obtaining a direct pointer. Retrieving a direct pointer may sometimes be preferred because some CPU cycles do not have to be
Accessing arrays of objects are done using GetObjectArrayElement and SetObjectArrayElement . You can obtain a reference to the objects referred to by the array elements only one at a time.
JNI provides a set of functions specific for string manipulation. This may seem odd because
java.lang.String
objects are like any other Java objects, and it should not be necessary to have functions specific to a type. Strings have been treated differently because they are common to many applications. It is much more convenient and faster to call these functions instead of the equivalent calls that treat strings like typical objects. In Java, strings are Unicode strings, but in C they are ASCII, so you have to be more careful when dealing with strings. JNI provides functions that deal with UTF-8 and not specifically ASCII. Even though UTF-8 strings can contain multibyte characters, a UTF-8 string that uses only single-byte
Java_Sample_MyNativeFunction(JNIEnv *env, jclass clazz, jstring string){ jsize stringLength = env->GetStringLength(string); char *buffer = (char *)
calloc
(stringLength+1, 1); env->GetStringUTFRegion(string, 0, stringLength, buffer); printf("buffer: %s", buffer); free(buffer); }
GetStringChars and GetStringUTFChars work similar to Get < primitiveType > ArrayElements . Finally, GetStringCritical is similar to GetPrimitiveArrayCritical . The isCopy parameter of these functions lets you know if the call results in the creation of a copy. Regardless, be sure to call the corresponding Release functions.
In Java, instances of the java.lang.String class are constant. In other words, once created, they cannot be changed (unlike java.lang.StringBuffer ). By being immutable, the VM can share string objects. This means that you should not be surprised to find out that they return constant pointers and that there are no SetStringChars functions.
The Java programming language specifies that an exception will be thrown when semantic constraints are violated, in which case it causes a
class Sample{ Sample(){ System.loadLibrary("MyNativeLibrary"); } private native void myNativeFunction(); public void myMethod() throws NullPointerException{ throw new NullPointerException("Sample.myMethod..."); } public static void main(String args[]) { Sample sample = new Sample(); sample.myNativeFunction(); } } #include "Sample.h" #include <stdio.h: JNIEXPORT void JNICALL Java_Sample_myNativeFunction(JNIEnv *env, jobject obj){ jclass clazz = env->GetObjectClass(obj); jmethodID mid = env->GetMethodID(clazz, "myMethod", "()V"); env->CallVoidMethod(obj, mid); if (env->ExceptionCheck()){ printf("An Exception has occurred\n"); env->ExceptionDescribe(); printf("clearing Exception\n"); env->ExceptionClear(); } }
The sample code
To check for pending exceptions you can use
ExceptionCheck
or
ExceptionOccurred
. The former is more efficient and useful when you simply want to know if an exception has been thrown.
ExceptionOccurred
returns a reference to the exception. Once you handle an exception in native code you should clear it by calling
ExceptionClear
. Because native code has to check for exception to transfer control, failing to clear a handled exception can result in unexpected behavior. This holds true for JNI functions
Native code can also throw an exception by calling Throw or ThrowNew . You can declare a native method with the throws keyword so that catching of the exceptions thrown from the native code is enforced at compile time. JNI function may throw exceptions themselves. Some JNI function returns success or failure that can be used as a more efficient way of error checking.
Direct buffers were introduced in JDK 1.4 as part of
java.nio
. They provide the means to make data visible to both native and Java code by directly exposing the data to both sides. Before going any further, it is important to know the difference between the Java heap and the system memory. As shown in Figure 11.2, we will define the Java heap as the memory
Figure 11.2:
Java heap and system memory.
Because the garbage collector moves the data in the Java heap when performing housekeeping, direct access to the data cannot be possible unless either the VM supports pinning of objects or the garbage collector can be disabled temporarily. As discussed earlier, JNI functions such as GetStringChars and GetPrimitiveArrayCritical attempt to pin objects or disable the garbage collector to avoid having to copy the data. However, even if a VM implementation supports these techniques, they can impose serious overhead and limitation.
If native code cannot manipulate data in the Java heap, the data must be copied from the Java heap to system memory, manipulated, and then copied back. Copying data back and forth can result in substantial performance loss for sizeable amounts of data. Passing primitive types or even accessing object data through JNI means that data is copied. Direct buffers, on the other hand, allow both Java and native code to access a buffer of data without
Figure 11.3:
Java ByteBuffer object that contains a pointer to a native buffer.
Let’s consider a scenario where we need to load an image, modify it dynamically, and then draw it in a window. From the native side, we can load the image into the system memory and then use a direct ByteBuffer to wrap the loaded image. Once we have a direct buffer reference that points to the image data as its buffer, we can pass it to the Java side so that the image can be manipulated from the Java side. Because the image resides in the system memory, if the direct ByteBuffer is passed to a native draw function, the address of the image can be obtained from the ByteBuffer and used to draw the image. Without the use of a direct buffer, we would run into a few problems. First, we would have to decide where to store the loaded image. If stored on the Java side, after modifying the image, it would have to be copied to system memory before being passed to a draw function. Again, this is because the garbage collector can move an object when performing housekeeping. If the image is stored in system memory, it would not be possible to directly modify the image data from the Java side unless a direct ByteBuffer object wraps the content of the image. In fact, this is the reason why it is necessary to use an instance of java.awt.bufferedImage to access the data of an AWT image. A buffered image is essentially an object that is used to indirectly manipulate the image data stored by default in the system memory. If direct buffers were available when AWT was designed, the solution would probably be different. The following segment shows how to create a direct buffer that wraps an arbitrary region of the system memory:
jobject jDirectByteBuffer = env->NewDirectByteBuffer(startAddress, length);
Note that from the Java side you cannot set a direct ByteBuffer’s buffer address. If this were not the case, Java would lose its
Other buffer-
The two handlers,
JNI_OnLoad
and
JNI_OnUnload
, allow you to perform some initialization and
JNI also includes an invocation interface that can be used to launch the VM from a native program. By using the functions provided by this interface, you could create and destroy a VM. In fact, this is how
Java.exe
launches the VM. The invocation interface is used to launch the VM from C/C++ code, which is also known as embedding the VM. If you want to use Java as a scripting language for a C/C++ game, you must use this interface. Note that Java is not exactly a scripting language. For more information
The AttachCurrentThread function can be used to let the VM know that you want to allow the current native thread to communicate with the VM. This step is important because the VM must give each thread a pointer to a different JNI environment instance.
As the following segment shows, launching a VM is pretty straightforward. The only requirements are that the project links to jvm.lib and the directory of jvm.dll be visible. Note that you cannot copy the jvm.dll to your directory because it uses its relative location to find other files.
JavaVM* g_jvm = 0; JNIEnv* g_env = 0; int StartVM(){ JavaVMInitArgs jvmArgs; jvmArgs.version = JNI_VERSION_1_4; jvmArgs.ignoreUnrecognized = JNI_TRUE; result = JNI_CreateJavaVM(&g_jvm, (void**)&g_env, &jvmArgs ); if (result){ printf("Error: JNI_CreateJavaVM failed"); return -1; } }
The following code segment destroys the VM, but note that because the
jvm.dll
uses internal global variables, after destroying the VM, a VM cannot be created in the same process again. This was not the original
void StopVM(){ if (g_env){ if (g_env->ExceptionOccurred()) { g_env->ExceptionDescribe(); } } if (g_jvm) { jint result = g_jvm->DestroyJavaVM(); if(result){ printf("Error: g_jvm->DestroyJavaVM() FAILED\n"); } } }
If you want to find out if a VM exists, you can call the following:
JavaVM* createdVms[1] = {0}; jsize numberOfVMs; JNI_GetCreatedJavaVMs(createdVms, 1, &numberOfVMs); if (numberOfVMs > 0) { g_jvm = createdVms[0]; g_jvm->GetEnv((void**)&g_env, JNI_VERSION_1_4); }