Lecture 5: Introduction to JNI

Balasubramanian Narasimhan
Department of Statistics
Stanford University
Stanford, CA 94305

Revision of Date

Table of Contents

Abstract

In this lecture, I introduce the Java Native Interface (JNI). JNI allows your Java objects to use code written in C/C++ or Fortran. This is a sizeable topic but the stuff here get you started.

Preliminaries

Before proceeding, there are some mundance issues that you should be aware of.

If you want to experiment with these things on RGMiller note that the default stuff is still Java 1.1. That is, if you use javac, you are getting the Java 1.1 compiler. This is what people want when they write applets since most browsers are still Java 1.x. The examples below should work with 1.1 as well. However, if you run into problems, try the Java 2 SDK. This is available in /usr/java2 which you have to explicity prepend to your path if you want to have Java 2 be the default. In your .nycshrc add the following line.

<C-Shell command to make Java 2 the default>=
set path = (/usr/java2/bin $path)

You obviously know that using JNI, by definition, will make your application platform dependent. However, there is an additional catch. If you use the Windows platform and Microsoft Visual J++, it also makes it vendor dependent. For, the JNI is at the core of the Sun/Microsoft lawsuit. Microsoft's implementation is called Raw Native Interface (RNI) and is useful for working with Microsoft's application especially with COM/DCOM which are acronyms for Component Object Model and Distributed Component Object Model respectively.

Because of the platform-specific nature of the JNI, I will try to cover as many combinations as possible.

JNI

[*] Java Native methods are flagged by the keyword native. The following are the basic steps in calling a C program from Java.
  1. Create a Java class that declares the native method. The implementation of this native method calls the actual C routine.
  2. Compile the Java class you created in step 1. At this point, you really don't even have to have the C routine at hand.
  3. Run the Java class through a tool called javah. This tool generates a header file for the native method that you will need to use with your C code.
  4. Code your native method in C. The name and signature your native code must have is dictated by the header file you created in step 3.
  5. Create a Shared library file using the C code. On Windows one produces DLL's and on Unix one produces .so libraries or whatever is appropriate for the platform. A Makefile is indispensible for this step.
  6. Now use the Java class like any other class. This may require setting of some environment variables so that the shared library you created in step 5 is located at runtime.

Example 1

[*]

