Contents | Prev | Next | Index | The Java Native Interface Programmer's Guide and Specification |
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.
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).
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.
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.
A 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);
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:
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.
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");
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 } }
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 a private method
g
, f
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.
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.
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.
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.
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.
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/PopLocalFrame
functions 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.
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.
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.
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.
Contents | Prev | Next | Index | The Java Native Interface Programmer's Guide and Specification |
Copyright ©
2002 Sun Microsystems, Inc. All rights reserved
Please send any comments
or corrections to jni@java.sun.com