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

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

  • 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: %d\n", value);
  env-&gt;SetIntField(obj, fid, 6);
  env-&gt;CallVoidMethod(obj, mid);
  value = env-&gt;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?

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



Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • On-demand Event Event Date: October 29, 2014 It's well understood how critical version control is for code. However, its importance to DevOps isn't always recognized. The 2014 DevOps Survey of Practice shows that one of the key predictors of DevOps success is putting all production environment artifacts into version control. In this webcast, Gene Kim discusses these survey findings and shares woeful tales of artifact management gone wrong! Gene also shares examples of how high-performing DevOps …

  • On-demand Event Event Date: December 18, 2014 The Internet of Things (IoT) incorporates physical devices into business processes using predictive analytics. While it relies heavily on existing Internet technologies, it differs by including physical devices, specialized protocols, physical analytics, and a unique partner network. To capture the real business value of IoT, the industry must move beyond customized projects to general patterns and platforms. Check out this webcast and join industry experts as …

Most Popular Programming Stories

More for Developers

RSS Feeds