Let us go through the canonical HelloWorld example.

  1. Create the CHelloWorld class.

    <CHelloWorld.java>=
    public class CHelloWorld  {
        // static initializer
        static {
            System.loadLibrary("hello");
        }
        public native void printHelloWorld();
    } // CHelloWorld
    

    Note that there is a static initializer. This part of code is executed by the Java Runtime whenever this class is loaded. Thus, one can be assured that whenever the class CHelloWorld is loaded, the runtime will make sure it knows how to find the C routines you will package into the shared library.

    Next, the actual method is flagged as native which tells the compiler that the actual implementation is elsewhere in native code. There is no implementation of the method in Java.

    A word about the name of the library: hello. As I said before, the library that you will create on windows is typically called hello.dll whereas on our main system RGMiller, it will be called libhello.so. Regardless, you only need to use the name hello and the system looks for the library by the appropriate name.

  2. Compile CHelloWorld.java.

    <Compile CHelloWorld.java>=
    javac CHelloWorld.java
    

  3. Generate the header file for use in your C code.

    <Generate CHelloWorld.h>=
    javah -jni CHelloWorld
    

    This will generate a new file in the current directory called CHelloWorld.h. I reproduce the contents of the file below.

    <Contents of CHelloWorld.h>=
    /* DO NOT EDIT THIS FILE - it is machine generated */
    #include <jni.h>
    /* Header for class CHelloWorld */
    
    #ifndef _Included_CHelloWorld
    #define _Included_CHelloWorld
    #ifdef __cplusplus
    extern "C" {
    #endif
    /*
     * Class:     CHelloWorld
     * Method:    printHelloWorld
     * Signature: ()V
     */
    JNIEXPORT void JNICALL Java_CHelloWorld_printHelloWorld
      (JNIEnv *, jobject);
    
    #ifdef __cplusplus
    }
    #endif
    #endif
    

    Note that to use this header file, you must have access to jni.h a header file that is distributed with the JDK. This is what defines things like JNIEXPORT, JNIEnv, jobject.

    The actual name of the C routine you must write is Java_CHelloWorld_printHelloWorld. The naming convention is

    <prefix><fully qualified class name>_<methodName>
    

    In our case the prefix is Java_. There is no package here so the second part is simply CHelloWorld and the rest is obvious. But if CHelloWorld was in a package called test then the second part would be test_CHelloWorld.

    As you probably guessed the scheme is more detailed than this since in Java methods are differentiated only by signatures and not just by names. Experiment by adding new methods with same names but with different signatures. But the scheme itself is not of importance, since you can always work from the generated header file.

  4. Here now is the code for the printHelloWorld method.

    <printHelloWorld.c>=
    #include "CHelloWorld.h"
    #include <stdio.h>
    
    JNIEXPORT void JNICALL  
    Java_CHelloWorld_printHelloWorld (JNIEnv *env, jobject obj) {
      printf("Hello World\n");
    }
    

  5. Create the shared library. I use the following on my linux machine.

    <Create libhello.so on Linux>=
    cc -I/usr/local/src/jdk1.2.2/include \
       -I/usr/local/src/jdk1.2.2/include/linux \
       -shared -o libhello.so printHelloWorld.c
    

    On RGMiller use the following.

    <Create libhello.so on RGMiller>=
         cc -n32 -I/usr/java2/include -I/usr/java2/include/irix -c CHelloWorld.c
         cc -n32 -shared CHelloWorld.o -L/usr2/java/lib32/sgi -ljava -Wl,-woff,85 -Wl,-woff,134 -o libhello.so
    

    The -Wl,-woff,85 -Wl,-woff,134 options simply filter out some harmless warnings that are typical when linking against libjava.so.

  6. Let us now use this in a simple program.

    <TestHello.java>=
    public class TestHello {
      public static void main(String[] args) {
        new CHelloWorld().printHelloWorld();
      }
    } // TestHello
    

    After compiling this, we can run it as follows on my linux machine.

    <Run TestHello>=
    LD_LIBRARY_PATH=. java TestHello
    

    On RGMiller use

    <Run TestHello on RGMiller>=
    env LD_LIBRARY_PATH=. java TestHello
    

Discussion

[*] The jobject passed to the native method is the equivalent of this.

Example 2

[*] In this example, we'll do several things. We'll create methods that return the sum of two integers, two doubles, the sum of all elements in a double array and one that returns a new double array containing twice the elements of the passed array.

First note that a native method can directly access Java primitive types such as booleans, integers, floats, and so on, that are passed from programs written in Java.


Java Type Native Type Size in bits
boolean jboolean 8, unsigned
byte jbyte 8
char jchar 16, unsigned
short jshort 16
int jint 32
long jlong 64
float jfloat 32
double jdouble 64
void void n/a
Mapping between primitive types and native types [*]

<Summer.java>=
public class Summer  {
    static {
        System.loadlibrary("summer");
    }
    public int native sum(int a, int b);
    public double native sum(double a, double b);
    public double native sum(double[] a);
    public double[] native twice(double[] a);
} // Summer

Here is the native method implementation for the stuff.

<NativeSummer.c>=
#include "Summer.h"
<Code for summing two integers>
<Code for summing two doubles>
<Code for summing array of doubles>
<Code for returning twice an array>

The first two are very easy.

<Code for summing two integers>= (<-U)
JNIEXPORT jint JNICALL Java_Summer_sum__II
  (JNIEnv *env, jobject jobj, jint j1, jint j2) {
    return j1 + j2;
}

<Code for summing two doubles>= (<-U)
JNIEXPORT jdouble JNICALL Java_Summer_sum__DD
(JNIEnv *env, jobject jobj, jdouble j1, jdouble j2) {
    return j1 + j2;
}

The third requires a bit of thought. The parameter here is an array, which is a Java object.

