a number of mistakes commonly made by JNI programmers.

Chapter 10

Traps and Pitfalls


To highlight the important techniques covered in previous chapters, this chapter covers a number of mistakes commonly made by JNI programmers. Each mistake described here has occurred in real-world projects.

10.1 Error Checking

The most common mistake when writing native methods is forgetting to check whether an error condition has occurred. Unlike the Java programming language, native languages do not offer standard exception mechanisms. The JNI does not rely on any particular native exception mechanism (such as C++ exceptions). As a result, programmers are required to perform explicit checks after every JNI function call that could possibly raise an exception. Not all JNI functions raise exceptions, but most can. Exception checks are tedious, but are necessary to ensure that the application using native methods is robust.

The tediousness of error checking greatly emphasizes the need to limit native code to those well-defined subsets of an application where it is necessary to use the JNI (§10.5).

10.2 Passing Invalid Arguments to JNI Functions

The JNI functions do not attempt to detect or recover from invalid arguments. If you pass NULL or (jobject)0xFFFFFFFF to a JNI function that expects a reference, the resulting behavior is undefined. In practice this could either lead to incorrect results or virtual machine crashes. Java 2 SDK release 1.2 provides you with a command-line option -Xcheck:jni. This option instructs the virtual machine to detect and report many, though not all, cases of native code passing illegal arguments to JNI functions. Checking the validity of arguments incurs a significant amount of overhead and thus is not enabled by default.

Not checking the validity of arguments is a common practice in C and C++ libraries. Code that uses the library is responsible for making sure that all the arguments passed to library functions are valid. If, however, you are used to the Java programming language, you may have to adjust to this particular aspect of the lack of safety in JNI programming.

10.3 Confusing jclass with jobject

The differences between instance references (a value of the jobject type) and class references (a value of the jclass type) can be confusing when first using the JNI.

Instance references correspond to arrays and instances of java.lang.Object or one of its subclasses. Class references correspond to java.lang.Class instances, which represent class types.

An operation such as GetFieldID, which takes a jclass, is a class operation because it gets the field descriptor from a class. In contrast, GetIntField, which takes a jobject, is an instance operation because it gets the value of a field from an instance. The association of jobject with instance operations and the association of jclass with class operations are consistent across all JNI functions, so it is easy to remember that class operations are distinct from instance operations.

10.4 Truncating jboolean Arguments

jboolean is an 8-bit unsigned C type that can store values from 0 to 255. The value 0 corresponds to the constant JNI_FALSE, and the values from 1 to 255 correspond to JNI_TRUE. But 32-bit or 16-bit values greater than 255 whose lower 8 bits are 0 pose a problem.

Suppose you have defined a function print that takes an argument condition whose type is 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");
     }
 }

There is nothing wrong with the previous definition. However, the following innocent-looking call to print will produce a somewhat unexpected result:

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

We passed a non-zero value (256) to print expecting that it would represent true. But because all bits other than the lower 8 are truncated, the argument evaluates to 0. The program prints "false," contrary to expectations.

A good rule of thumb when coercing integral types, such as int, to jboolean is always to evaluate conditions on the integral type, thereby avoiding inadvertent errors during coercion. You can rewrite the call to print as follows:

 n = 256;
 print (n ? JNI_TRUE : JNI_FALSE);

10.5 Boundaries between Java Application and Native Code

A common question when designing a Java application supported by native code is "What, and how much, should be in native code?" The boundaries between the native code and the rest of the application written in the Java programming language are application-specific, but there are some generally applicable principles:

  • Keep the boundaries simple. Complex control flow that goes back and forth between the Java virtual machine and native code can be hard to debug and maintain. Such control flow also gets in the way of optimizations performed by high-performance virtual machine implementations. For example, it is much easier for a virtual machine implementation to inline methods defined in the Java programming language than to inline native methods defined in C and C++.
  • Keep the code on the native code side minimal. There are compelling reasons to do so. Native code is neither portable nor type-safe. Error checking in native code is tedious (§10.1). It is good software engineering to keep such parts to a minimum.
  • Keep native code isolated. In practice, this could mean that all native methods are in the same package or in the same class, isolated from the rest of the application. The package or the class containing native methods essentially becomes the "porting layer" for the application.

