The main design issues in JNI are discussed here. Most of the design issues in this section relate to the local approach.
JNI interface functions and Pointers
Native code accesses JVM features by calling JNI functions. JNI functions are available through interface Pointers. An interface pointer is a pointer to a pointer. This pointer points to an array of Pointers, each pointing to an interface function. Each interface function has a predefined offset in the array. Figure 2-1 illustrates the organization of interface Pointers.
JNI interfaces are organized similarly to C++ virtual function tables or COM interfaces. The advantage of using interface tables instead of hard-wired function entries is that the JNI namespace is separated from the native code. The VM can easily provide multiple versions of the JNI function table. For example, a VM can support two JNI function tables:
- Perform thorough illegal parameter check, suitable for debugging;
- The other is the minimum number of checks required to execute the JNI specification and is therefore more efficient.
JNI interface Pointers are valid only in the current thread. Therefore, local methods cannot pass interface Pointers from one thread to another. VMS that implement JNI can allocate and store thread-local data in areas that JNI interface Pointers point to.
The local method receives a JNI interface pointer as an argument. When the VM calls the local method multiple times from the same Java thread, it guarantees that the same interface pointer is passed to the local method. However, local methods can be called from different Java threads, so different JNI interface Pointers can be received.
Compile, load, and link local methods
Since the JVM is multithreaded, you should also compile the local libraries and link them using a native compiler that supports multithreading. For example, the -mt flag should be used for C++ code compiled with the Sun Studio compiler. For code that follows the GNU GCC compiler, the -d_reentrant or -d_posix_c_source flags should be used. Refer to the local compiler documentation for more information.
All local methods are loaded by the system.loadLibrary method. In the following example, the class initialization method loads a platform-specific local library that defines the local method F:
package pkg; class Cls { native double f(int i, String s); The static {System. LoadLibrary (" pkg_Cls "); }}Copy the code
The parameter to system. loadLibrary is a library name chosen arbitrarily by the programmer. The system follows a standard, but platform-specific, method of converting library names to local library names. For example, the Solaris system converts the name pkg_Cls to libpkg_cls.so. Convert the same pkg_Cls name to pkG_cls.dll when the system is Win32.
A programmer can use a library to store all the native methods needed by any number of classes, as long as the classes are loaded using the same classloader. The VM internally maintains a list of loaded local libraries for each class loader. The VM vendor should choose a local library name that minimizes name conflicts.
If the underlying operating system does not support dynamic linking, all native methods must be pre-linked to the VM. In this case, the VM completes the System.loadLibrary call without actually loading the library.
Programmers can also call the JNI function RegisterNatives() to register local methods associated with the class. The RegisterNatives() function is particularly useful for statically linked functions.
Parse the local method name
The dynamic linker resolves entries based on their name. Local method names are concatenated from the following components:
- The prefix Java_
- A confused fully qualified class name
- Underscore (” _ “) separator
- A messy method name
- For overloaded local methods, two underscores (” __ “) are followed by messy parameter signatures
The VM checks whether the method name matches the method residing in the local library. VM first looks for the short name; That is, no parameter signature name. It then looks for the long name with the parameter signature. Programmers need to use long names only if a local method is overloaded by another local method. However, if a local method has the same name as a non-local method, this is not a problem. Non-native methods (Java methods) do not reside in local libraries.
In the following example, there is no need to link the local method G with a long name because the other method G is not local and therefore not in the local library.
class Cls1 {
int g(int i);
native int g(double d);
}
Copy the code
We use a simple name-obfuscation mode to ensure that all Unicode characters are converted to valid C function names. In fully qualified class names, we use the underscore (” _ “) character instead of the slash (“/”). Since name or type descriptors never start with a number, we can use _0,… , _9 represents the escape sequence, as shown in the following table:
Escape sequences | said |
---|---|
_0XXXX | A Unicode character XXXX. Note the use of lowercase letters to represent non-ASCII Unicode characters, for example,_0abcd vs _0abcd. |
_1 | Characters “_” |
_2 | The character “; “in the signature. |
_3 | The character [in the signature |
Both the local methods and the interface apis follow the standard library calling conventions on a given platform. For example, UNIX systems use the C calling convention, while Win32 systems use __stdCall.
Local method parameters
The JNI interface pointer is the first argument to the local method. The JNI interface pointer is of type JNIEnv. The second argument depends on whether the local method is static or non-static, and the second argument to a non-static local method is a reference to an object. The second argument to a static local method is a reference to its Java Class. The remaining parameters correspond to regular Java method parameters. A local method call passes the result back to the calling routine by returning a value. The following sections describe the mapping between Java and C types.
The code example demonstrates how to implement the native method f using C functions. The local method f is declared as follows:
package pkg; class Cls { native double f(int i, String s); . }Copy the code
The long name of the C function is Java_pkg_Cls_f_ILjava_lang_String_2, which implements the local method f:
Implement native methods using C
jdouble Java_pkg_Cls_f__ILjava_lang_String_2 (
JNIEnv *env, /* interface pointer */
jobject obj, /* "this" pointer */
jint i, /* argument #1 */
jstring s) /* argument #2 */
{
/* Obtain a C-copy of the Java string */
const char *str = (*env)->GetStringUTFChars(env, s, 0);
/* process the string */
...
/* Now we are done with str */
(*env)->ReleaseStringUTFChars(env, s, str);
return ...
}
Copy the code
Note that we always use the interface pointer ENV to manipulate Java objects. With C++, you can write a slightly cleaner version of the code, as shown in the following code example:
Implement native methods using C++
extern "C" /* specify the C calling convention */ jdouble Java_pkg_Cls_f__ILjava_lang_String_2 ( JNIEnv *env, /* interface pointer */ jobject obj, /* "this" pointer */ jint i, /* argument #1 */ jstring s) /* argument #2 */ { const char *str = env->GetStringUTFChars(s, 0); . env->ReleaseStringUTFChars(s, str); return ... }Copy the code
In C++, additional indirection layers and interface pointer parameters are eliminated from the source code. However, the underlying mechanism is exactly the same as C. In C++, JNI functions are defined as inline member functions that can be extended to their C counterparts.
5. Reference Java objects
Copy basic types like integers, characters, and so on between Java and native code. Any Java object, on the other hand, is passed by reference. The VM must keep track of all objects passed to the native code so that the garbage collector does not release them. In turn, the native code must have a way to tell the VM that it no longer needs objects. In addition, the garbage collector must be able to move objects referenced by native code.
5.1 Global and Local References
JNI divides object references used by native code into two categories: local and global. Local references are valid for the duration of a local method call and are released automatically when the native method returns. Global references remain valid until they are explicitly released.
Object is passed as a local reference to the local method. All Java objects returned by JNI functions are local references. JNI allows programmers to create global references from local references. JNI functions expect Java objects to accept global and local references. Local methods can return a local or global reference to the VM as a result.
In most cases, programmers should rely on the VM to release all local references when the local method returns. Sometimes, however, programmers should explicitly release local references. For example, consider the following situation:
- Local methods access large Java objects to create local references to that Java object. The local method then performs additional calculations before returning to the caller. A local reference to a large Java object will prevent the object from being garbage collected, even if the object is no longer used in the rest of the calculation.
- The local method creates a large number of local references, although not all references are used at the same time. Since the VM needs some space to keep track of local references, creating too many local references can cause the system to run out of memory. For example, local methods iterate over a large number of objects, retrieving elements that are locally referenced, and manipulating one element per iteration. After each iteration, the programmer no longer needs local references to array elements.
JNI allows programmers to manually remove local references anywhere in a local method. To ensure that programmers can manually release local references, JNI functions do not allow the creation of additional local references unless they return references as a result.
Local references are valid only in the thread in which they were created. Native code cannot pass local references from one thread to another.
5.2 Implement local Reference
To implement local references, the JVM creates a registry for each control transformation from Java to a local method. The registry maps non-removable local references to Java objects and prevents objects from being garbage collected. All Java objects passed to local methods, including those returned as a result of JNI function calls, are automatically added to the registry. Delete the registry after the local method returns, allowing all of its entries to be garbage collected.
There are different ways to implement registries, such as using tables, linked lists, or hash tables. While reference counting can be used to avoid duplicates in the registry, the JNI implementation is not obligated to detect and collapse duplicates.
Note that scanning the local stack conservatively does not faithfully implement local references. Local code can store local references in global or heap data structures.
Accessing Java objects
JNI provides a rich set of accessor functions for global and local references. This means that the same native method implementation will work regardless of how Java objects are represented inside the VM. This is a key reason why VARIOUS VM implementations support JNI.
Using accessor functions through opaque references is more expensive than accessing C data structures directly. We believe that in most cases, Java programmers use native methods to perform important tasks that mask the overhead of this interface.
6.1 Accessing raw Arrays
For large Java objects that contain many basic data types, such as integer arrays and strings, this overhead is unacceptable (consider the native methods used to perform vector and matrix calculations). Walking through a Java array and using function calls to retrieve each element is very inefficient.
One solution introduces the concept of “fixation” so that local methods can require a VM to fix the contents of an array. The local method then receives a direct pointer to the element. However, this approach has two implications:
- The garbage collector must support fixation.
- The VM must continuously lay out primitive arrays in memory. Although this is the most natural implementation of most basic arrays, Boolean arrays can be implemented as packaged or unpackaged. Therefore, native code that relies on the exact layout of a Boolean array is not portable.
We have overcome both problems by adopting a compromise.
First, we provide a set of functions to copy the original array elements between segments of a Java array and the local memory buffer. These functions are used if the local method only needs to access a small number of elements in a large array.
Second, a programmer can use another set of functions to retrieve a compressed version of an array element. Keep in mind that these functions may require the JVM to perform storage allocation and replication. Whether these functions actually copy the array depends on the VM implementation, as shown below:
- If the garbage collector supports pinning, and the array layout is the same as expected from the local method, no copying is required.
- Otherwise, the array is copied to an immovable block of memory (for example, in the C heap) and the necessary format conversion is performed. Returns a pointer to the copy.
Finally, the interface provides functions to inform the VM that local code no longer needs to access array elements. When you call these functions, the system either unzips the array or reconciles the original array with its immovable copy and releases the copy.
Our approach provides flexibility. For each given array, the garbage collector algorithm can individually decide whether to copy or fix it. For example, the garbage collector can copy small objects but lock large ones.
JNI implementations must ensure that local methods running in multiple threads can access the same array at the same time. For example, JNI can keep an internal counter for each fixed array so that one thread does not unpin an array that is also fixed by another thread. Note that JNI does not need to lock primitive arrays exclusively accessed by native methods. Updating Java arrays from different threads at the same time results in inconclusive results.
6.2 Accessing Fields and methods
JNI allows native code to access fields and call methods on Java objects. JNI identifies methods and fields by their symbolic names and types. Two steps extract the name and signature of a field or method to locate the field or method. For example, to call method F in the CLS class, the native code first gets a method ID like this:
JmethodID mid = env-> methodid (CLS, "f", "(ILjava/lang/String;) D ");Copy the code
Native code can then reuse the method ID, as shown below:
jdouble result = env->CallDoubleMethod(obj, mid, 10, str);
Copy the code
A field or method ID does not prevent the VM from unloading the class derived from that ID. Invalid method or field ID after unloading class. Therefore, native code must ensure that:
- Keep real-time references to the underlying classes, or
- Recalculate the method or field ID
If it intends to use a method or field ID over a longer period of time.
JNI does not impose any restrictions on how field and method ids are implemented internally.
7. Programming error report
JNI does not check for programming errors, such as passing NULL Pointers or invalid parameter types. Illegal parameter types include using plain Java objects instead of Java Class objects. JNI does not check for these programming errors for the following reasons:
- Forcing JNI functions to check for all possible error conditions degrades the performance of normal (correct) native methods.
- In many cases, there is not enough runtime type information to perform such checks.
Most C library functions do not protect against programming errors. For example, the printf() function usually causes a runtime error when it receives an invalid address, rather than returning an error code. Forcing C library functions to check for all possible error conditions can result in repeating such checks — once in user code, and again in the library.
Programmers must not pass illegal Pointers or arguments of the wrong type to JNI functions. Doing so can lead to arbitrary consequences, including corrupted system state or VM crashes.
Java exception
JNI allows native methods to throw arbitrary Java exceptions. Native code can also handle unhandled Java exceptions. Unhandled Java exceptions are propagated back to the VM.
8.1 Exception and Error Codes
Some JNI functions use the Java exception mechanism to report error conditions. In most cases, JNI functions report error conditions by returning error code and throwing Java exceptions. The error code is usually a special return value (such as NULL) that is outside the normal return value range. Therefore, programmers can:
- Quickly check the return value of the previous JNI call to determine if an error occurred, and
- A function, ExceptionOccurred(), is called to get an exception object with a more detailed description of the error condition.
There are two cases in which the programmer needs to check for exceptions without first checking the error code:
- The JNI function that calls a Java method returns the result of the Java method. The programmer must call ExceptionOccurred() to check for exceptions that may occur during the execution of a Java method.
- Certain JNI array access function does not return an error code, but an ArrayIndexOutOfBoundsException may be thrown or ArrayStoreException.
In all other cases, a non-error return value ensures that no exceptions are thrown.
8.2 Asynchronous Exceptions
In the case of multithreading, threads other than the current thread may issue asynchronous exceptions. Asynchronous exceptions do not immediately affect the execution of native code in the current thread until:
- The local code calls one of the JNI functions that may raise a synchronization exception, or
- The native code explicitly checks for synchronous and asynchronous exceptions using ExceptionOccurred().
Note that only those JNI functions that might raise synchronous exceptions check for asynchronous exceptions.
Local methods should insert ExceptionOccurred() checks where necessary (such as in tight loops with no other exception checks) to ensure that the current thread responds to asynchronous exceptions within a reasonable amount of time.
8.3 Exception Handling
There are two ways to handle exceptions in native code:
- Local methods can choose to return immediately, causing an exception to be thrown in the Java code that initiates the local method call.
- Native code can clear the exception by calling ExceptionClear() and then execute its own exception-handling code.
After an exception is thrown, the native code must first clear the exception before making any other JNI calls. The JNI functions that can be safely called when there is a pending exception are:
ExceptionOccurred()
ExceptionDescribe()
ExceptionClear()
ExceptionCheck()
ReleaseStringChars()
ReleaseStringUTFChars()
ReleaseStringCritical()
Release<Type>ArrayElements()
ReleasePrimitiveArrayCritical()
DeleteLocalRef()
DeleteGlobalRef()
DeleteWeakGlobalRef()
MonitorExit()
PushLocalFrame()
PopLocalFrame()
Copy the code