<Code for summing array of doubles>= (<-U)
JNIEXPORT jdouble JNICALL Java_Summer_sum___3D
  (JNIEnv *env, jobject jobj, jdoubleArray jarray) {
    jboolean isCopy1;
    jdouble* srcArrayElems = 
           env -> GetDoubleArrayElements(jarray, &isCopy1);
    jint n = env -> GetArrayLength(jarray);
    jdouble sum;
    sum = 0.0;
    int i;
    for (i = 0; i < n; i++) {
       sum += srcArrayElems[i];
    } 
    if (isCopy1 == JNI_TRUE) {
       env -> ReleaseDoubleArrayElements(jarray, srcArrayElems, JNI_ABORT);
    }
    return sum;
}

This code calls for some explanation. The isCopy variable boolean indicates whether the copy of the primitive elements was a copy or not. If it is a copy, one must remember to free it before returning. Also, if we made any changes to the array, we must copy it back to the original one so that the changes are reflected in the original. This is the purpose of the ReleaseDoubleArrayElements call. The last parameter is summarized in the table below.


Value Meaning
0 Copy the contents of the buffer back into array and free the buffer
JNI_ABORT Free the buffer without copying back any changes
JNI_COMMIT Copy the contents of the buffer back into array but do not free buffer
Release Modes [*]

Using 0 will cause no harm, but here using JNI_ABORT is ok for us.

Another important fact to remember that since arrays of dimension higher than one are not contiguous, if your C or Fortran routine takes a multidimensional array as argument, then you must marshall those arrays in a compatible form yourself! This can be a chore especially because C stores arrays in row major order and fortran in column major order.

<Code for returning twice an array>= (<-U)
JNIEXPORT jdoubleArray JNICALL Java_Summer_twice
  (JNIEnv *env, jobject jobj, jdoubleArray jarray) {
    int i;
    jboolean isCopy1;

    jdouble* srcArrayElems = 
           env -> GetDoubleArrayElements(jarray, &isCopy1);
    jint n = env -> GetArrayLength(jarray);

    jboolean isCopy2;   
    jdoubleArray result = env -> NewDoubleArray(n);
    jdouble* destArrayElems = 
           env -> GetDoubleArrayElements(result, &isCopy2);
           
    for (i = 0; i < n; i++) {
       destArrayElems[i] = 2 * srcArrayElems[i];
    } 

    if (isCopy1 == JNI_TRUE) {
       env -> ReleaseDoubleArrayElements(jarray, srcArrayElems, JNI_ABORT);
    }

    if (isCopy2 == JNI_TRUE) {
       env -> ReleaseDoubleArrayElements(jarray, destArrayElems, 0);
    }

    return result;
}

Important
The code as written above is meant for use with a C++ compiler. So use CC or c++ instead of cc. This distinction is important because, otherwise, you will get a bunch of error messages.

If you really don't have access to a C++ compiler, the above code will still work provided you make some minor changes. For example, the call to ReleaseDoubleArrayElements would be changed to

(*env) -> ReleaseDoubleArrayElements(env, jarray, destArrayElems, 0);
Such a change has to be applied to all your calls; now you see why I did it the other way.

And now a test program:

<TestEx2.java>=
public class TestEx2 {
    public static void main(String[] args) {
        Summer s = new Summer();
        System.out.println("Sum of 5.0 and 7.0 is " + s.sum(5.0, 7.0));
        double[] a = {1.0, 2.0, 3.0, 4.0};
        double sum1 = s.sum(a);
        double[] b = s.twice(a);
        double sum2 = s.sum(b);

        System.out.println(" A    2A");
        System.out.println("--------");
        for (int i = 0; i < a.length; i++) {
            System.out.println(a[i] + "   " + b[i]);
        }
        System.out.println("--------");
        System.out.println(sum1 + "   " + sum2);
    }
} // TestEx2
In the next and last lecture, I will continue with calling Java methods from C and conclude with the Invocation API.

References

[*]

The following are very useful.

  1. Sun Tutorial on JNI at http://java.sun.com/docs/books/tutorial/native1.1/TOC.html#implementing
  2. Rob Gordon's book Essential JNI, Wiley, ISBN 0-13-679895-0.
  3. Our local SGI documentation file:/usr/java2/webdocs/release.html.

Indices

[*]

Code Chunks

[*] This index is generated automatically. The numeral is that of the first definition of the chunk.

Index of Identifiers

[*] Here is a list of the identifiers used, and where they appear. Underlined entries indicate the place of definition. This index is generated automatically. *