Chapter 9 Leveraging existing local libraries

One application of JNI is to write native methods using existing code in existing native libraries. A typical approach described in this chapter is to generate a class library that encapsulates a set of native methods.

This chapter begins by introducing the most straightforward way to write a wrapper class – a one-to-one mapping. Then we introduce a technique, shared pegs, to simplify the task of writing encapsulated classes.

One-to-one mapping and shared pegs are techniques that encapsulate local methods. At the end of this chapter, we will also discuss how to use the Peer class to encapsulate local data structures.

The methods described in this chapter expose local libraries directly using local methods and therefore have the disadvantage of making applications calling such local methods dependent on local libraries. Such an application can only run on an operating system that provides the native library. A better approach is to declare local methods that are operating system independent. Only the native functions that implement these native methods use the native libraries directly, limiting the need to migrate to native functions. Applications that include native method declarations do not require migration. (The last sentence is not very clear what the meaning of original: A preferred approach is to declare operating system-independent native methods. Only the native functions implementing those native methods use the native libraries directly, limiting the need for porting to those native functions. The application, including the native method declarations, Does not need to be ported.)

9.1 One-to-one Mapping

Let’s start with a simple example. Suppose we want to write a wrapper class that exposes the atOL methods in the standard C library:

long atol(const char *str);
Copy the code

The atol method parses a string and returns the decimal value the string represents. There is probably no reason to define such a function in practice, since the Java API already provides methods of type integer.parseInt. For example, the result of atol(” 100 “) is an integer value of 100. The wrapper class we define is as follows:

public class C {
    public static native int atol(String str); . }Copy the code

To demonstrate JNI programming in C++, we will use C++ to implement native methods in this chapter. C. toc’s native methods implemented in C++ are as follows:

JNIEXPORT jint JNICALL
Java_C_atol(JNIEnv *env, jclass cls, jstring str)
{
    const char *cstr = env->GetStringUTFChars(str, 0);
    if (cstr == NULL) {
        return 0; /* out of memory */
    }
    int result = atol(cstr);
    env->ReleaseStringUTFChars(str, cstr);
    return result;
}
Copy the code

The implementation of the method is fairly straightforward. We use GetStringUTFChars to convert Unicode strings because decimal values are ASCII characters.

Let’s test the first more complex example where we pass a structure pointer to a C function. Suppose we want to write a wrapper class that exposes the Win32 platform CreateFile API function:

typedef void * HANDLE;
typedef long DWORD;
typedef struct {. } SECURITY_ATTRIBUTES;HANDLE CreateFile(
    const char *fileName, // file name
    DWORD desiredAccess, // access (read-write) mode
    DWORD shareMode, // share mode
    SECURITY_ATTRIBUTES *attrs, // security attributes
    DWORD creationDistribution, // how to create
    DWORD flagsAndAttributes, // file attributes
    HANDLE templateFile // file with attr. to copy
);
Copy the code

The CreateFile function supports some features unique to Win32 that are not available on the File API of the Java standalone platform. For example, the CreateFile method may be used to specify special access modes and file properties to open Win32 named pipes and handle serial communication.

In this book, we won’t discuss the CreateFile function in too much detail. Our focus will be on how to map the CreateFile function to a local method defined in a wrapper class named Win32:

public class Win32 {
    public static native int CreateFile(
        String fileName, // file name
        int desiredAccess, // access (read-write) mode
        int shareMode, // share mode
        int[] secAttrs, // security attributes
        int creationDistribution, // how to create
        int flagsAndAttributes, // file attributes
        int templateFile); // file with attr. to copy. }Copy the code

The move from char to String is obvious. We map the native Win32 type long(DWORD) to the Java programming language int. The Win32 type HANDLE, an opaque 32-bit pointer type, is also mapped to an int.

Because of potential differences in how fields are arranged in the internals, we cannot map C constructs to classes in the Java programming language. Instead, we use an array to store the contents of the C structure SECURITY_ATTRIBUTES. The caller may also pass NULL as an argument to secAttrs to specify the default value for the Win32 security property. We won’t discuss the contents of the SECURITY_ATTRIBUTES structure and how to encode them in an int array.

A C++ implementation of the above native method looks like this:

JNIEXPORT jint JNICALL Java_Win32_CreateFile(
    JNIEnv *env,
    jclass cls,
    jstring fileName, // file name
    jint desiredAccess, // access (read-write) mode
    jint shareMode, // share mode
    jintArray secAttrs, // security attributes
    jint creationDistribution, // how to create
    jint flagsAndAttributes, // file attributes
    jint templateFile) // file with attr. to copy
{
    jint result = 0;
    jint *cSecAttrs = NULL;
    if (secAttrs) {
        cSecAttrs = env->GetIntArrayElements(secAttrs, 0);
        if (cSecAttrs == NULL) {
            return 0; /* out of memory */}}char *cFileName = JNU_GetStringNativeChars(env, fileName);
    if (cFileName) {
        /* call the real Win32 function */
        result = (jint)CreateFile(cFileName,
                    desiredAccess,
                    shareMode,
                    (SECURITY_ATTRIBUTES *)cSecAttrs,
                    creationDistribution,
                    flagsAndAttributes,
                    (HANDLE)templateFile);
        free(cFileName);
    }
    /* else fall through, out of memory exception thrown */
    if (secAttrs) {
        env->ReleaseIntArrayElements(secAttrs, cSecAttrs, 0);
    }
    return result;
}
Copy the code

First we convert the security properties stored in an int array to a Jint array. If the secAttrs argument is a NULL reference, we pass NULL as a security attribute to the Win32 CreateFile method. Next, we call a helper function JNU_GetStringNativeChars(Section 8.2.2) to get the C string file name represented for the locale. Once we have converted the security attributes and filename, we pass the converted result and the remaining parameters to the Win32 CreateFile function.

We mainly check for exceptions thrown and free virtual machine resources (such as CSecAttrs).

The c. attol and Win32.createFile examples demonstrate a common way to write encapsulated classes and native methods. Each local function (for example, CreateFile) maps to a separate local stub function (for example, Java_Win32_CreateFile), which then maps to a defined local method (for example, win32.createFile). In one-to-one mapping, the pile function has two purposes:

  • This peg adjusts the local method passing convention to what the Java VIRTUAL machine expects. The virtual machine expects the local method implementation to follow the given naming convention and to accept two additional arguments (the JNIEnv pointer and the “this” pointer).
  • This stub converts between Java programming language types and native types. For example, the Java_Win32_CreateFile function converts the JString filename to a locale-specific C string.

9.2 sharing pile

The one-to-one mapping method requires you to write a peg function for each local method you want to encapsulate. This can get tedious when you’re faced with the task of writing wrapped classes for a large number of native methods. In this section, we introduce the concept of shared stakes and demonstrate how shared stakes can be used to simplify the task of writing encapsulation classes.

A shared peg is a local method derived from other local methods. The shared stake is responsible for converting the type received by the local function from the parameter type supplied by the caller.

We’ll introduce a shared peg class called CFunction, but first let’s demonstrate how it simplifies the implementation of the C.atol method.

public class C {
    private static CFunction c_atol =
        new CFunction("msvcrt.dll".// native library name
                "atol".// C function name
                "C"); // calling convention
    public static int atol(String str) {
        return c_atol.callInt(newObject[] {str}); }... }Copy the code

C. Atol is no longer a local method (and thus no longer requires a peg function). Instead, c. tol is defined to use the CFunction class. A shared stake is implemented inside the CFunction class. The static variable c.c_atol stores a CFunction instance object corresponding to the C function atol in the msvcrt. DLL library. The CFunction constructor call also specifies that atOL follows the C calling convention. Once the C_atol field is initialized, calling the C. atonl method only needs to be rescheduled through c_atol.callint (shared stake).

The CFunction class belongs to the class hierarchy we will create and use:

An instance of the CFunction class represents a pointer to a C function. CFunction is a subclass of CPointer that represents an arbitrary C pointer.

public class CFunction extends CPointer {
    public CFunction(String lib, // native library name
                String fname, // C function name
                String conv) { // calling convention. }public native int callInt(Object[] args); . }Copy the code

The callInt method takes a java.lang.Object array as its argument. It checks the array types in elements, converts them (for example, from JString to CHAR *), and passes them as arguments to the underlying C function. The callInt method then returns the result of the underlying C function as an int. The CFunction class can define methods such as callFloat or callDouble to handle C functions with other return types.

The CPointer class is defined as follows:

public abstract class CPointer {
    public native void copyIn(
        int bOff, // offset from a C pointer
        int[] buf, // source data
        int off, // offset into source
        int len); // number of elements to be copied
    public native void copyOut(...).; . }Copy the code

CPointer is an abstract class that supports arbitrary access to C Pointers. For example, the copyIn method copies elements from an int array to the position pointed to by the C pointer. This method must be used with care, as it can easily be used to destroy any memory location in the address space. Local methods such as cpointer.copyin are just as insecure as direct pointer operations in C.

CMalloc is a subclass of CPointer that points to a large chunk of memory allocated in the C heap using malloc.

public class CMalloc extends CPointer {
    public CMalloc(int size) throws OutOfMemoryError {... }public native void free(a); . }Copy the code

The CMalloc constructor allocates a memory region of a given size in the C heap, which is freed by the cmalloc.free method.

Using the CFunction and CMalloc classes, we can re-implement win32.createFile as follows:

public class Win32 {
    private static CFunction c_CreateFile =
        new CFunction ("kernel32.dll".// native library name
                "CreateFileA".// native function
                "JNI"); // calling convention

    public static int CreateFile(
        String fileName, // file name
        int desiredAccess, // access (read-write) mode
        int shareMode, // share mode
        int[] secAttrs, // security attributes
        int creationDistribution, // how to create
        int flagsAndAttributes, // file attributes
        int templateFile) // file with attr. to copy
    {
        CMalloc cSecAttrs = null;
        if(secAttrs ! =null) {
            cSecAttrs = new CMalloc(secAttrs.length * 4);
            cSecAttrs.copyIn(0, secAttrs, 0, secAttrs.length);
        }
        try {
            return c_CreateFile.callInt(new Object[] {
                        fileName,
                        new Integer(desiredAccess),
                        new Integer(shareMode),
                        cSecAttrs,
                        new Integer(creationDistribution),
                        new Integer(flagsAndAttributes),
                        new Integer(templateFile)});
        } finally {
            if(secAttrs ! =null) { cSecAttrs.free(); }}}... }Copy the code

We cache the CFunction object in a static variable. Win32 API CreateFile is exported from kernel32.dll as the CreateFileA method. Another export entry, CreateFileW, takes a Unicode string as a filename argument. This function follows the JNI calling convention, which is the standard Win32.CreateFile calling convention (STdCall).

The Win32.createFile implementation first allocates an area of memory in the C heap large enough to hold the security property parameters. All parameters are then packaged into an array and the underlying C function CreateFileA is called through the shared scheduler. Finally, the win32. CreateFile method frees the block of memory used to temporarily store security property parameters. We call csecattrs.free in the finally clause to ensure that temporary C memory is freed, even if the c_createFile.callint call throws an exception.

9.3 One-to-one mapping and comparison of shared piles

One-to-one mapping and shared pegs are two ways to create encapsulated classes for local libraries. Each has its own advantages.

The advantage of the shared peg approach is that programmers do not need to write a lot of peg functions in native code. Once a shared peg implementation, such as CFunction, is available, the programmer may not need to write an extra line of native code to create a wrapper class.

But sharing piles must be used with care. By sharing stakes, programmers can essentially write C code using the Java programming language. This breaks type safety in the Java programming language. Incorrect use of shared pegs can result in corrupt memory layout errors and application crashes.

The advantage of one-to-one mapping is that it is generally more efficient in transforming data type transfers between the Java virtual machine and native code. On the other hand, shared pegs handle at most a set of predefined parameter types, and even these parameter types do not achieve optimal performance. CallInt callers often need to create an Integer object for each int argument. This increases the memory and time overhead of the shared peg scheme.

In practice, you need to balance performance, portability, and short-term productivity. Shared pegs may be suitable for taking advantage of code that is inherently non-portable and can tolerate slight performance degradation, while one-to-one mapping should be used in cases where maximum performance and portability are required.

9.4 Realization of shared piles

So far, we’ve treated the CFunction, CPointer, and CMalloc classes as black boxes. This section describes how to implement them using the basic JNI functionality.

9.4.1 CPointer class

We want to look at the CPointer class because it is a superclass of CFunction and CMalloc. The virtual CPointer class contains a 64-bit field, peer, that stores the underlying C pointer:

public abstract class CPointer {
    protected long peer;
    public native void copyIn(int bOff, int[] buf, int off,int len);
    public native void copyOut(...).; . }Copy the code

The implementation of local methods such as copyIn is fairly straightforward:

JNIEXPORT void JNICALL
Java_CPointer_copyIn__I_3III(JNIEnv *env, jobject self, jint boff, jintArray arr, jint off, jint len)
{
    long peer = env->GetLongField(self, FID_CPointer_peer);
    env->GetIntArrayRegion(arr, off, len, (jint *)peer + boff);
}
Copy the code

FID_CPointer_peer is a precomputed field ID for cpointer. peer. Native code implementations use the long name naming scheme (Section 11.3) to resolve conflicts with overloaded copyIn native method implementations of other array types in the CPointer class.

9.4.2 CMalloc class

The CMalloc class adds two local methods to allocate and free C memory blocks:

public class CMalloc extends CPointer {
    private static native long malloc(int size);
    public CMalloc(int size) throws OutOfMemoryError {
        peer = malloc(size);
        if (peer == 0) {
            throw newOutOfMemoryError(); }}public native void free(a); . }Copy the code

The CMalloc constructor calls the local method cmalloc. malloc and throws an exception if cmalloc. malloc fails to allocate a chunk of memory in C stack space. We can implement the CMallo. Malloc and CMalloc. Free methods as follows:

JNIEXPORT jlong JNICALL
Java_CMalloc_malloc(JNIEnv *env, jclass cls, jint size) {
    return (jlong)malloc(size);
}

JNIEXPORT void JNICALL
Java_CMalloc_free(JNIEnv *env, jobject self) {
    long peer = env->GetLongField(self, FID_CPointer_peer);
    free((void *)peer);
}
Copy the code

9.4.3 CFunction class

The implementation of the CFunction class requires the use of dynamic linking support and CPU-specific assembly code in the operating system. The implementation described below is for the Win32/Intel X86 environment. Once you understand how to implement the CFunction class, you can follow the same steps to implement it on other platforms.

The CFunction class is implemented as follows:

public class CFunction extends CPointer {
    private static final int CONV_C = 0;
    private static final int CONV_JNI = 1;
    private int conv;
    private native long find(String lib, String fname);

    public CFunction(String lib,// native library name
            String fname, // C function name
            String conv) { // calling convention
        if (conv.equals("C")) {
            conv = CONV_C;
        } else if (conv.equals("JNI")) {
            conv = CONV_JNI;
        } else {
            throw new IllegalArgumentException( "bad calling convention");
        }
        peer = find(lib, fname);
    }

    public native int callInt(Object[] args); . }Copy the code

The CFunction class declares a private field, conv, that stores the rules for calling C functions. The cfunction. find native method is implemented as follows:

JNIEXPORT jlong JNICALL
Java_CFunction_find(JNIEnv *env, jobject self, jstring lib, jstring fun)
{
    void *handle;
    void *func;
    char *libname;
    char *funname;

    if ((libname = JNU_GetStringNativeChars(env, lib))) {
        if ((funname = JNU_GetStringNativeChars(env, fun))) {
            if ((handle = LoadLibrary(libname))) {
                if(! (func = GetProcAddress(handle, funname))) { JNU_ThrowByName(env,"java/lang/UnsatisfiedLinkError", funname); }}else {
                JNU_ThrowByName(env,
                    "java/lang/UnsatisfiedLinkError",
                    libname);
            }
            free(funname);
        }
        free(libname);
    }
    return (jlong)func;
}
Copy the code

Cfunction. find converts library and function names to locale-specific C strings, and then calls the Win32 API functions LoadLibrary and GetProcAddress to locate C functions in the named local library.

The callInt method performs the task of rescheduling to the underlying C language:

JNIEXPORT jint JNICALL
Java_CFunction_callInt(JNIEnv *env, jobject self, jobjectArray arr)
{
#define MAX_NARGS 32
    jint ires;
    int nargs, nwords;
    jboolean is_string[MAX_NARGS];
    word_t args[MAX_NARGS];

    nargs = env->GetArrayLength(arr);
    if (nargs > MAX_NARGS) {
        JNU_ThrowByName(env,
            "java/lang/IllegalArgumentException"."too many arguments");
        return 0;
    }

    // convert arguments
    for (nwords = 0; nwords < nargs; nwords++) {
        is_string[nwords] = JNI_FALSE;
        jobject arg = env->GetObjectArrayElement(arr, nwords);

        if (arg == NULL) {
            args[nwords].p = NULL;
        } else if (env->IsInstanceOf(arg, Class_Integer)) {
            args[nwords].i =
                env->GetIntField(arg, FID_Integer_value);
        } else if (env->IsInstanceOf(arg, Class_Float)) {
            args[nwords].f =
                env->GetFloatField(arg, FID_Float_value);
        } else if (env->IsInstanceOf(arg, Class_CPointer)) {
            args[nwords].p = (void *)
                env->GetLongField(arg, FID_CPointer_peer);
        } else if (env->IsInstanceOf(arg, Class_String)) {
            char * cstr =
                JNU_GetStringNativeChars(env, (jstring)arg);
            if ((args[nwords].p = cstr) == NULL) {
                goto cleanup; // error thrown
            }
            is_string[nwords] = JNI_TRUE;
        } else {
            JNU_ThrowByName(env,
                "java/lang/IllegalArgumentException"."unrecognized argument type");
            goto cleanup;
        }
        env->DeleteLocalRef(arg);
    }
    void *func =
        (void *)env->GetLongField(self, FID_CPointer_peer);
    int conv = env->GetIntField(self, FID_CFunction_conv);

    // now transfer control to func.
    ires = asm_dispatch(func, nwords, args, conv);

cleanup:
    // free all the native strings we have created
    for (int i = 0; i < nwords; i++) {
        if (is_string[i]) {
            free(args[i].p); }}return ires;
}
Copy the code

Let’s assume that we have set up a set of global variables to cache the appropriate class references and field ids. For example, the field ID of the global reference to FID_CPointer_peer cache cpointer. peer and the global reference Class_String are global references to java.lang.String. The word_t type represents a machine word, defined as follows:

typedef union {
    jint i;
    jfloat f;
    void *p;
} word_t;
Copy the code

The Java_CFunction_callInt function iterates through the array of arguments and checks the type of each element:

  • If the element is a null reference, it passes the null pointer to the C function
  • If the element is an instance of the java.lang.Integer class, its Integer value is extracted and passed to the C function
  • If the element is an instance of the java.lang.Float class, its floating-point value is extracted and passed to the C function
  • If the element is an instance of the CPointer class, its peer pointer is extracted and passed to the C function
  • If the argument is an instance of java.lang.String, it is converted to the locale-c String and passed to the C method
  • Otherwise, IllegalArgumentException will be thrown before returning from the Java_CFunction_callInt method. We need to carefully check for possible errors in converting arguments and releasing temporary storage allocated by C strings.

Code that passes arguments from temporary buffer arguments to C functions requires direct manipulation of the C stack. It is written with inline assemblies:

int asm_dispatch(void *func, // pointer to the C function
            int nwords, // number of words in args array
            word_t *args, // start of the argument data
            int conv)  // calling convention 0: C
                        // 1: JNI
{
    __asm {
        mov esi, args
        mov edx, nwords
        // word address -> byte address
        shl edx, 2
        sub edx, 4
        jc args_done

        // push the last argument first
args_loop:
        mov eax, DWORD PTR [esi+edx]
        push eax
        sub edx, 4
        jge SHORT args_loop

args_done:
        call func
        // check for calling convention
        mov edx, conv
        or edx, edx
        jnz jni_call

        // pop the arguments
        mov edx, nwords
        shl edx, 2
        add esp, edx
jni_call:
        // done, return value in eax}}Copy the code

The assembler routine copies the arguments to the C stack and then redispatches them to the C function func. When func returns, the ASM_Dispatch routine checks the func calling convention. If func follows the C call convention, ASM_Dispatch will pop up the arguments passed to func. If func follows the JNI calling convention, asM_Dispatch does not pop arguments; Func pops up the argument before returning.

9.5 Peer class

Both one-to-one mapping and shared pegs solve the problem of encapsulating local functions. The problem of wrapping local data structures was also encountered while building a shared stub implementation. Recall the definition of the CPointer class:

public abstract class CPointer {
    protected long peer;
    public native void copyIn(int bOff, int[] buf, int off, int len);
    public native void copyOut(...).; . }Copy the code

It contains a 64-bit peer field that references a local data structure (in this case, a chunk of memory in the C address space). Subclasses of CPointer assign specific meanings to the peer field. For example, the CMalloc class uses the peer field to point to a chunk of memory in the C heap:

Classes that directly correspond to local data structures such as CPointer and CMalloc are called peer classes. You can build peer classes for various local data structures, such as:

  • File descriptor
  • Socket descriptor
  • Windows or other graphical user interface components

9.5.1 Peer Class in Java

The current Java 2 SDK version 1.2 internally uses the peer class to implement the java.io, Java.net, and java.awt packages. For example, the java.io.FileDescriptor class contains a private field fd representing the local FileDescriptor.

// Implementation of the java.io.FileDescriptor class
public final class FileDescriptor {
    private intfd; . }Copy the code

Suppose you want to perform a file operation that is not supported by the Java platform. You might try to use JNI to find the underlying FileDescriptor for instances of java.io.FileDescriptor. JNI allows you to access a private field as long as you know the name and type of the field. You might think you can operate directly on the local file descriptor. However, there are some problems with this approach:

  • First, you rely on the java.io.FileDescriptor implementation to store the local FileDescriptor in a private field called fd. However, there is no guarantee that future implementations of Sun or third-party implementations of the java.io.FileDescriptor classes will still use the same private field name fd as the native FileDescriptor. Assume that the native code for the peer field name may not work with different implementations of the Java platform.
  • Second, operations performed directly on the native file descriptor may break the internal consistency of the Peer class. For example, the java.io.FileDescriptor instance maintains an internal state indicating whether the underlying native FileDescriptor is closed or not. If you use native code to bypass the peer class and turn off the underlying FileDescriptor directly, the state maintained in the java.io.FileDescriptor instance will no longer be consistent with the real state of the local FileDescriptor. Peer class implementations generally assume that they have exclusive access to the underlying local data structure. The only way to solve these problems is to define your own peer class that encapsulates local data structures. In the example above, you can define your own file descriptor peer class that hides the requirement operations. This approach does not let you use your own peer class to implement Java API classes. For example, you cannot pass your own FileDescriptor instances to methods that expect instances of java.io.FileDescriptor. However, you can easily define your own peer class in a Java API that implements a standard interface. This is a strong argument for designing apis based on interfaces rather than classes.

9.5.2 Releasing local Data Structures

The Peer class is defined using the Java programming language, although instances of the Peer class are automatically collected by the garbage collector. But you want to make sure that the underlying local data structure is also released.

Recall that the CMalloc class contains a free method that explicitly frees MALloc’s C memory:

public class CMalloc extends CPointer {
    public native void free(a); . }Copy the code

You must remember to call the free method of an instance of the CMalloc class. Otherwise, the CMalloc class instance may be collected by the garbage collector, but its corresponding MALloc allocated C memory will not be collected.

Some programmers like to add a Finalize method to the Peer class, such as CMalloc:

public class CMalloc extends CPointer {
    public native synchronized void free(a);
    protected void finalize(a) { free(); }... }Copy the code

The virtual machine calls the Finalize method before it garbage collects the CMalloc instance. If you forget to call the free method, finalize method will help you free the C memory area that malloc requested.

You need to make some minor changes to the cmalloc.free native method implementation to account for the possibility of multiple calls. You also need to make cmalloc.free a synchronous method to avoid thread contention conditions:

JNIEXPORT void JNICALL
Java_CMalloc_free(JNIEnv *env, jobject self)
{
    long peer = env->GetLongField(self, FID_CPointer_peer);
    if (peer == 0) {
        return; /* not an error, freed previously */
    }
    free((void *)peer);
    peer = 0;
    env->SetLongField(self, FID_CPointer_peer, peer);
}
Copy the code

We use two statements to set the peer field:

peer = 0;
env->SetLongField(self, FID_CPointer_peer, peer);
Copy the code

Instead of a statement:

env->SetLongField(self, FID_CPointer_peer, 0);
Copy the code

The C++ compiler treats the literal 0 as a 32-bit integer, not a 64-bit integer. Some C++ compilers allow you to specify 64-bit integer literals, but using 64-bit literals is not as easy to use.

Defining a Finalize method is an appropriate safeguard, but you should never rely on Finalizers as your sole means of releasing native data structures. The reason is that local data structures may consume more resources than equal instances. Java virtual machines may not be able to free local resources fast enough when garbage collection and Finalize instances occur.

Defining a Finalize method may introduce performance issues. It is generally slower to create and recycle instances with A Finalize method than to create and recycle instances without a Finalize method.

You don’t need to define a Finalize method if you can ensure that you can manually reclaim local data structures for the Peer class. You must ensure that local data structures are freed on all execution paths; Otherwise you could be causing a resource leak. Pay special attention to exceptions that may be thrown when using the peer instance. Always release local data structures in the finally clause:

CMalloc cptr = new CMalloc(10);
try{...// use cptr
} finally {
    cptr.free();
}
Copy the code

The finally clause ensures that CPTR will be released even if an exception occurs ina try block.

9.5.3 Return point of a Peer instance

We have shown that the Peer class usually contains a private field pointing to the underlying local data structure. In some cases, you also want to include references from local data structures in instances of peer classes. This happens, for example, when the native code needs to start a callback for instance methods in the Peer class.

Suppose we are building a hypothetical user interface component called KeyInput. The native C++ component of KeyInput, key_input, receives a key_pressed C++ function call event from the operating system when the user presses a key. The key_input C++ component reports operating system events to the KeyInput instance by calling the keyPressed method on the KeyInput instance. The arrows in the figure below indicate how key events are initiated by the user’s key and propagated from the key_INPUT C ++ component to the KeyInput peer:

The KeyInput peer class is defined as follows:

class KeyInput {
    private long peer;
    private native long create(a);
    private native void destroy(long peer);

    public KeyInput(a)   {
        peer = create();
    }

    public destroy(a) {
        destroy(peer);
    }

    private void keyPressed(int key) {.../* process the key event */}}Copy the code

The create local method implementation allocates an instance of the C++ structure key_input. C++ structs are similar to C++ classes, except that all members are public in the structure and private in the class. In this example, we use a C++ construct instead of a C++ class mainly because of confusion with classes in the Java programming language.

// C++ structure, native counterpart of KeyInput
struct key_input {
    jobject back_ptr;         // back pointer to peer instance
    int key_pressed(int key); // called by the operating system
};

JNIEXPORT jlong JNICALL Java_KeyInput_create(JNIEnv *env, jobject self)
{
    key_input *cpp_obj = new key_input();
    cpp_obj->back_ptr = env->NewGlobalRef(self);
    return (jlong)cpp_obj;
}

JNIEXPORT void JNICALL Java_KeyInput_destroy(JNIEnv *env, jobject self, jlong peer)
{
    key_input *cpp_obj = (key_input*)peer;
    env->DeleteGlobalRef(cpp_obj->back_ptr);
    delete cpp_obj;
    return;
}
Copy the code

The create local method allocates a C++ structure and initializes its back_ptr field as a global reference to the KeyInput peer instance. The local method destroy removes global references to peer instances and C++ constructs applied by peer instances. The keyInput constructor calls the create method to set the connection between the peer instance and the local instance:

When the user presses a key, the operating system calls the C++ member function key_input::key_pressed. This member function responds to the event by calling back to the keyPressed method on the KeyInput peer instance.

// returns 0 on success, -1 on failure
int key_input::key_pressed(int key) {
    jboolean has_exception;
    JNIEnv *env = JNU_GetEnv();
    JNU_CallMethodByName(env, &has_exception, java_peer, "keyPressed"."()V", key);
    if (has_exception) {
        env->ExceptionClear();
        return - 1;
    } else {
        return 0; }}Copy the code

The key_press member function clears any exceptions after the callback and returns the error condition to the operating system using the -1 return code. For definitions of the JNU_CallMethodByName and JNU_GetEnv utility functions, see Sections 6.2.3 and 8.4.1, respectively.

Let’s discuss one last question before we close this section. Suppose you add finalize methods to your KeyInput class to avoid potential memory leaks:

class KeyInput {...public synchronized destroy(a) {
        if(peer ! =0) {
            destroy(peer);
            peer = 0; }}protect void finalize(a) { destroy(); }}Copy the code

The destroy method checks to see if the peer field is zero and sets it to zero after calling the overloaded local destroy method. It is defined as a synchronous method to avoid race conditions.

However, the code above does not work as you would expect. The virtual machine will never garbage collect any KeyInput instances unless you explicitly call destroy. The KeyInput constructor creates a JNI global reference to the KeyInput instance. Global references prevent garbage collection of KeyInput instances. You can overcome this problem by using weak global references instead of global references:

JNIEXPORT jlong JNICALL
Java_KeyInput_create(JNIEnv *env, jobject self)
{
    key_input *cpp_obj = new key_input();
    cpp_obj->back_ptr = env-><b>NewWeakGlobalRef</b>(self);
    return (jlong)cpp_obj;
}

JNIEXPORT void JNICALL
Java_KeyInput_destroy(JNIEnv *env, jobject self, jlong peer) {
    key_input *cpp_obj = (key_input*)peer;
    env-><b>DeleteWeakGlobalRef</b>(cpp_obj->back_ptr);
    delete cpp_obj;
    return;
}
Copy the code

Chapter 10 Pitfalls and Pitfalls

To highlight the important techniques covered in previous chapters, this chapter covers some of the mistakes made by JNI programmers. Each of the errors described here occurs in a real-world project.

10.1 Error Checking

The most common mistake in writing native methods is forgetting to check to see if an error condition has occurred. Unlike the Java programming language, native languages do not provide a standard exception mechanism. JNI is not dependent on any specific native exception mechanism (such as C++ exceptions). Therefore, programmers need to perform explicit checks after every call that might cause an exception. Not all JNI functions raise exceptions, but most can be checked. Exception detection is tedious, but it is necessary to ensure the robustness of applications that use native methods.

The tedious work of error checking greatly underscores the need to limit native code to a well-defined subset of applications that need to use JNI (Section 10.5, p.

10.2 Passing incorrect arguments to JNI functions

JNI functions do not attempt to detect or recover from invalid arguments. If NULL or (jobject) 0xFFFFFFFF is passed to a JNI function that expects a reference, the behavior of the result is undefined. In practice, this can result in incorrect results or a virtual machine crash. Java 2 SDK version 1.2 gives you the command line option -xcheck :jni. This option instructs the virtual machine to detect and report many (though not all) cases where native code passes illegal arguments to JNI functions. Checking the validity of parameters can cause a lot of overhead, so it is not enabled by default.

Not checking the validity of arguments is a common practice in C and C++ libraries. The code using the library is responsible for ensuring that all arguments passed to the library function are valid. However, if you are used to the Java programming language, you may need to get used to certain aspects of JNI programming that lack security checks.

10.3 Confusing JClass with Jobject

When you first use JNI, the distinction between instance references (values of type Jobject) and class references (values of type JClass) can cause confusion.

An instance reference corresponds to an array, a java.lang.Object instance, or one of its subclasses. A Class reference corresponds to a java.lang.Class instance that represents the Class type.

An operation like GetFieldID that accepts jClass is a class operation because it gets the field descriptor from the class. In contrast, GetIntField using Jobject is an instance operation because it gets the value of a field from an instance. Jobject’s association with instance operations and jClass’s association with class operations is consistent across all JNI functions, so it’s easy to remember that class operations are different from instance operations.

10.4 Phase jboolean parameters

Jboolean is an 8-bit unsigned C type that can store values between 0 and 255. The value 0 corresponds to the constant JNI_FALSE, and values between 1 and 255 correspond to JNI_TRUE. However, numbers with 32-bit or 16-bit values greater than 255 and 8 bits lower than 0 can cause problems.

Suppose you have defined a function print that takes a parameter condition of type jboolean:

void print(jboolean condition) {
    /* C compilers generate code that truncates condition to its lower 8 bits. */
    if (condition) {
        printf("true\n");
    } else {
        printf("false\n"); }}Copy the code

The previous definition was correct. However, the following legitimate calls can have some unexpected results:

int n = 256; /* the value 0x100, whose lower 8 bits are all 0 */
print(n);
Copy the code

We pass a non-zero value (256) to print, expecting it to represent truth. But because all bits except the low 8 are truncated, the argument evaluates to 0. The program prints “false,” contrary to expectations.

When casting a cast type (such as int) to JBoolean, a good rule of thumb is to always evaluate the condition of an integer type to avoid unintentional errors in the casting process. You can override the print method as follows:

n = 256;
print (n ? JNI_TRUE : JNI_FALSE);
Copy the code

10.5 Boundaries between Java applications and native code

When designing Java applications supported by native code, a common question is “What and how much should I include in native code?” The boundary between native code and other applications written in the Java programming language is application-specific, but there are some general principles that apply:

  • Keep boundaries simple. The complex flow of control back and forth between the Java virtual machine and native code can be difficult to debug and maintain. This control flow also hinders optimization for high-performance virtual machine implementations. For example, it is much easier for virtual machines to implement inline methods defined in the Java programming language than inline native methods defined in C and C ++.
  • Minimize native code code. There are compelling reasons for doing so. Native code is neither portable nor type safe. Checking for errors in native code is tedious (§10.1). Keeping these parts to a minimum is good software engineering.
  • Keep local code isolated. In practice, this might mean that all native methods are in the same package, or in the same class, separated from the rest of the application. Packages, or classes that contain native methods, are essentially the “migration layer” of the application. JNI provides access to virtual machine functionality such as class loading, object creation, field access, method calls, thread synchronization, and so on. Sometimes it’s tempting to use native code for complex interactions with Java virtual machine functionality, when it’s actually easier to accomplish the same tasks in the Java programming language. The following example shows why “Programming Java in native code” is bad practice. Consider a simple statement to create a new thread written in the Java programming language:
new JobThread().start();
Copy the code

The same code can be rewritten in native code:

/* Assume these variables are precomputed and cached: * Class_JobThread: the class "JobThread" * MID_Thread_init: method ID of constructor * MID_Thread_start: method ID of Thread.start() */
aThreadObject = (*env)->NewObject(env, Class_JobThread, MID_Thread_init);
if (aThreadObject == NULL) {.../* out of memory */
}
(*env)->CallVoidMethod(env, aThreadObject, MID_Thread_start);
if ((*env)->ExceptionOccurred(env)) {
    ... /* thread did not start */
}
Copy the code

Although we have omitted the lines of code required for error checking, native code is much more complex than code written in the Java programming language.

Rather than writing a complex snippet of native code that manipulates the Java virtual machine, it is often better to define a helper method in the Java programming language and have native code issue callbacks to the helper method.

10.6 Confusing ids and References

JNI treats objects as references. Classes, strings, and arrays are special types of references. JNI uses methods and fields as ids. An ID is not a reference. Do not call a class reference a “class ID,” and do not call a method ID a “method reference.”

References are virtual machine resources that can be explicitly managed by native code. For example, the JNI function DeleteLocalRef allows local code to remove local references. In contrast, fields and method ids are managed by the virtual machine and remain valid until the class they define is unloaded. Native code cannot explicitly remove a field or method ID until the virtual machine unloads a defined class.

Native code may create multiple references to the same object. For example, a global reference and a local reference might refer to the same object. Instead, only a unique field or method ID is exported for the same definition of a field or method. If class A defines method F and class B inherits f from A, the two GetMethodID calls in the following code always return the same result:

jmethodID MID_A_f = (*env)->GetMethodID(env, A, "f"."()V");
jmethodID MID_B_f = (*env)->GetMethodID(env, B, "f"."()V");
Copy the code

10.7 Cache Fields and Method ids

The native code gets the field or method ID from the virtual machine by specifying the field or method name and type descriptor as a string (Sections 4.1, 4.2). Field and method lookups using name and type strings are slow. It is often advantageous to cache these ids. Failing to cache fields and method ids is a common performance problem in native code.

In some cases, cache ids are more than just a performance gain. The cached ID may be necessary to ensure that native code accesses the correct field or method. The following example illustrates how a cache field ID failure can result in a subtle error:

class C {
    private int i;
    native void f(a);
}
Copy the code

Suppose the local method F needs to get the value of field I in an instance of C. A simple implementation that does not cache an ID does this in three steps: 1) Get the class of the object; 2) Find the field ID of I from the class reference; And 3) access field values based on object references and field IDS:

// No field IDs cached. JNIEXPORT
void JNICALL
Java_C_f(JNIEnv *env, jobject this) {
    jclass cls = (*env)->GetObjectClass(env, this); ./* error checking */
    jfieldID fid = (*env)->GetFieldID(env, cls, "i"."I"); ./* error checking */
    ival = (*env)->GetIntField(env, this, fid); ./* ival now has the value of this.i */
}
Copy the code

The code works fine until we define another class, D, as a subclass of C, and declare a private field called “I” in D as well

// Trouble in the absence of ID caching
class D extends C {
    private int i;
    D() {
        f(); // inherited from C}}Copy the code

When D’s constructor calls C.f, the local method receives an instance of D as this parameter, CLS points to class D, and fid stands for D.i. At the end of the local method, ival contains the value of D.i instead of C.i. This may not be what you would expect when implementing native methods c.F.

The workaround is to calculate and cache the field ID instead of D when you determine that you have a class reference to C. Subsequent access to this cache ID will always refer to the correct field C.i. here is the correct version:

// Version that caches IDs in static initializers
class C {
    private int i;
    native void f(a);
    private static native void initIDs(a);
    static {
        initIDs(); // Call an initializing native method}}Copy the code

The modified native code is:

static jfieldID FID_C_i;
JNIEXPORT void JNICALL
Java_C_initIDs(JNIEnv *env, jclass cls) {
    /* Get IDs to all fields/methods of C that native methods will need. */
    FID_C_i = (*env)->GetFieldID(env, cls, "i"."I");
}

JNIEXPORT void JNICALL
Java_C_f(JNIEnv *env, jobject this) {
    ival = (*env)->GetIntField(env, this, FID_C_i); ./* ival is always C.i, not D.i */
}
Copy the code

The field ID is computed and cached in C’s static initializer. This guarantees that the field ID of C.i will be cached, so the local method implementation Java_C_f will read the value of C.i, independent of the actual class of the object.

Some method calls may also require caching. If we change the above example slightly so that classes C and D each have their own private definition of method G, then F needs to cache C.g’s method ID to avoid accidentally calling D.g. No caching is required to make proper virtual method calls. A virtual method is dynamically bound by definition to the instance that calls the method. As a result, you can safely invoke virtual methods using the JNU_CallMethodByName utility function (Section 6.2.3, p. 462). The previous example shows why we don’t define a similar JNU_GetFieldByName utility function.

10.8 Terminating a Unicode String

Unicode strings obtained from GetStringChars or GetStringCritical are not null-terminated. Call GetStringLength to find the number of 16-bit Unicode characters in the string. Some operating systems, such as Windows NT, expect two trailing zero-byte values to terminate Unicode strings. You cannot pass the result of GetStringChars to Windows NT apis that require Unicode strings. You must make another copy of the string and insert two trailing zero-byte values.

10.9 Access control rules Are Violated

JNI does not enforce access control restrictions on classes, fields, and methods, which can be expressed in the Java programming language using modifiers such as private and final. You can write native code to access or modify fields of an object, even if doing so at the Java programming language level results in an IllegalAccess sexception. Since native code can access and modify any memory location in the heap, the generosity of JNI is a conscious design decision.

Native code that bypasses source language-level access checks can adversely affect program execution. For example, if a local method modifies the final field after it is accessed inline by the JUST-in-time (JIT) compiler, inconsistencies may result. Similarly, local methods should not modify immutable objects, such as fields, in java.lang.String or java.lang.Integer instances. Doing so can lead to the destruction of invariants in the Java platform implementation.

10.10 Ignore internationalization

Strings in a Java virtual machine consist of Unicode characters, whereas native strings usually exist in locale-specific encoding. Use helper functions such as JNU_NewStringNative (Section 8.2.1) and JNU_GetStringNativeChars (Section 8.2.2) to convert between Unicode strings and locale-specific strings based on the host environment. Pay special attention to message strings and file names, which are usually internationalized. If the local method takes the file name as a JString, the file name must be converted to a local string before being passed to the C library routine.

The following native methods, myfile.open, open a file and return the file descriptor as a result:

JNIEXPORT jint JNICALL
Java_MyFile_open(JNIEnv *env, jobject self, jstring name, jint mode)
{
    jint result;
    char *cname = JNU_GetStringNativeChars(env, name);
    if (cname == NULL) {
        return 0;
    }
    result = open(cname, mode);
    free(cname);
    return result;
}
Copy the code

We use the JNU_GetStringNativeChars function to convert the JString argument because the open system call expects the filename to be a locale-specific encoding.

10.11 Reserving VM Resources

A common mistake in the local approach is forgetting to release virtual machine resources. Programmers need to pay more attention to code paths that only execute if something goes wrong. The following code snippet (slightly modified with the example in Section 6.2.2) misses a ReleaseStringChars call:

JNIEXPORT void JNICALL
Java_pkg_Cls_f(JNIEnv *env, jclass cls, jstring jstr)
{
    const jchar *cstr = (*env)->GetStringChars(env, jstr, NULL);
    if (cstr == NULL) {
        return; }...if(...). {/* exception occurred */
        /* misses a ReleaseStringChars call */
        return; }.../* normal return */
    (*env)->ReleaseStringChars(env, jstr, cstr);
}
Copy the code

Forgetting to call the ReleaseStringChars function can result in jString objects being fixed indefinitely, resulting in memory fragmentation, or C copies being retained indefinitely, resulting in memory leaks.

Whether or not GetStringChars has already created a copy of the string, there must be a ReleaseStringChars call. The following code fails to release virtual machine resources correctly:

/* The isCopy argument is misused here! * /
JNIEXPORT void JNICALL
Java_pkg_Cls_f(JNIEnv *env, jclass cls, jstring jstr)
{
    jboolean isCopy;
    const jchar *cstr = (*env)->GetStringChars(env, jstr, &isCopy);
    if (cstr == NULL) {
        return; }.../* use cstr */
    /* This is wrong. Always need to call ReleaseStringChars. */
    if(isCopy) { (*env)->ReleaseStringChars(env, jstr, cstr); }}Copy the code

Even if isCopy is JNI_FALSE, ReleaseStringChars needs to be called so that the VIRTUAL machine will unbind the JString element.

10.12 Excessive Local Reference Creation

Excessive local reference creation causes the program to retain unnecessary memory. Unnecessary local references waste memory of both the reference object and the reference itself.

Pay special attention to long-running local methods, local references created in loops, and helper functions. Take advantage of the new Push/PopLocalFrame function in Java 2 SDK version 1.2 to manage local references more efficiently. See sections 5.2.1 and 5.2.2 for a more detailed discussion of this issue.

You can specify the -verbose: Jni option in the Java 2 SDK 1.2 to require the virtual machine to detect and report excessive local reference creation. Suppose you run a class Foo with this option:

% java -verbose:jni Foo
Copy the code

The output includes the following information:

***ALERT: JNI local ref creation exceeded capacity
        (creating: 17, limit: 16).
    at Baz.g (Native method)
    at Bar.f (Compiled method)
    at Foo.main (Compiled method)
Copy the code

The local method implementation of Baz.g may not manage local references properly.

10.13 Using an Invalid Local Reference

A local reference is valid only in a single call to a local method. Local references created in a local method call are released automatically when the local function implementing the method returns. Local code should not store a local reference in a global variable and expect to use it in future local method calls.

Local references are valid only in the thread in which they were created. You should not pass a local reference from one thread to another. Global references are created when references need to be passed across threads.

10.14 Using JNIEnv across threads

The JNIEnv pointer passed as the first argument to each local method can only be used in the thread associated with it. It is an error to cache a JNIEnv interface pointer obtained from one thread and use it in another thread. Section 8.1.4 explains how to get a pointer to the current thread’s JNIEnv interface.

10.15 Thread Model Mismatch

JNI works only if the host-native code and the Java virtual machine implementation share the same threading model (Section 8.1.5). For example, a programmer cannot attach a local platform thread to an embedded Java virtual machine implemented using a user thread package.

On Solaris, Sun provides a virtual machine implementation based on a user threading package called “green Threads.” If your native code relies on Solaris native threading support, it is not suitable for a Green thread-based Java virtual machine implementation. You need a virtual machine implementation designed to work with Solaris native threads. Native thread support in Solaris JDK version 1.1 requires a separate download. Native threading support comes bundled with Solaris Java 2 SDK version 1.2.

Sun’s virtual machine implementation on Win32 supports native threads by default and can be easily embedded into native Win32 applications.

Chapter 11 JNI design Overview

This chapter gives an overview of the JNI design and, where necessary, provides motivation for the underlying technology. The design overview serves as a specification for key JNI concepts such as JNIEnv interface Pointers, local and global references, and field and method ids. Technical motivation is intended to help readers understand the trade-offs of various designs. Several times, we’ll talk about how to implement certain features. The purpose of this discussion is not to come up with a practical implementation strategy, but to clarify delicate semantic issues.

The concept of bridging programming interfaces to different languages is not new. For example, C programs can often call functions written in FORTRAN and assembly language. Similarly, implementations of programming languages such as LISP and Smalltalk support various external function interfaces.

JNI addresses issues similar to those addressed by interoperability mechanisms supported by other languages. However, there are significant differences between JNI and the interoperability mechanisms used in many other languages. JNI was not designed for a specific implementation of the Java Virtual machine. Rather, it is a native interface that every Java virtual machine implementation can support. We will elaborate on this further when we describe the JNI design goals.

11.1 Design Objectives

The most important goal of JNI’s design is to ensure that it provides binary compatibility between different Java virtual machine implementations in a given host environment. The same local library binaries will run on different virtual machine implementations for a given host environment without recompilation.

To achieve this goal, the JNI design cannot make any assumptions about the internal details of the Java virtual machine implementation. Because Java virtual machine implementation technology is evolving rapidly, we must be careful not to introduce any restrictions that might affect future advanced implementation technologies.

The second goal of JNI’s design is efficiency. To support time-critical code, JNI imposes as little overhead as possible. However, as we will see, our first goal, the need to achieve independence, sometimes requires us to adopt slightly less efficient designs than we would otherwise. We reached a compromise between efficiency and achieving independence.

Finally, JNI must be fully functional. It must expose enough Java virtual machine functionality to enable native methods and applications to accomplish useful tasks.

JNI is not intended to be the only native programming interface supported by a given Java virtual machine implementation. A standard interface benefits programmers who want to load their native code base into different Java virtual machine implementations. However, in some cases, lower-level implementation-specific interfaces may achieve higher performance. In other cases, programmers may use higher-level interfaces to build software components.

11.2 Loading a Local Library

Before an application can invoke a local method, the virtual machine must locate and load the local library that contains the implementation of the local method.

11.2.1 Class loader

Local libraries are located in the class loader. Class loaders have many uses in Java virtual machines, such as loading class files, defining classes and interfaces, providing namespace separation between software components, resolving symbolic references between classes and interfaces, and finally locating local libraries. We assume you have a basic understanding of class loaders, so we won’t go into the details of how to load and link classes in a Java virtual machine. For more detailed information on class loaders, you can refer to ACM Object-oriented Programming Systems by Sheng Liang and Gilad Bracha, Dynamic Class Loading in the Java Virtual Machine, recorded in the Minutes of the Language and Application Conference (OOPSLA).

Class loaders provide the namespace separation required to run multiple components (such as applets downloaded from different web sites) in one instance of the same virtual machine. The classloader maintains a separate namespace by mapping the name of a class or interface in the Java virtual machine to the actual class or interface type of the object. Each class or interface type is associated with its defined loader, which initially reads the class file and defines the class or interface object. Two class or interface types are identical only if they have the same name and the same definition loader. For example, in Figure 11.1, class loaders L1 and L2 both define a class named C. The two classes named C are not the same. Indeed, they contain two different f methods with different return types.

The dotted lines in the figure above represent delegate relationships between class loaders. A class loader may ask another class loader to load a class or an interface on its behalf. For example, both L1 and L2 delegate to the boot classloader to load the system class java.lang.String. Delegates allow system classes to be shared among all class loaders. This is necessary because, for example, application and system code can violate type safety if they have different concepts for java.lang.String content.

11.2.2 Class loaders and local libraries

Now assume that method F in both classes C are local methods. The virtual machine uses the name “C_f” to locate the native implementation of the two C.f methods. To ensure that each C class is linked to the correct local functions, each class loader must maintain its own set of local libraries, as shown in Figure 11.2.

Because each class loader maintains a set of native libraries, a programmer can use a library to store all the native methods needed by any number of classes, as long as the classes have the same definition loader.

Local libraries are automatically unloaded by the virtual machine when the corresponding class loader is garbage collected (Section 11.2.5).

11.2.3 Locating a Local Library

Local libraries are loaded by the system.loadLibrary method. In the following example, a clS-like static initializer loads a platform-specific native library that defines the native method f:

package pkg;
class Cls {
    native double f(int i, String s);
    static {
        System.loadLibrary("mypkg"); }}Copy the code

The parameter to system. loadLibrary is the library name chosen by the programmer. The software developer is responsible for choosing the local library name to minimize the chance of name conflicts. The virtual machine follows a standard, but host-environment-specific convention for converting library names to local library names. For example, the Solaris operating system converts the name mypkg to libmypkg.so, while the Win32 operating system converts the same mypkg name to mypkg.dll.

When the Java virtual machine starts, it builds a list of directories that will be used to locate the local libraries for classes in the application. The content of the list depends on the host environment and the implementation of the virtual machine. For example, under the Win32 JDK or Java 2 SDK version, the directory list consists of the Windows system directory, the current working directory, and entries in the PATH environment variable. In Solaris JDK or Java 2 SDK distributions, the directory list consists of entries in the LD_LIBRARY_PATH environment variable.

UnsatisfiedLinkError is raised if System.loadLibrary cannot load the specified local library. If a previous call to System.loadLibrary already loads the same local library, system.loadLibrary will complete automatically. If the underlying operating system does not support dynamic linking, all native methods must be pre-linked to the virtual machine. In this case, the virtual machine completes the System.loadLibrary call without actually loading the library.

The virtual machine internally maintains a list of loaded local libraries for each class loader. It follows three steps to determine which classloader should be associated with the newly loaded local library:

  • Identifies the direct caller to system.loadLibrary
  • Identify the class that defines the caller – Get the definition loader for the caller class in the following example, local library foo will be associated with the definition loader for C:

Java 2 SDK version 1.2 introduced a new classloader.findlibrary method that allows programmers to specify a custom library loading strategy specific to a given ClassLoader. The classLoader.findLibrary method takes a platform-independent library name (such as mypkg) as an argument and:

  • Or return empty instructions to the virtual machine to follow the default library search path
  • Or return the absolute path related to the host environment of the library file (such as “C: \mylibs\mypkg.dll”). Classloader.findlibrary is typically used with system.maplibraryName, another method added in Java 2 SDK version 1.2. System.maplibraryname maps platform-independent library names (such as mypkg) to platform-specific library filenames (such as mypkg.dll).

You can override the default library search path in Java 2 SDK version 1.2 by setting the property java.library.path. For example, the following command line launches a program Foo that needs to load a local library in the c: \mylibs directory:

java -Djava.library.path=c:\mylibs Foo
Copy the code

11.2.4 Type safety Restrictions

The virtual machine does not allow a given JNI local library to be loaded by more than one class loader. Attempting to load the same local library from multiple classloaders will result in an UnsatisfiedLinkError exception. The purpose of this restriction is to ensure that the classloader-based namespace separation mechanism remains in the local library. Without this limitation, it becomes easier to mismix classes and interfaces from different class loaders through local methods. Consider a local method foo. f that caches its own definition class Foo in a global reference:

JNIEXPORT void JNICALL
Java_Foo_f(JNIEnv *env, jobject self) {
    static jclass cachedFooClass; /* cached class Foo */
    if (cachedFooClass == NULL) {
        jclass fooClass = (*env)->FindClass(env, "Foo");
        if (fooClass == NULL) {
            return; /* error */
        }
        cachedFooClass = (*env)->NewGlobalRef(env, fooClass);
        if (cachedFooClass == NULL) {
            return; /* error */} } assert((*env)->IsInstanceOf(env, self, cachedFooClass)); ./* use cachedFooClass */
}
Copy the code

We expect the assertion to succeed because foo. f is an instance method and self refers to an instance of Foo. However, if two different Foo classes are loaded by class loaders L1 and L2, and both Foo classes are linked to the previous foo.f implementation, the assertion may fail. The cachedFooClass global reference will be created for the Foo class that first calls the f method. A later call to the f method of another Foo class will result in an assertion failure.

JDK version 1.1 does not properly perform local library separation between class loaders. This means that two classes in different class loaders can be linked using the same local method. As the previous example shows, the methods in JDK version 1.1 cause the following two problems:

  • A class may erroneously link to a local library loaded by a class with the same name in a different class loader.
  • Native methods can easily mix classes from different class loaders. This breaks the namespace separation provided by the class loader and leads to type-safety issues.

11.2.5 Uninstalling the Local Library

After garbage collection of the classloader associated with the local library, the virtual machine unloads the local library. Because classes refer to the loaders they define, this means that the virtual machine also unloads classes that call System.loadLibrary in its static initializer and load the local library (Section 11.2.2).

11.3 Linking local Methods

The virtual machine attempts to link each local method before it calls it for the first time. The earliest that a local method f can be linked is the first call to method G, where the method body of G has a reference to f. Virtual machine implementations should not attempt to link local methods prematurely. Doing so can result in unexpected linking errors because the local library implementing the local method may not have been loaded.

Linking local methods involves the following steps:

  • Identifies the classloader for the class that defines the local method.

  • Search the local library set associated with the classloader to locate the local function that implements the local method.

  • Set up the internal data structure so that all future calls to local methods jump directly to local functions. The virtual machine deduces the name of the native function from the name of the native method by wiring the following components:

  • The prefix “java_”

  • The full class name of an encoding

  • An underscore delimiter

  • An encoded function name

  • For overloaded local methods, two underscores (” __ “) are followed by encoded parameter descriptors

The virtual machine iterates through all local libraries associated with the defined loader, searching for native functions by the appropriate name. For each local library, the virtual machine first looks for a short name, that is, a name without a parameter descriptor. It then looks for the long name, which is the name with the parameter descriptor. Programmers need to use long names only if a local method is overloaded by another local method. However, this is not a problem if you override native methods using non-local methods. The latter is not in the local library.

In the following example, the native method G does not have to be linked with a long name because the other methods g are not native methods.

class Cls1 {
    int g(int i) {... }// regular method
    native int g(double d);
}
Copy the code

JNI uses a simple name encoding scheme to ensure that all Unicode characters are converted to valid C function names. The underscore (” _ “) character separates the components of the fully formatted class name. Since name or type descriptors never start with a number, we can use _0,… , _9 as the escape sequence, as follows:

If there is a native function that matches the native method name encoded in more than one local library, the native library function loaded first is linked to the native method. If no function matches the local method name, UnsatisfiedLinkError is raised.

Programmers can also call the JNI function RegisterNatives to register local methods associated with the class. The RegisterNatives function is particularly useful in static link functions.

11.4 Invocation Rules

The calling convention determines how the native function receives arguments and returns results. There are no standard calling conventions between native languages or between different implementations of the same language. For example, it is common for different C ++ compilers to generate code that follows different calling conventions.

It would be difficult, if not impossible, to require Java virtual machines to interoperate with various native calling conventions. JNI requires that local methods be written using specific standard invocation rules in a given host environment. For example, JNI follows the C calling convention on UNIX and the STdCall convention on Win32.

When programmers need to call functions that follow different calling conventions, they must write calling conventions that adapt the JNI calling conventions to suit the local language.

11.5 JNIEnv Interface Pointer

Native code accesses virtual machine functionality by calling various functions exported through the JNIEnv interface pointer.

11.5.1 Organization of JNIEnv interface Pointers

A JNIEnv interface pointer is a pointer to local thread data, with each interface function at a predefined offset in the table. NIEnv interfaces are organized like C ++ virtual function tables and also like Microsoft COM interfaces. Figure 11.3 shows a set of JNIEnv interface Pointers.

Functions that implement native methods receive a JNIEnv interface pointer as their first argument. The virtual machine guarantees that the same interface pointer is passed to the local method implementation function called from the same thread. However, local methods can be called from different threads, so different Pointers to the JNIEnv interface can be passed. Although the interface pointer is thread-local, the dual indirect JNI function table is shared by multiple threads.

The reason JNIEnv interface Pointers refer to thread-local structures is that some platforms do not have efficient thread-local storage access support. By passing a thread-local pointer, the JNI implementation inside the virtual machine can avoid many thread-local storage access operations that it would otherwise have to perform.

Because the JNIEnv interface pointer is thread-local, native code cannot use a JNIEnv interface pointer that belongs to a thread in another thread. Native code might use a JNIEnv pointer as a thread ID, which is unique for the lifetime of the thread.

11.5.2 Benefits of Interface Pointers

As opposed to hard-wired function entries. Using interface Pointers has several advantages:

  • Most importantly, because the JNI function table is passed as an argument to each local method, the local library does not have to be linked to a specific implementation of the Java virtual machine. This is critical because different vendors may name virtual machine implementations differently. In a given host environment, then each local library becomes self-contained is a prerequisite for the same local library to work across VMS from different vendors.
  • Second, by not using hard-wired feature items, virtual machine implementations can choose to provide multiple versions of the JNI menu. For example, a virtual machine implementation might support two JNI function tables: one that performs thorough illegal parameter checking and is suitable for debugging; The other performs the minimum number of checks required by the JNI specification and is therefore more efficient. Java 2 SDK version 1.2 supports the -xCheck: JNI option, which optionally turns on additional checks for JNI functions.
  • Finally, multiple JNI function tables make it possible to support multiple JNIEnv-like interfaces in the future. While we don’t foresee the need to do this, future versions of the Java platform could support new JNI function tables in addition to those pointed to by the JNIEnv interface in 1.1 and 1.2. Java 2 SDK version 1.2 introduced a JNI_Onload function that can be defined by the local library to indicate the version of the JNI function table required by the local library. A future implementation of the Java virtual machine could support multiple versions of the JNI function table at the same time, passing the correct version to the various local libraries as needed.

11.6 Transferring Data

The ability to copy raw data types, such as integers, characters, etc., between the Java virtual machine and native code. Objects, on the other hand, are passed by reference. Each reference contains a direct pointer to the underlying object. Native code never uses a pointer to the object directly. References are opaque from a native code point of view.

Passing a reference rather than a direct pointer to an object allows the virtual machine to manage objects in a more flexible manner. Figure 11.4 illustrates this flexibility. When native code holds references, the virtual machine may perform garbage collection, causing objects to be copied from one area of memory to another. The virtual machine can automatically update the contents of the reference so that the reference is still valid even though the object has been moved.

11.6.1 Global and Local References

JNI creates two kinds of object references for native code: local and global. Local references are valid for the duration of a local method call and are released automatically when the local method returns. Global references remain valid until they are explicitly released.

Object is passed as a local reference to the local method. Most JNI functions return local references. JNI allows programmers to create global references from local references. JNI functions that take objects as arguments accept global and local references. As a result, local methods may return local or global references to the virtual machine.

Local references are valid only in the thread in which they were created. Native code may not pass local references from one thread to another.

A NULL reference in JNI is an empty object in a Java virtual machine. Local or global references whose value is not NULL do not refer to empty objects.

11.6.2 Implementing a Local Reference

958/5000 To implement local references, the Java virtual machine creates a registry for each control transformation from the virtual machine to a local method. The registry maps non-removable local references to object Pointers. Registered objects cannot be garbage collected. All objects passed to local methods, including those returned as a result of JNI function calls, are automatically added to the registry. When the local method returns, the registry is deleted, allowing its entries to be garbage collected. Figure 11.5 shows how to create and delete the local reference registry. The Java Virtual Machine framework corresponding to local methods contains Pointers to the local reference registry. The D.f method calls the local method C.g. C.g implemented by the C function Java_C_g. The vm creates a local reference registry before entering Java_C_g and deletes the local reference registry after Java_C_g returns.

There are different ways to implement registries, such as using stacks, tables, linked lists, or hash tables. Although reference counting can be used to avoid duplicate entries in the registry, the JNI implementation is not necessarily required to detect and collapse duplicate entries.

Local references cannot be faithfully implemented by conservatively scanning the local stack. Native code may store local references in global or C heap data structures.

11.6.3 Weak Global Reference

Java 2 SDK version 1.2 introduced a new kind of global reference: weak global reference. Unlike normal global references, weak global references allow referenced objects to be garbage collected. Remove weak global references after the underlying object is garbage collected. Native code can test if weak global references are cleared by comparing references with NULL using IsSameObject.

11.7 Accessing Objects

JNI provides rich access functions for reference objects. This means that the same local method implementation will work regardless of how the virtual machine represents objects internally. This is a critical design decision to enable JNI to be supported by any virtual machine implementation.

The overhead of using access functions through opaque references is higher than that of accessing C data structures directly. We believe that in most cases, local methods perform unimportant tasks, thus masking the cost of additional function calls.

11.7.1 Accessing an Array of Basic data types

However, the overhead of function calls is unacceptable because the values of primitive data types in large objects, such as integer arrays and strings, are accessed repeatedly. Consider native methods for performing vector and matrix computations. Iterating over an array of integers and iterating over a function call would be very inefficient.

One solution introduces the concept of “pinning” so that local methods can ask the virtual machine not to move the contents of an array. The local method then receives a direct pointer to the element. However, this approach has two implications:

  • Garbage collector must support pinning. In many implementations, pinning is undesirable because it complicates the garbage collection algorithm and leads to memory fragmentation.
  • The virtual machine must place the raw array consecutively in memory. While this is the natural implementation of most basic arrays, Boolean arrays can be implemented as compressed or uncompressed. A packaged Boolean array uses one bit for each element, while an unzipped array usually uses one byte for each element. Therefore, native code that relies on the exact layout of a Boolean array is not portable. JNI adopted a compromise solution to both of these problems.

First, JNI provides a set of functions (for example, GetIntArrayRegion and SetIntArrayRegion) to copy the original array elements between fragments of the original array and the native memory buffer. These functions can be used if the local method needs to access only a few elements in a large array, or if the local method needs to copy the array.

Second, a programmer can use another set of functions (for example, GetIntArrayElements) to try to get a fixed version of an array element. However, depending on the implementation of the virtual machine, these capabilities can lead to storage allocation and replication. Whether these functions actually copy arrays depends on the implementation of the virtual machine, as shown below:

  • If the garbage collector supports fixation and the array layout is the same as that of a local array of the same type, copying is not required.
  • Otherwise, the array is copied to an immovable block of memory (for example, the C heap) and the necessary format conversion is performed. Returns a pointer to that copy. The local code calls a third set of functions (for example, ReleaseIntArrayElements) to inform the virtual machine that the local code no longer needs to access the array elements. When this happens, the virtual machine either cancels the array or aligns the original array with its immovable copy and frees the copy.

This approach provides flexibility. The garbage collector algorithm can make duplicate or fixed independent decisions for each array. Under a particular implementation, the garbage collector might copy small arrays but insert large ones.

Finally, the Java 2 SDK version 1.2 introduces two new functions: etPrimitiveArrayCritical and ReleasePrimitiveArrayCritical. These functions can be used in a manner similar to GetIntArrayElements and ReleaseIntArrayElements. However, after using GetPrimitiveArrayCritical to obtain a pointer to the array elements, before using ReleasePrimitiveArrayCritical released pointer, native code has a lot of restrictions. In the “critical zone,” native code should not run indefinitely, call JNI functions at will, or perform operations that might cause the current thread to block and wait for another thread in the virtual machine. Given these limitations, a virtual machine can temporarily disable garbage collection while giving native code direct access to array elements. Because without the aid of a fixed, so GetPrimitiveArrayCritical are more likely to return a pointer to the original array elements directly, such as GetIntArrayElements.

JNI implementations must ensure that native methods running in multiple threads can access the same array at the same time. For example, JNI might keep an internal counter for each fixed array so that one thread does not unpin another thread’s fixed array. Note that JNI does not need to lock the original array to monopolize the local methods. The array is also allowed to be updated from different threads, although this results in inconclusive results.

11.7.2 Fields and Methods

JNI allows native code to access fields and invoke methods defined in the Java programming language. JNI identifies methods and fields by their symbolic names and type descriptors. The two-step process is to find out the cost of a field or method from its name and descriptor. For example, to read integer instance field I in class CLS, the native code first gets a field ID, as follows:

jfieldID fid = env->GetFieldID(env, cls, "i"."I");
Copy the code

Native code can then reuse field ids without the cost of field lookups, as shown below:

jint value = env->GetIntField(env, obj, fid);
Copy the code

The field or method ID remains valid until the virtual machine uninstalls the class or interface that defines the corresponding field or method. The method or field ID becomes invalid after the class or interface is unmounted.

A programmer can derive a field or method ID from the class or interface that parses the corresponding field or method. Fields or methods can be defined in the class or interface itself, or can be inherited from a superclass or superinterface. The Java™ virtual Machine specification contains precise rules for parsing fields and methods. If the same field or method definition is resolved from these two classes or interfaces, the JNI implementation must derive the same field or method ID from both classes or interfaces. For example, if B defines the field FLD and C inherits the FLD from B, the programmer is guaranteed to get the same field ID with the field name “FLD” from classes B and C.

JNI imposes no restrictions on how field and method ids are implemented internally.

Note that you need the field name and field descriptor to get the field ID from the given class or interface. This seems unnecessary because fields cannot be overloaded with the Java programming language. However, it is legal to overload fields in a class file and run such a class file on a Java virtual machine. As a result, JNI can handle legal class files that are not generated by the Compiler of the Java programming language.

JNI can only be used to invoke a method or access a field if the programmer knows the name and type of the method or field. In contrast, the Java core reflection API allows programmers to determine the set of fields and methods in a given class or interface. It is also sometimes useful to reflect class or interface types in native code. Java 2 SDK version 1.2 provides new JNI functions that can be used in conjunction with existing Java core reflection apis. The new functions include one pair that converts between the JNI Field ID and an instance of the java.lang.Reflect. Field class, and another pair that converts between the JNI Method ID and an instance of the java.lang.Reflect. Method class.

11.8 Errors and Exceptions

Errors in JNI programming are different from those that occur in Java Virtual machine implementations. Programmer errors are caused by misuse of JNI functions. For example, a programmer might mistakenly pass an object reference to GetFieldID instead of a class reference. Throws a Java virtual machine exception, for example, when native code tries to allocate an object through JNI and runs out of memory.

11.8.1 Programming errors not checked

JNI functions do not check for programming errors. Passing invalid arguments to JNI functions results in undefined behavior. The reasons for this design decision are as follows:

  • Forcing JNI functions to check for all possible error conditions degrades the performance of all (usually correct) native methods.
  • In many cases there is not enough runtime type information to perform this check. Most C library functions are not protected against programming errors. For example, the printf function usually fires a runtime error rather than returning an error code when an invalid address is received. Forcing C library functions to check for all possible error conditions can cause the check to be repeated, once in user code, and again in the library.

Although the JNI specification does not require virtual machines to check for programming errors, virtual machine implementations are encouraged to check for common errors. For example, a virtual machine can perform more checks in a debug version of the JNI function table (Section 11.5.2, p. 541).

11.8.2 Java VM Exception

JNI does not rely on exception handling mechanisms in the native programming language. Native code can cause the Java virtual machine to Throw an exception by calling Throw or ThrowNew. Log a pending exception in the current thread. Unlike exceptions thrown in the Java programming language, exceptions thrown by native code do not immediately interrupt current execution.

The native language has no standard exception handling mechanism. Therefore, JNI programmers need to check and handle exceptions after every operation that might throw an exception. JNI programmers can handle exceptions in two ways:

  • Local methods may choose to return immediately, causing an exception to be thrown in the code that initiates the native method call.
  • Native code can clear exceptions by calling ExceptionClear and then execute its own exception-handling code. Before calling any subsequent JNI functions, it is important to check, handle, and clear pending exceptions. Calling most JNI functions with pending exceptions results in undefined results. Here is a complete list of JNI functions that can be safely called when there is a pending exception:
ExceptionOccurred
ExceptionDescribe
ExceptionClear
ExceptionCheck

ReleaseStringChars
ReleaseStringUTFchars
ReleaseStringCritical
Release<Type>ArrayElements
ReleasePrimitiveArrayCritical
DeleteLocalRef
DeleteGlobalRef
DeleteWeakGlobalRef
MonitorExit
Copy the code

The first four functions are directly related to exception handling. The rest are generic, freeing the various virtual machine resources exposed through JNI. Resources usually need to be released when an exception occurs.

11.8.3 Asynchronous Exception

One Thread may raise an asynchronous exception in another Thread by calling Thread.stop. Asynchronous exceptions do not 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 uses ExceptionOccurred to explicitly check for synchronous and asynchronous exceptions. Only those JNI functions that might raise synchronous exceptions check for asynchronous exceptions.

The local method may insert ExceptionOccurred checks where necessary (such as in a tight loop with no other exception checks) to ensure that the current thread responds to asynchronous exceptions within a reasonable amount of time.

The Java Thread API for generating asynchronous exceptions, Thread.stop, was deprecated in Java 2 SDK version 1.2. Programmers are strongly advised not to use Thread.stop, as it often results in unreliable programs. This is a particular problem for JNI code. For example, many JNI libraries written today do not carefully follow the rules for checking for asynchronous exceptions described in this section.