Check out the new USENIX Web site.
Using javaUSENIX

 

using java

The Java Native Interface

rao_prithvi

by Prithvi Rao
<prithvi+@kiwilabs.com>

Prithvi Rao is the founder of Kiwilabs, which specializes in software engineering methodology and Java training. He has worked on the development of the MACH OS and a real-time version of MACH, and he holds two patents resulting from his work on mobile robots.



In a previous ;login: article (June 1998), I discussed the embedding of a Java Bean into an ActiveX environment. I showed that components written in Java can interoperate with components written in other languages at the "component" level, namely ActiveX components. This article continues the theme of interoperability, but with a different perspective. We will look at the Java Native Interface (JNI), which is also a very important part of the Java development infrastructure. The JNI capability permits programs written in Java to call programs written in C or C++, and also the reverse: a C or C++ program can invoke the Java Virtual Machine (JVM) and invoke methods on a Java object.

This capability is mandated in several situations. For instance, in order to run Abstract Windowing ToolKit (AWT) applications, the graphics context for the application has to eventually make "native" calls to the libraries that perform the graphics operations. It is likely that these libraries are written in C (more likely than C++). Also, Java communication packages eventually make "native" calls; we can gather from this that "native" refers to either C or C++. Finally, it is likely that most organizations have significant amounts of C and C++ code that has been debugged and tested. JNI plays a vital role in supporting the use of this code.

Let's start with a summary of the main considerations of JNI, then present examples of code and a more detailed description of this capability.

Java Native Interface (JNI)

The Java Native Interface is a standard programming interface between Java and native programs. Typically, native programs are used when they cannot be written in Java (legacy code, performance considerations).

The JNI makes it possible to create, inspect, and update Java objects including arrays and strings. The JNI also permits the catching and throwing of exceptions, loads classes, obtains class information, and performs runtime type checking. It is possible using JNI to link a Java Virtual Machine (JVM) into non-Java applications.

As with any important technology, interested parties will jostle for acceptance of their ideas. Sun, with its JNI, is one of three currently trying to influence the Native Interface standard. The other two main parties who can influence the standard are Netscape with the Java Runtime Interface (JRI) and Microsoft with the Raw Native Interface (RNI). The standard is still evolving, and this article will discuss only JNI from Sun.

The advantages of a standard are that each vendor can support a large body of native code. Also, tool builders need not maintain different kinds of native method interfaces. This means that application programmers write one version of their native code. Some requirements of the evolving standard are:

  • Binary compatibility so that one version of native code exists.
  • Efficiency so that the overhead of the interface should be small.
  • Functionality so that native methods must be able to do useful things.

It appears that JNI is becoming the standard, so it is probably a good idea to program to it.

Working with Native Methods

The steps involved in using JNI are:

  1. Write the Java code and define relevant methods "native."
  2. Compile the Java code.
  3. Create a header file that acts as a bridge between Java and native code.
  4. Implement the native code.
  5. Create a shared library of the native code.

JNI Example

Let's take a look at an example of how JNI can be used to call C programs from Java. (In a future article I will discuss how to do the reverse.) We will look at a package to manipulate rational numbers. The package supplies the following routines:

  • radd - add two rational numbers
  • rsub - subtract two rational numbers
  • rmul - multiply two rational numbers
  • rdiv - divide two rational numbers
  • euclid_gcd - the greatest common divisor of two integers

