Java Native Interface - the Essentials
From The Java™ Native Interface - Programmer’s Guide and Specification by Sheng Liang. Copy the essentials only…
The Java™ Native Interface (JNI) is a powerful feature of the Java platform. Applications that use the JNI can incorporate native code written in programming languages such as C and C++, as well as code written in the Java programming language.
Overview
Design Goals
The most important goal of the JNI design is ensuring that it offers binary compatibility among different Java virtual machine implementations on a given host environment. The term host environment represents the host operating system, a set of native libraries, and the CPU instruction set. To achieve this goal, the JNI design cannot make any assumptions about the internal details of the Java virtual machine implementation.
The second goal of JNI design is efficiency. To support time-critical code, the JNI imposes as little overhead as possible.
Lastly, the JNI must be functionally complete. It must expose enough Java virtual machine functionality to enable native methods and applications to accomplish useful tasks.
Loading Native Libraries
Native libraries are located by class loaders. Class loaders have many uses in the Java virtual machine including, for example, loading class files, defining classes and interfaces, providing namespace separation among software components, resolving symbolic references among different classes and interfaces, and finally, locating native libraries.
Class loaders provide the namespace separation needed to run multiple components (such as the applets downloaded from different web sites) inside an instance of the same virtual machine. Each class or interface type is associated with its defining loader, the loader that initially reads the class file and defines the class or interface object. Two class or interface types are the same only when they have the same name and the same defining loader. For example, in Figure 11.1, class loaders L1 and L2 each define a class named C. These two classes named C are not the same.
The dotted lines in the above figure represent the delegation relationships among class loaders. A class loader may ask another class loader to load a class or an interface on its behalf. Delegation allows system classes to be shared among all class loaders. This is necessary, otherwise type safety would be violated.
System.loadLibrar
completes silently if an earlier call to System.loadLibrary
has already loaded the same native library. The virtual machine internally maintains a list of loaded native libraries for each class loader. In Java 1.2, ClassLoader.findLibrary
allows the programmer to specify a custom library loading policy that is specific to a given class loader. System.mapLibraryName
maps platform-independent library names to platformdependent library file names.
The virtual machine does not allow a given JNI native library to be loaded by more than one class loader. The purpose of this restriction is to make sure that namespace separation based on class loaders is preserved in native libraries.
Android NDK libraries can share across class loaders, seem break this specification. While, namespace based dynamic linking enhance restriction for some perspective – library can be loaded among different class loaders, but they cannot share resource directly.
Linking Native Methods
Linking a native method involves the following steps:
- Determining the class loader of the class that defines the native method.
- Searching the set of native libraries associated with this class loader to locate the native function that implements the native method.
- Setting up the internal data structures so that all future calls to the native method will jump directly to the native function.
The virtual machine deduces the name of the native function from the name of the native method. For each native method, the virtual machine looks first for the short name, that is, the name without the argument descriptor. It then looks for the long name, which is the name with the argument descriptor.
Calling Conventions
The calling convention determines how a native function receives arguments and returns results. There is no standard calling convention among various native languages, or among different implementations of the same language. The JNI requires the native methods to be written in a specified standard calling convention on a given host environment.
Other Concepts
Native applications are written in native programming languages such as C and C++, compiled into host-specific binary code, and linked with native libraries.
Native methods that allow Java applications to call functions implemented in native libraries.
Invocation interface that allows you to embed a Java virtual machine implementation into native applications.
There are many ways for Java applications to interoperate with code written in other languages, such as TCP/IP, IPC, JDBC API and Java IDL API. A common characteristic of these alternative solutions is that the Java application and native code reside in different processes (and in some cases on different machines).
The Java virtual machine automatically runs the static initializer before invoking any methods in the HelloWorld
class, thus ensuring that the native library is loaded before the print native method is called.
It is important to set your native library path correctly for your program to run. If you are running on a Solaris system, the LD_LIBRARY_PATH
environment variable is used to define the native library path.
Types
The JNI defines a set of C/C++ types that correspond to the primitive and reference types in the Java programming language.
All primitive types in the JNI have well-defined sizes.
JNI reference types are organized in the hierarchy shown below.
Concept
The JNIEXPORT
and JNICALL
macros (defined in the jni.h
header file) ensure that this function is exported from the native library and C compilers generate code with the correct calling convention for this function.
The first parameter JNIEnv
interface pointer, points to a location that contains a pointer to a function table. Each entry in the function table points to a JNI function. Native methods always access data structures in the Java virtual machine through one of the JNI functions. As Figure 3.1. The second argument to an instance native method is a reference to the object on which the method is invoked, similar to the this pointer in C++. The second argument to a static native method is a reference to the class in which the method is defined.
There are two kinds of types in the Java programming language: primitive types such as int
, float
, and char
, and reference types such as classes, instances, and arrays.
The JNI passes objects to native methods as opaque references. Opaque references are C pointer types that refer to internal data structures in the Java virtual machine. The exact layout of the internal data structures, however, is hidden from the programmer. The native code must manipulate the underlying objects via the appropriate JNI functions, which are available through the JNIEnv
interface pointer.
Strings
Failure to call ReleaseStringUTFChars
would result in a memory leak.
In a typical Java virtual machine implementation, the garbage collector relocates objects in the heap. Once a direct pointer to a java.lang.String
instance is passed back to the native code, the garbage collector can no longer relocate the java.lang.String
instance. To put it another way, the virtual machine must pin the java.lang.String instance
. Because excessive pinning leads to memory fragmentation, the virtual machine implementation may, at its discretion, decide to either copy the characters or pin the instance for each individual GetStringChars
call.
Native code between a Get/ReleaseStringCritical
pair must not issue blocking calls or allocate new objects in the Java virtual machine. Otherwise, the virtual machine may deadlock. The Get/ReleaseStringCritical
pairs need NOT be strictly nested in a stack order.
Array
The JNI treats primitive arrays and object arrays differently. Primitive arrays contain elements that are of primitive types such as int and boolean. Object arrays contain elements that are of reference types such as class instances and other arrays.
Accessing primitive arrays in a native method requires the use of JNI functions similar to those used for accessing strings.
The JNI provides a separate pair of functions to access objects arrays. GetObjectArrayElement
returns the element at a given index, whereas SetObjectArrayElement
updates the element at a given index. Next the NewObjectArray
function allocates an array whose element type is denoted by the intArrCls
class reference.
Inside a critical region the native code should not run for an indefinite period of time, must not invoke arbitrary JNI functions, and must not perform operations that might cause the current thread to block and wait for another thread in the virtual machine. A JNI implementation must ensure that native methods running in multiple threads can simultaneously access the same array.
Fields and Methods
The JNI allows native code to access fields and to call methods defined in the Java programming language. The JNI identifies methods and fields by their symbolic names and type descriptors. A field or method ID remains valid until the virtual machine unloads the class or interface that defines the corresponding field or method.
The JNI implementation must derive the same field or method ID for a given name and descriptor from two classes or interfaces if the same field or method definition is resolved from these two classes or interfaces. JNI does not impose any restrictions on how field and method IDs are implemented internally.
It is legal, to have overloaded fields in a class file, and to run such class files on Java virtual machines.
Fields
Each instance of a class has its own copy of the instance fields of the class, whereas all instances of a class share the static fields of the class. The JNI provides functions that native code can use to get and set instance fields in objects and static fields in classes.
To access an instance field, the native method follows a two-step process. GetFieldID
–> Get/SetObjectField
. C string "Ljava/lang/String;"
represents a field type in Java is JNI field descriptors. For static fields, use ` GetStaticFieldID`, and pass class reference rather than object reference to access field.
Methods
There are several kinds of methods in the Java programming language. Instance methods must be invoked on a specific instance of a class, whereas static methods may be invoked independent of any instance.
Two steps to call instance method: GetMethodID
–> Call<Type>Method
. Similarly, call static methods via GetStaticMethodID
–> CallStatic<Type>Method
. The former takes a class reference as the second argument, whereas the latter takes an object reference as the second argument.
At the Java programming language level, you can invoke a static method f
in class Cls
using two alternative syntaxes: either Cls.f
or obj.f
where obj
is an instance of Cls
. (The latter is the recommended programming style, however.) In the JNI, you MUST always specify the class reference when issuing static method calls from native code.
The JNI uses descriptor strings to denote method types in a way similar to how it denotes field types. A method descriptor combines the argument types and the return type of a method.
You can call instance methods which were defined in a superclass but that have been overridden in the class to which the object belongs by CallNonvirtual<Type>Method
.
Caching ID
There are two ways to cache field and method IDs, depending upon whether caching is performed at the point of use of the field or method ID, or in the static initializer of the class that defines the field or method.
In many situations it is more convenient to initialize the field and method IDs required by a native method before the application can have a chance to invoke the native method.
Caching IDs at the point of use is the reasonable solution if the JNI programmer does not have control over the source of the class that defines the field or method.
Performance
A typical virtual machine may execute a Java/native call roughly two to three times slower than it executes a Java/Java call.
The performance characteristics of a native/Java callback is technically similar to a Java/native call.
The overhead of field access using the JNI lies in the cost of calling through the JNIEnv
. Rather than directly dereferencing objects, the native code has to perform a C function call which in turn dereferences the object. The function call is necessary because it isolates the native code from the internal object representation maintained by the virtual machine implementation. The JNI field access overhead is typically negligible because a function call takes only a few cycles.
Local and Global References
Overview
Primitive data types, such as integers, characters, and so on, are copied between the Java virtual machine and native code. Objects, on the other hand, are passed by reference. Passing references, instead of direct pointers to objects, enables the virtual machine to manage objects in more flexible ways.
The JNI supports three kinds of opaque references: local references, global references, and weak global references.
Local and global references have different lifetimes. Local references are automatically freed, whereas global and weak global references remain valid until they are freed by the programmer. Objects are passed to native methods as local references. Most JNI functions return local references.
A local or global reference keeps the referenced object from being garbage collected. A weak global reference, on the other hand, allows the referenced object to be garbage collected.
Not all references can be used in all contexts. It is illegal, for example, to use a local reference after the native method that created the reference returns.
Managing JNI references properly is crucial to writing reliable and space-efficient code.
Local and Global
Local
A local reference is valid only within the dynamic context of the native method that creates it, and only within that one invocation of the native method.
There are two ways to invalidate a local reference: VM automatically frees all local references created during the execution of a native method after the native method returns; programmers explicitly manage the lifetime of local references using JNI functions such as DeleteLocalRef
.
A local reference may be passed through multiple native functions before it is destroyed. Local references are also only valid in the thread that creates them.
To implement local references, the Java virtual machine creates a registry for each transition of control from the virtual machine to a native method. A registry maps non-movable local references to object pointers. Objects in the registery cannot be garbage collected. All objects passed to the native method, including those that are returned as the results of JNI function calls, are automatically added to the registry. The registry is deleted after the native method returns, allowing its entries to be garbage collected.
There are different ways to implement a registry, such as using a stack, a table, a linked list, or a hash table. Although reference counting may be used to avoid duplicated entries in the registry, a JNI implementation is not obliged to detect and collapse duplicate entries.
Global
You can use a global reference across multiple invocations of a native method. A global reference can be used across multiple threads and remains valid until it is freed by the programmer.
Global references are created by just one JNI function, NewGlobalRef
.
Weak Global
Weak global references are new in Java 2 SDK release 1.2. They are created using NewGlobalWeakRef
and freed using DeleteGlobalWeakRef
. Like global references, weak global references remain valid across native method calls and across different threads. Unlike global references, weak global references do not keep the underlying object from being garbage collected.
java.lang.String
is a system class and will never be garbage collected.
Given two local, global, or weak global references, you can check whether they refer to the same object using the IsSameObject
function. A NULL
reference in JNI refers to the null
object in the Java virtual machine. You can use IsSameObject
to determine whether a non-NULL
weak global reference still points to a live object.
Freeing References
There are times when you, the JNI programmer, should explicitly free local references: create a large number of local references in a single native method invocation; write a utility function that is called from unknown contexts; your native method does not return at all; your native method accesses a large object, thereby creating a local reference to the object.
The JNI specification dictates that the virtual machine automatically ensures that each native method can create at least 16 local references. EnsureLocalCapacity
call to make sure that space for a sufficient number of local references is available.
Alternatively, the Push/PopLocalFrame
functions allow programmers to create nested scopes of local references. Failing to place PopLocalFrame
calls properly would lead to undefined behavior, such as virtual machine crashes.
There is no guarantee, however, that memory will be available. The virtual machine exits if it fails to allocate the memory.
You should call DeleteWeakGlobalRef
when your native code no longer needs access to a weak global reference. If you fail to call this function the Java virtual machine will still be able to garbage collect the underlying object, but will not be able to reclaim the memory consumed by the weak global reference itself.
Errors and Exceptions
Programmer errors are caused by misuses of JNI functions. Java virtual machine exceptions are raised, for example, by out-of-memory situations that occur when native code tries to allocate an object through the JNI.
Exceptions
Native method can check/print/clear/throw exception via ExceptionOccurred
/ ExceptionDescribe
/ ExceptionClear
/ ThrowNew
.
A pending exception raised through the JNI (by calling ThrowNew
, for example) does not immediately disrupt the native method execution. JNI programmers must explicitly implement the control flow after an exception has occurred.
It is extremely important to check, handle, and clear a pending exception before calling any subsequent JNI functions. Calling most JNI functions with a pending exception leads to undefined results. The following is the complete list of JNI functions that can be called safely when there is a pending exception:
ExceptionOccurred
ExceptionDescribe
ExceptionClear
ExceptionCheck
ReleaseStringChars
ReleaseStringUTFchars
ReleaseStringCritical
Release<Type>ArrayElements
ReleasePrimitiveArrayCritical
DeleteLocalRef
DeleteGlobalRef
DeleteWeakGlobalRef
MonitorExit
Proper Exception Handling
If a JNI function returns its error value, a subsequent ExceptionCheck
call in the current
thread is guaranteed to return JNI_TRUE
.
About the description——>
- how could JVM knows the “error return value” of a JNI call???? NULL?
- If JVM thinks JNI is error and has exception, the exception should be handled by Java. Then, how could a
ExceptionCheck
return TRUE???Shall be the assumption:
- JNI function throws exception when return error.
- The exception is acceptable(no need to catch); Java doesn’t catch the exception.
It is extremely important to check, handle, and clear a pending exception before calling any subsequent JNI functions. Native code may handle a pending exception by: return immediately - let caller handle; clear the exception - handle by itself.
When there is a pending exception you can call the JNI functions that are designed to handle exceptions and the JNI functions that release various virtual machine resources exposed through the JNI.
Programmers writing utility functions should pay special attention to ensure that exceptions are propagated to the caller native method.
The Invocation Interface
A Java virtual machine implementation is typically shipped as a native library. Native applications can link against this library and use the invocation interface to load the Java virtual machine. Indeed, the standard launcher command (java) in JDK or Java 2 SDK releases is no more than a simple C program linked with the Java virtual machine. The launcher parses the command line arguments, loads the virtual machine, and runs Java applications through the invocation interface.
Creating JVM
When the JNI_CreateJavaVM
function returns successfully, the current native thread has bootstrapped itself into the Java virtual machine. At this point it is running just like a native method. Thus it can, among other things, issue JNI calls to invoke the Prog.main
method.
Eventually the program calls the DestroyJavaVM
function to unload the Java virtual machine.
Linking with JVM
You may decide that your native application will be deployed only with a particular virtual machine implementation. For exmaple, cc -I<jni.h dir> -L<libjava.so dir> -lthread -ljvm invoke.c
.
JNI does not specify the name of the native library that implements a Java virtual machine, so you cannot link the native application to a unknown JVM directly. The solution is to use run-time dynamic linking to load the particular virtual machine library needed by the application as below.
void *JNU_FindCreateJavaVM(char *vmlibpath)
{
void *libVM = dlopen(vmlibpath, RTLD_LAZY);
if (libVM == NULL) {
return NULL;
}
return dlsym(libVM, "JNI_CreateJavaVM");
}
Attaching Native Threads
Sometimes, like Figure 7.1, server-spawned native methods may have a shorter life span than the Java virtual machine. We need a way to attach a native thread to a Java virtual machine that is already running.
APIs for such purpose are: JNI_AttachCurrentThread
, DetachCurrentThread
. Code example of the thread is. Note that, the caller should create JVM and provides jvm
.
void thread_fun(void* arg)
{
JNIENV *env;
jint ret = (*jvm)->AttachCurrentThread(jvm, (void**)&env, NULL);
if (res < 0) {
fprintf(stderr, "Attach failed\n");
return;
}
/* get and run Java method... */
detach:
if ((*env)->ExceptionOccurred(env)) {
(*env)->ExceptionDescribe(env);
}
(*jvm)->DetachCurrentThread(jvm);
}
Additional JNI Features
JNI and Threads
A good reference on multithreaded programming in the Java programming language is Concurrent Programming in Java, Design Principles and Patterns, by Doug Lea (Addison-Wesley, 1997).
A JNIEnv
pointer is only valid in the thread associated with it. Local references are valid only in the thread that created them.
Monitor
Monitors are the primitive synchronization mechanism on the Java platform. Each object can be dynamically associated with a monitor. Native code can use JNI functions to perform equivalent synchronization on JNI references. You can use the MonitorEnter
function to enter the monitor and the MonitorExit
function to exit the monitor.
Calling MonitorExit
when the current thread does not own the monitor results in an error and causes an IllegalMonitorStateException
to be raised.
The Java API contains several other methods that are useful for thread synchronization.: Object.wait
, Object.notify
, and Object.notifyAll
. No JNI functions are supplied that correspond directly to these methods because monitor wait and notify operations are not as performance critical as monitor enter and exit operations.
JNIEnv
and JavaVM
When the current thread is already attached to the virtual machine, AttachCurrentThread
returns the JNIEnv
interface pointer that belongs to the current thread.
There are many ways to obtain the JavaVM
pointer: by recording it when the virtual machine is created, by querying for the created virtual machines using JNI_GetCreatedJavaVMs
, by calling the JNI function GetJavaVM
inside a regular native method, or by defining a JNI_OnLoad
handler. Unlike the JNIEnv
pointer, the JavaVM
pointer remains valid across multiple threads so it can be cached in a global variable.
Although the interface pointer is thread-local, the doubly indirected JNI function table is shared among multiple threads. Native code may use the JNIEnv
pointer as a thread ID that remains unique for the lifetime of the thread.
There are several advantages of using an interface pointer.
- Most importantly, because the JNI function table is passed as an argument to each native method, native libraries do not have to link with a particular implementation of the Java virtual machine.
- Second, by not using hard-wired function entries, a virtual machine implementation may choose to provide multiple versions of JNI function tables.
- Finally, multiple JNI function tables make it possible to support multiple versions of
JNIEnv
-like interfaces in the future.
Thread Models
The thread model dictates how the system implements essential thread operations such as scheduling, context switching, synchronization, and blocking in system calls. In a native thread model the operating system manages all the essential thread operations. In a user thread model, on the other hand, the application code implements the thread operations.
The application using native code may not function properly if the Java virtual implementation and the native code have a different notion of threading and synchronization.
If the Java virtual machine implementation uses native thread support, the native code can freely invoke thread-related primitives in the host environment. If the Java virtual machine implementation is based on a user thread package, the native code should either link with the same user thread package or rely on no thread operations at all.
Registering Native Methods
The RegisterNatives
function is useful for a number of purposes: sometimes more convenient and more efficient; native method implementation can be updated at runtime; for native methods in a native application - The virtual machine would not be able to find this native method implementation automatically because it only searches in native libraries, not the application itself.
Load and Unload Handlers
The JNI_OnLoad
function returns JNI_ERR
on error and otherwise returns the JNIEnv
version JNI_VERSION_1_2
needed by the native library.
The rules of unloading native libraries are:
- The virtual machine associates each native library with the class loader
L
of the classC
that issues theSystem.loadLibrary
call. - The virtual machine calls the
JNI_OnUnload
handler and unloads the native library after it determines that the class loaderL
is no longer a live object. - The
JNI_OnUnload
handler runs in a finalizer, and is either invoked synchroniously byjava.lang.System.runFinalization
or invoked asynchronously by the virtual machine.
JNI_OnUnload
runs in an unknown thread context, avoid complex locking operations that may introduce deadlocks. Keep in mind that classes have been unloaded when the JNI_OnUnload
handler is invoked.
Reflection Support
Reflection generally refers to manipulating language-level constructs at runtime.
Reflection support is provided at the Java programming language level through the java.lang.reflect
package as well as some methods in the java.lang.Object
and java.lang.Class
classes.
JNI provides the following functions to make the frequent reflective operations from native code more efficient and convenient: GetSuperclass
, GetObjectClass
, IsInstanceOf
, [From/To]ReflectedField
and [From/To]ReflectedMethod
.