The Java Native Interface The JNIEnv argument

Bruce Eckel’s Thinking in Java Contents | Prev | Next

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.

  • Create,
    inspect, and update Java objects (including arrays and
    Strings)
  • Call
    Java methods
  • Catch
    and throw exceptions
  • Load
    classes and obtain class information
  • Perform
    runtime type checking

Calling
a native method

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( ).

The
first step is to write the Java code declaring a native method and its arguments:

class ShowMsgBox {
  public static void main(String [] args) {
    ShowMsgBox app = new ShowMsgBox();
    app.ShowMessage("Generated with JNI");
  }
  private native void ShowMessage(String msg);
  static {
    System.loadLibrary("MsgImpl");
  }
}

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.


The
C header file generator: javah

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 &lt;jni.h&gt;
/* 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


Name
mangling and function signatures

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.


Implementing
your DLL

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”).

#include &lt;windows.h&gt;
#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)-&gt;GetStringUTFChars(jEnv, jMsg,0);
  MessageBox(HWND_DESKTOP, msg,
    "Thinking in Java: JNI",
    MB_OK | MB_ICONEXCLAMATION);
  (*jEnv)-&gt;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.

Accessing
JNI functions:

The
JNIEnv argument

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:

  • Obtaining
    version information
  • Performing
    class and object operations
  • Handling
    global and local references to Java objects
  • Accessing
    instance fields and static fields
  • Calling
    instance methods and static methods
  • Performing
    string and array operations
  • Generating
    and handling Java exceptions
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.


Accessing
Java Strings

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

Passing
and using Java objects

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.

class MyJavaClass {
  public void divByTwo() { aValue /= 2; }
  public int aValue;
}
 
public class UseObjects {
  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);
  }
  private native void
  changeObject(MyJavaClass obj);
  static {
    System.loadLibrary("UseObjImpl");
  }
}

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.

JNIEXPORT void JNICALL
Java_UseObjects_changeObject(
  JNIEnv * env, jobject jThis, jobject obj) {
  jclass cls;
  jfieldID fid;
  jmethodID mid;
  int value;
  cls = env-&gt;GetObjectClass(obj);
  fid = env-&gt;GetFieldID(cls,
        "aValue", "I");
  mid = env-&gt;GetMethodID(cls,
        "divByTwo", "()V");
  value = env-&gt;GetIntField(obj, fid);
  printf("Native: %dn", value);
  env-&gt;SetIntField(obj, fid, 6);
  env-&gt;CallVoidMethod(obj, mid);
  value = env-&gt;GetIntField(obj, fid);
  printf("Native: %dn", 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?

JNI
and Java exceptions

  • Throw( )

    Throws
    an existing exception object. Used in native methods to rethrow an exception.

  • ThrowNew( )

    Generates
    a new exception object and throws it.

  • ExceptionOccurred( )

    Determines
    if an exception was thrown and not yet cleared.

  • ExceptionDescribe( )

    Prints
    an exception and the stack trace.

  • ExceptionClear( )

    Clears
    a pending exception.

  • FatalError( )

    Raises
    a fatal error. Does not return.

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.

JNI
and threading

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.

Using
a pre-existing code base

More by Author

Must Read