We will create a Java class called Rational:

 public class Rational
 {
   public int[] r;

 public Rational(int n, int d)
   {
     if (d == 0)
      throw new ArithmeticException("Rational: requires " +
            ;"non-zero denominator");
     r = new int[2];
     r[0] = n;
     r[1] = d;

   }

 public static Rational RationalAdd(Rational r1, Rational r2)
 {
     return new Rational(add(r1.r, r2.r));
 }
 public static Rational RationalSub(Rational r1, Rational r2)
 {
     return new Rational(sub(r1.r, r2.r));
}

 public static Rational RationalMul(Rational r1, Rational r2)

 {
     return new Rational(mul(r1.r, r2.r));
 }

 public static Rational RationalDiv(Rational r1, Rational r2)
 {
     return new Rational(div(r1.r, r2.r));
 }

Next we define the native methods:

 public static native int[] add(int[] r1, int[] r2);

This says that there is a function called "add" which is not written in Java.

 public static native int[] sub(int[] r1, int[] r2);

 public static native int[] mul(int[] r1, int[] r2);

 public static native int[] div(int[] r1, int[] r2);

 public static native int[] gcd(int x, int y);

Next we create a toString method. This is always a good idea:

 public String toString()
 {
   StringBuffer sb = new StringBuffer();

   sb.append(r[0]);
   sb.append('/');
   sb.append(r[1]);

   return new String(sb);
 }

Now we need to add code to load the shared library:

 static
 {
   System.loadLibrary("rat");
 }

This call loads the librat.so (Solaris) shared library so that Java programs can invoke the native calls.

Now that we have the Rational class defined we can compile the code:

 javac Rational.java

This produces a .class file from which we create a header file which acts as a "bridge" between the Java and C code.

 javah -jni Rational

The output of this is a file "Rational.h". The entries in this file must not be edited, because the JVM uses it to interpret the parameters being passed between Java and the native code.

For our native function "add" the entry in this file looks like this:
/*
* Class:Rational
* Method:add
* Signature: ([I[I)[I
*/
JNIEXPORT jintArray JNICALL Java_Rational_add
(JNIEnv *, jclass, jintArray, jintArray);

Every native function will have a similar entry based on its signature. We will discuss the details in a subsequent article.

Next we write the C code that implements the native functions. We include only the "add" example here. Readers can send mail to <prithvi+@kiwilabs.com> for a full code example.

 /*
 The interface between java and C for the rational number package
 */

 #include <jni.h>
 #include "Rational.h"
 #include <stdio.h>

 int *radd(int *, int *);

 JNIEXPORT jintArray JNICALL
 Java_Rational_add(JNIEnv *jenv, jclass jc, jintArray jr1,
          jintArray jr2)
 {
 jint *r;
 jintArray jr;
 jsize rlen = (*jenv)->GetArrayLength(jenv, jr1);
 jint *r1arr = (*jenv)->GetIntArrayElements(jenv, jr1, 0);
 jint *r2arr = (*jenv)->GetIntArrayElements(jenv, jr2, 0);

 (*jenv)->ReleaseIntArrayElements(jenv, jr1, r1arr, 0);
 (*jenv)->ReleaseIntArrayElements(jenv, jr2, r2arr, 0);

 r = (jint *) radd((int *) r1arr, (int *) r2arr);
 jr = (*jenv)->NewIntArray(jenv, rlen);
 (*jenv)->SetIntArrayRegion(jenv, jr, 0, rlen, r);

 return jr;
 }

The above code example is the native code implementation of "add". In subsequent articles I will cover in more detail the writing of native functions. For now it is important to note that native code includes the "jni.h" and "Rational.h" include files. These two files are crucial because they form the "bridge" which permits the Java native programs to pass parameters between them.

JNI and Threading

Since Java is a multithreaded language, several threads can call a native method concurrently. However, it is possible that a native method might be suspended in the middle of its operation when a second thread calls it. It is entirely up to the programmer to guarantee that the native call is thread-safe. One way of doing this is to declare the native thread as "synchronized" or implement some other strategy within the native method to ensure correct concurrent data manipulation.

Another consideration of multithreading is that the JNIEnv pointer must not be passed across threads. This is because the internal structure to which it points is allocated on a per-thread basis and so contains information that makes sense only in that particular thread.

JNI and Java Exceptions

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. Some JNI functions for exception handling are:

  • 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 exception.

Of these exceptions, it is not possible to ignore ExceptionOccurred() and ExceptionClear(). After exceptions are handled by ExceptionOccurred() it is necessary to clear them using ExceptionClear() to avoid unpredictable results (especially if a call is made to a JNI function while an exception is pending).

Conclusion

We have looked at the use of JNI for calling native methods written in C from a Java program. Clearly, this is a very important consideration in Java being a credible language; Java could not be taken seriously were this not possible. It is equally important to be able to invoke Java methods from native code, and this is also possible.

A strength of Java is that not only is native code integration possible, but it is simple and straightforward with all the tools available as part of the JDK distribution. However, it is still necessary to understand the use of JNI within the larger context of multithreading and exception handling, which have important implications when you are working with native interfaces.

 

?Need help? Use our Contacts page.
Last changed: 16 Mar. 1999 jr
Java index
Publications index
USENIX home