Revision of Date
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.
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.
Let us go through the canonical HelloWorld example.
<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.
<Compile CHelloWorld.java>= javac CHelloWorld.java
<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.
<printHelloWorld.c>= #include "CHelloWorld.h" #include <stdio.h> JNIEXPORT void JNICALL Java_CHelloWorld_printHelloWorld (JNIEnv *env, jobject obj) { printf("Hello World\n"); }
<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.
<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
<Javap usage example>= javap -s CHelloWorld
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.
Mapping between primitive types and native types [*]
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
<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>
<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.
Release Modes [*]
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
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;
}
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.
The following are very useful.