[76]
The Java language and its standard API are rich enough to write full-fledged
applications. But in some cases you must call
non-Java code; for example, if
you want to access operating-system-specific features,
interface with special hardware
devices, reuse a pre-existing, non-Java code base, or implement
time-critical sections of code.
Interfacing with non-Java code requires
dedicated support in the compiler and in the Virtual Machine, and additional
tools to map the Java code to the non-Java code. (There’s also a simple
approach: in Chapter 15, the section titled “a Web application”
contains an example of connecting to non-Java code using standard input and
output.) The standard solution for calling non-Java code that is provided by
Javasoft is called the Java Native Interface, which will be introduced in
this appendix. This is not an in-depth treatment, and in some cases you’re
assumed to have partial knowledge of the related concepts and
techniques.
JNI is a fairly rich programming
interface that allows you to call native methods from a Java application. It was
added in Java 1.1, maintaining a certain degree of compatibility with its Java
1.0 equivalent, the native
method interface (NMI). NMI has design characteristics that make it unsuitable
for adoption in all virtual machines. For this reason, future versions of the
language might no longer support NMI, and it will not be covered
here.
Currently, JNI is designed to interface
with native methods written only in C or C++. Using JNI,
your native methods can:
Thus,
virtually everything you can do with classes and objects in ordinary Java you
can also do in native methods.
We’ll start with a simple example:
a Java program that calls a native method, which in turn calls the Win32
MessageBox( ) API function to display a graphical text box. This
example will also be used later with J/Direct. If your platform is not Win32,
just replace the C header include:
#include <windows.h>
with
#include <stdio.h>
and replace the call to
MessageBox( ) with a call to printf( ).
//: appendixb:ShowMsgBox.java public class ShowMsgBox { private native void ShowMessage(String msg); static { System.loadLibrary("MsgImpl"); } public static void main(String[] args) { ShowMsgBox app = new ShowMsgBox(); app.ShowMessage("Generated with JNI"); } } ///:~
The native method declaration is followed
by a static block that calls System.loadLibrary( ) (which you
could call at any time, but this style is more appropriate).
System.loadLibrary( ) loads a DLL in memory and links to it. The DLL
must be in your system path or in the directory containing the Java class file.
The file name extension is automatically added by the JVM depending on the
platform.
Now compile your Java source file and run
javah on the resulting .class file.
Javah was present in version 1.0, but since you are using Java 1.1 JNI
you must specify the –jni switch:
javah –jni ShowMsgBox
Javah reads the Java class file
and for each native method declaration it generates a function prototype in a C
or C++ header file. Here’s the output: the ShowMsgBox.h source file
(edited slightly to fit into the book):
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class ShowMsgBox */ #ifndef _Included_ShowMsgBox #define _Included_ShowMsgBox #ifdef __cplusplus extern "C" { #endif /* * Class: ShowMsgBox * Method: ShowMessage * Signature: (Ljava/lang/String;)V */ JNIEXPORT void JNICALL Java_ShowMsgBox_ShowMessage (JNIEnv *, jobject, jstring); #ifdef __cplusplus } #endif #endif
As you can see by the #ifdef
__cplusplus preprocessor directive, this file can be compiled either by a C
or a C++ compiler. The first #include directive includes jni.h, a
header file that, among other things, defines the types that you can see used in
the rest of the file. JNIEXPORT and
JNICALL are macros that expand to match
platform-specific directives; JNIEnv, jobject and jstring
are JNI data type definitions.
JNI imposes a naming convention (called
name mangling) on native methods; this is important, since it’s
part of the mechanism by which the virtual machine links Java calls to native
methods. Basically, all native methods start with the word “Java,”
followed by the name of the class in which the Java native declaration appears,
followed by the name of the Java method; the underscore character is used as a
separator. If the Java native method is overloaded, then the function signature
is appended to the name as well; you can see the native signature in the
comments preceding the prototype. For more information about name mangling and
native method signatures, please refer to the JNI
documentation.
At this point, all you have to do is
write a C or C++ source file that includes the javah-generated header file and
implements the native method, then compile it and generate a dynamic link
library. This part is platform-dependent, and I’ll assume that you know
how to create a DLL. The code below implements the native method by calling a
Win32 API. It is then compiled and linked into a file called MsgImpl.dll
(for “Message Implementation”).
//: appendixb:MsgImpl.cpp //# Tested with VC++. Include path must be //# adjusted to find the JNI headers. See the //# makefile for this chapter (in the //# downloadable source code) for an example. #include <jni.h> #include <windows.h> #include "ShowMsgBox.h" BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, void** lpReserved) { return TRUE; } JNIEXPORT void JNICALL Java_ShowMsgBox_ShowMessage(JNIEnv * jEnv, jobject this, jstring jMsg) { const char * msg; msg = (*jEnv)->GetStringUTFChars(jEnv, jMsg,0); MessageBox(HWND_DESKTOP, msg, "Thinking in Java: JNI", MB_OK | MB_ICONEXCLAMATION); (*jEnv)->ReleaseStringUTFChars(jEnv, jMsg,msg); } ///:~
If you have no interest in Win32, just
skip the MessageBox( ) call; the interesting part is the surrounding
code. The arguments that are passed into the native method are the gateway back
into Java. The first, of type JNIEnv, contains all the hooks that
allow you to call back into the JVM. (We’ll look at this in the next
section.) The second argument has a different meaning depending on the type of
method. For non-static methods like the example above (also called
instance methods), the second argument is the equivalent of the
“this” pointer in C++ and similar to this in Java: it’s
a reference to the object that called the native method. For static
methods, it’s a reference to the Class object where the method is
implemented.
The remaining arguments represent the
Java objects passed into the native method call. Primitives are also passed in
this way, but they come in by value.
In the following sections we’ll
explain this code by looking at how to access and control the JVM from inside a
native method.
JNI functions are those that the
programmer uses to interact with the JVM from inside a native method. As you can
see in the example above, every JNI native method receives a special argument as
its first parameter: the JNIEnv argument, which is a pointer to a special
JNI data structure of type JNIEnv_. One element of the JNI data structure
is a pointer to an array generated by the JVM; each element of this array is a
pointer to a JNI function. The JNI functions can be called from the native
method by dereferencing these pointers (it’s simpler than it sounds).
Every JVM provides its own implementation of the JNI functions, but their
addresses will always be at predefined offsets.
Through the JNIEnv argument, the
programmer has access to a large set of functions. These functions can be
grouped into the following categories:
The number of JNI
functions is quite large and won’t be covered here. Instead, I’ll
show the rationale behind the use of these functions. For more detailed
information, consult your compiler’s JNI documentation.
If you take a look at the jni.h
header file, you’ll see that inside the #ifdef __cplusplus
preprocessor conditional, the JNIEnv_ structure is defined as a class
when compiled by a C++ compiler. This class contains a number of inline
functions that let you access the JNI functions with an easy and familiar
syntax. For example, the line in the preceding example
(*jEnv)->ReleaseStringUTFChars(jEnv, jMsg,msg);
can be rewritten as follows in
C++:
jEnv->ReleaseStringUTFChars(jMsg,msg);
You’ll notice that you no longer
need the double dereferencing of the jEnv pointer, and that the same
pointer is no longer passed as the first parameter to the JNI function call. In
the rest of these examples, I’ll use the C++ style.
As an example of accessing a JNI
function, consider the code shown above. Here, the JNIEnv argument
jEnv is used to access a Java String. Java Strings are in
Unicode format, so if you receive one and want to pass it to a non-Unicode
function (printf( ), for example), you must first convert it into
ASCII characters with the JNI function GetStringUTFChars( ). This
function takes a Java String and converts it to UTF-8 characters. (These
are 8 bits wide to hold ASCII values or 16 bits wide to hold Unicode. If the
content of the original string was composed only of ASCII, the resulting string
will be ASCII as well.)
GetStringUTFChars is the name of
one of the fields in the structure that JNIEnv is indirectly pointing to,
and this field in turn is a pointer to a function. To access the JNI function,
we use the traditional C syntax for calling a function though a pointer. You use
the form above to access all of the JNI
functions.
In the previous example we passed a
String to the native method. You can also pass Java objects of your own
creation to a native method. Inside your native method, you can access the
fields and methods of the object that was received.
To pass objects, use the ordinary Java
syntax when declaring the native method. In the example below,
MyJavaClass has one public field and one public method. The
class UseObjects declares a native method that takes an object of class
MyJavaClass. To see if the native method manipulates its argument, the
public field of the argument is set, the native method is called, and
then the value of the public field is printed.
//: appendixb:UseObjects.java class MyJavaClass { public int aValue; public void divByTwo() { aValue /= 2; } } public class UseObjects { private native void changeObject(MyJavaClass obj); static { System.loadLibrary("UseObjImpl"); } public static void main(String[] args) { UseObjects app = new UseObjects(); MyJavaClass anObj = new MyJavaClass(); anObj.aValue = 2; app.changeObject(anObj); System.out.println("Java: " + anObj.aValue); } } ///:~
After compiling the code and handing the
.class file to javah, you can implement the native method. In the
example below, once the field and method ID are obtained, they are accessed
through JNI functions.
//: appendixb:UseObjImpl.cpp //# Tested with VC++. Include path must be //# adjusted to find the JNI headers. See the //# makefile for this chapter (in the //# downloadable source code) for an example. #include <jni.h> JNIEXPORT void JNICALL Java_UseObjects_changeObject( JNIEnv * env, jobject jThis, jobject obj) { jclass cls; jfieldID fid; jmethodID mid; int value; cls = env->GetObjectClass(obj); fid = env->GetFieldID(cls, "aValue", "I"); mid = env->GetMethodID(cls, "divByTwo", "()V"); value = env->GetIntField(obj, fid); printf("Native: %d\n", value); env->SetIntField(obj, fid, 6); env->CallVoidMethod(obj, mid); value = env->GetIntField(obj, fid); printf("Native: %d\n", value); } ///:~
The first argument aside, the C++
function receives a jobject, which is the native side of the Java object
reference we pass from the Java code. We simply read aValue, print it
out, change the value, call the object’s divByTwo( ) method,
and print the value out again.
To access a field or method, you must
first obtain its identifier. Appropriate JNI functions take the class object,
the element name, and the signature. These functions return an identifier that
you use to access the element. This approach might seem convoluted, but your
native method has no knowledge of the internal layout of the Java object.
Instead, it must access fields and methods through indexes returned by the JVM.
This allows different JVMs to implement different internal object layouts with
no impact on your native methods.
If you run the Java program, you’ll
see that the object that’s passed from the Java side is manipulated by
your native method. But what exactly is passed? A pointer or a Java reference?
And what is the garbage collector doing during native method
calls?
The garbage
collector continues to operate during native method execution, but it’s
guaranteed that your objects will not be garbage collected during a native
method call. To ensure this, local references are created before, and
destroyed right after, the native method call. Since their lifetime wraps the
call, you know that the objects will be valid throughout the native method
call.
Since these references are created and
subsequently destroyed every time the function is called, you cannot make local
copies in your native methods, in static variables. If you want a
reference that lasts across function invocations, you need a global reference.
Global references are not created by the JVM, but the programmer can make a
global reference out of a local one by calling specific JNI functions. When you
create a global reference, you become responsible for the lifetime of the
referenced object. The global reference (and the object it refers to) will be in
memory until the programmer explicitly frees the reference with the appropriate
JNI function. It’s similar to malloc( ) and
free( ) in C.
With JNI, Java
exceptions can be thrown, caught, printed, and rethrown just as they are inside
a Java program. But it’s up to the programmer to call dedicated JNI
functions to deal with exceptions. Here are the JNI functions for exception
handling:
Among these, you
can’t ignore ExceptionOccurred( ) and
ExceptionClear( ). Most JNI functions can generate exceptions, and
there is no language feature that you can use in place of a Java try block, so
you must call ExceptionOccurred( ) after each JNI function call to
see if an exception was thrown. If you detect an exception, you may choose to
handle it (and possibly rethrow it). You must make certain, however, that the
exception is eventually cleared. This can be done in your function using
ExceptionClear( ) or in some other function if the exception is
rethrown, but it must be done.
You must ensure that the exception is
cleared, because otherwise the results will be unpredictable if you call a JNI
function while an exception is pending. There are few JNI functions that are
safe to call during an exception; among these, of course, are all the exception
handling functions.
Since Java is a multithreaded language,
several threads can call a native method concurrently. (The native method might
be suspended in the middle of its operation when a second thread calls it.)
It’s entirely up to the programmer to guarantee that the native call is
thread-safe, i.e. it does not modify shared data in an unmonitored way.
Basically, you have two options: declare the native method as
synchronized or implement some other strategy within the native method to
ensure correct, concurrent data manipulation.
Also, you should never pass the
JNIEnv pointer across threads, since the internal structure it points to
is allocated on a per-thread basis and contains information that makes sense
only in that particular thread.
The easiest way to implement JNI native
methods is to start writing native method prototypes in a Java class, compile
that class, and run the .class file through javah. But what if you
have a large, pre-existing code base that you want to call from Java? Renaming
all the functions in your DLLs to match the JNI name mangling convention is not
a viable solution. The best approach is to write a wrapper DLL
“outside” your original code base. The Java code calls functions in
this new DLL, which in turn calls your original DLL functions. This solution is
not just a work-around; in most cases you must do this anyway because you must
call JNI functions on the object references before you can use
them.
Microsoft does not support JNI, but
provides proprietary support to call non-Java code. This support is built into
the compiler, the Microsoft JVM, and external tools. You can find an
introduction to the Microsoft Java technologies in Appendix A of the online
first edition of Thinking in Java (which was also written by Andrea
Provaglio). That book is freely downloadable from http://www.BruceEckel.com and
is on the CD ROM that accompanies this book. The features described in that
section will work only if your program was compiled using the Microsoft Java
compiler and run on the Microsoft Java Virtual Machine. If you plan to
distribute your application on the Internet, or if your Intranet is built on
different platforms, this can be a serious issue.
The features demonstrated in that
appendix include:
[76]
This appendix was contributed by and used with the permission of Andrea
Provaglio (www.AndreaProvaglio.com).