The JNI provides access to virtual machine functionality such as class loading, object creation, field access, method calls, thread synchronization, and so forth. It is sometimes tempting to express complex interactions with Java virtual machine functionality in native code, when in fact it is simpler to accomplish the same task in the Java programming language. The following example shows why "Java programming in native code" is bad practice. Consider a simple statement that creates a new thread written in the Java programming language:

 new JobThread().start();

The same statement can also be written 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 */
 }

The native code is much more complex than its equivalent written in the Java programming language despite the fact that we have omitted the lines of code needed for error checks.

Rather than writing a complex segment of native code manipulating the Java virtual machine, it is often preferable to define an auxiliary method in the Java programming language and have the native code issue a callback to the auxiliary method.

10.6 Confusing IDs with References

The JNI exposes objects as references. Classes, strings, and arrays are special types of references. The JNI exposes methods and fields as IDs. An ID is not a reference. Do not call a class reference a "class ID" or a method ID a "method reference."

References are virtual machine resources that can be managed explicitly by native code. The JNI function DeleteLocalRef, for example, allows native code to delete a local reference. In contrast, field and method IDs are managed by the virtual machine and remain valid until their defining class is unloaded. Native code cannot explicitly delete a field or method ID before the the virtual machine unloads the defining class.

Native code may create multiple references that refer to the same object. A global and a local reference, for example, may refer to the same object. In contrast, a unique field or method ID is derived for the same definition of a field or a 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");

10.7 Caching Field and Method IDs

Native code obtains field or method IDs from the virtual machine by specifying the name and type descriptor of the field or method as strings (§4.1,§4.2). Field and method lookups using name and type strings are slow. It often pays off to cache the IDs. Failure to cache field and method IDs is a common performance problem in native code.

In some cases caching IDs is more than a performance gain. A cached ID may be necessary to ensure that the correct field or method is accessed by native code. The following example illustrates how the failure to cache a field ID can lead to a subtle bug:

 class C {
     private int i;
     native void f();
 }

Suppose that the native method f needs to obtain the value of the field i in an instance of C. A straightforward implementation that does not cache an ID accomplishes this in three steps: 1) get the class of the object; 2) look up the field ID for i from the class reference; and 3) access the field value based on the object reference and field ID:

 // 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 */
 }

The code works fine until we define another class D as a subclass of C, and declare a private field also named "i" in D:

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

When D's constructor calls C.f, the native method receives an instance of D as the this argument, cls refers to the D class, and fid represents D.i. At the end of the native method, ival contains the value of D.i, instead of C.i. This might not be what you expected when implementing native method C.f.

The solution is to compute and cache the field ID at the time when you are certain that you have a class reference to C, not D. Subsequent accesses from this cached ID will always refer to the right field C.i. Here is the corrected version:

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

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 */
 }

The field ID is computed and cached in C's static initializer. This guarantees that the field ID for C.i will be cached, and thus the native method implementation Java_C_f will read the value of C.i independent of the actual class of the this object.

Caching may be needed for some method calls as well. If we change the above example slightly so that classes C and D each have their own definition of aprivate method gf needs to cache the method ID of C.g to avoid accidentally calling D.g. Caching is not needed for making correct virtual method calls. Virtual methods by definition dynamically bind to the instance on which the method is invoked. Thus you can safely use the JNU_CallMethodByName utility function (§6.2.3) to call virtual methods. The previous example tells us, however, why we do not define a similar JNU_GetFieldByName utility function.

10.8 Terminating Unicode Strings

Unicode strings obtained from GetStringChars or GetStringCritical are not NULL-terminated. Call GetStringLength to find out the number of 16-bit Unicode characters in a 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 expect a Unicode string. You must make another copy of the string and insert the two trailing zero byte values.

10.9 Violating Access Control Rules

The JNI does not enforce class, field, and method access control restrictions that can be expressed at the Java programming language level through the use of modifiers such as private and final. It is possible to write native code to access or modify fields of an object even though doing so at the Java programming language level would lead to an IllegalAccessException. JNI's permissiveness was a conscious design decision, given that native code can access and modify any memory location in the heap anyway.

Native code that bypasses source-language-level access checks may have undesirable effects on program execution. For example, an inconsistency may be created if a native method modifies a final field after a just-in-time (JIT) compiler has inlined accesses to the field. Similarly, native methods should not modify immutable objects such as fields in instances of java.lang.String or java.lang.Integer. Doing so may lead to breakage of invariants in the Java platform implementation.

10.10 Disregarding Internationalization

Strings in the Java virtual machine consist of Unicode characters, whereas native strings are typically in a locale-specific encoding. Use utility functions such as JNU_NewStringNative (§8.2.1) and JNU_GetStringNativeChars (§8.2.2) to translate between Unicode jstrings and locale-specific native strings of the underlying host environment. Pay special attention to message strings and file names, which typically are internationalized. If a native method gets a file name as a jstring, the file name must be translated to a native string before being passed to a C library routine.

The following native method, MyFile.open, opens a file and returns the file descriptor as its 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;
 }

We translate the jstring argument using the JNU_GetStringNativeChars function because the open system call expects the file name to be in the locale-specific encoding.

10.11 Retaining Virtual Machine Resources

A common mistake in native methods is forgetting to free virtual machine resources. Programmers need to be particularly careful in code paths that are only executed when there is an error. The following code segment, a slight modification of an 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);
 }

Forgetting to call the ReleaseStringChars function may cause either the jstring object to be pinned indefinitely, leading to memory fragmentation, or the C copy to be retained indefinitely, a memory leak.

There must be a corresponding ReleaseStringChars call whether or not GetStringChars has made a copy of the string. The following code fails to release virtual machine resources properly:

 /* 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);
     }
 }

The call to ReleaseStringChars is still needed even when isCopy is JNI_FALSE so that the virtual machine will unpin the jstring elements.

10.12 Excessive Local Reference Creation

Excessive local reference creation causes programs to retain memory unnecessarily. An unnecessary local reference wastes memory both for the referenced object and for the reference itself.

Pay special attention to long-running native methods, local references created in loops, and utility functions. Take advantage of the new Push/PopLocalFramefunctions in Java 2 SDK release 1.2 to manage local references more effectively. Refer to Section 5.2.1 and Section 5.2.2 for a more detailed discussion of this problem.

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

 % java -verbose:jni Foo

and the output contains the following:

 ***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)

It is likely that the native method implementation for Baz.g fails to manage local references properly.

10.13 Using Invalid Local References

Local references are valid only inside a single invocation of a native method. Local references created in a native method invocation are freed automatically after the native function that implements the method returns. Native code should not store a local reference in a global variable and expect to use it in later invocations of the native method.

Local references are valid only within the thread in which they are created. You should not pass a local reference from one thread to another. Create a global reference when it is necessary to pass a reference across threads.

10.14 Using the JNIEnv across Threads

The JNIEnv pointer, passed as the first argument to every native method, can only be used in the thread with which it is associated. It is wrong to cache the JNIEnv interface pointer obtained from one thread, and use that pointer in another thread. Section 8.1.4 explains how you can obtain the JNIEnv interface pointer for the current thread.

10.15 Mismatched Thread Models

The JNI works only if the host native code and the Java virtual machine implementation share the same thread model (§8.1.5). For example, programmers cannot attach native platform threads to an embedded Java virtual machine implemented using a user thread package.

On Solaris, Sun ships a virtual machine implementation that is based on a user thread package known as Green threads. If your native code relies on Solaris native thread support, it will not work with a Green-thread-based Java virtual machine implementation. You need a virtual machine implementation that is designed to work with Solaris native threads. Native threads support in Solaris JDK release 1.1 requires a separate download. The native threads support is bundled with Solaris Java 2 SDK release 1.2.

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值