http://www.javaworld.com/javaworld/javatips/jw-javatip141.html
While developing a computer-generated hologram (CGH) program, I noticed that the math routines in Java 2 Platform, Standard Edition (J2SE) 1.4x were several times slower than the corresponding routines in J2SE 1.3.1. Sun Microsystems has implemented a new math package, StrictMath
, in J2SE 1.4 that guarantees identical math calculations across all platforms at the expense of execution speed. My CGH program ran seven times slower with the new J2SE 1.4x routines! I didn't want to switch back to J2SE 1.3.1, however, because I wanted to use the new javax.imageio
package to save my holograms to disk as .png
files. I submitted some bug reports to Sun about this serious performance regression, but received no meaningful response.
Since my trusty C++ compiler produced fast math code, I just needed to call these math routines instead of the slow StrictMath
routines from my Java program. Java Native Interface (JNI) enables Java code to interface with native code written in other languages like C/C++. It enables you to write the bulk of your application in Java and still utilize highly optimized native code where necessary. And in the corporate computing world, JNI enables Java applications to connect to legacy code that might be difficult or impractical to port to Java.
To build my JNI math library, I first create a Java class, MathLib
, which declares three native methods, cos()
, sin()
, and sqrt()
. I add a main method to test these native methods:
package com.softtechdesign.math; /** * MathLib declares native math routines to get around the slow StrictMath math * routines in JDK 1.4x * @author Jeff S Smith */ public class MathLib { public static native double sin(double radians); public static native double cos(double radians); public static native double sqrt(double value); static { System.loadLibrary("MathLib"); } public static void main(String[] args) { System.out.println("MathLib benchmark..." + new java.util.Date()); double d = 0.5; for (int i=0; i < 10000000; i++) { d = (d * i) / 3.1415926; d = MathLib.sin(d); d = MathLib.cos(d); d = MathLib.sqrt(d) * 3.1415926; } System.out.println("Benchmark done. Time is " + new java.util.Date()); } }
The static initializer, which executes before the code in the main method, loads the MathLib
library. The main method performs a benchmark test of the routines by calling them in a loop. Next, I compile my class and create a header (.h
) file for it by using the standard javah tool:
javah -jni -o MathLib.h -classpath . com.softtechdesign.math.MathLib
This command creates a file called MathLib.h
that contains the following C function signatures: cos()
, sin()
, and sqrt()
. This C header file includes <jni.h>
, the standard header file for JNI applications. Note how the fully qualified class name becomes part of the C function name. MathLib.h
looks like this:
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class com_softtechdesign_math_MathLib */ #ifndef _Included_com_softtechdesign_math_MathLib #define _Included_com_softtechdesign_math_MathLib #ifdef __cplusplus extern "C" { #endif /* * Class: com_softtechdesign_math_MathLib * Method: sin */ JNIEXPORT jdouble JNICALL Java_com_softtechdesign_math_MathLib_sin (JNIEnv *, jclass, jdouble); /* * Class: com_softtechdesign_math_MathLib * Method: cos */ JNIEXPORT jdouble JNICALL Java_com_softtechdesign_math_MathLib_cos (JNIEnv *, jclass, jdouble); /* * Class: com_softtechdesign_math_MathLib * Method: sqrt */ JNIEXPORT jdouble JNICALL Java_com_softtechdesign_math_MathLib_sqrt (JNIEnv *, jclass, jdouble); #ifdef __cplusplus } #endif #endif
Next, I create a file called Math.c
and implement the native C code that includes the three math routines using the signatures found in my MathLib.h file
. Note that these routines simply call the standard C versions of sin()
, cos()
, and sqrt()
:
#include <jni.h> #include <math.h> #include "MathLib.h" #include <stdio.h> JNIEXPORT jdouble JNICALL Java_com_softtechdesign_math_MathLib_sin(JNIEnv *env, jobject obj, jdouble value) { return(sin(value)); } JNIEXPORT jdouble JNICALL Java_com_softtechdesign_math_MathLib_cos(JNIEnv *env, jobject obj, jdouble value) { return(cos(value)); } JNIEXPORT jdouble JNICALL Java_com_softtechdesign_math_MathLib_sqrt(JNIEnv *env, jobject obj, jdouble value) { return(sqrt(value)); }
The routines all return a jdouble
and have two standard JNI parameters, JNIEnv
(JNI interface pointer) and jobject
(a reference to the calling object itself, similar to this
). All implemented JNI routines have these first two parameters (even if the native method doesn't declare any parameters). The third parameter is my user-defined parameter that contains the value I pass into the function. These routines calculate the sin()
, cos()
, or sqrt()
of this parameter and return the value as a jdouble
.
Native methods can access the following types:
Boolean jboolean string jstring byte jbyte char jchar short jshort int jint long jlong float jfloat double jdouble void void
All of these types can be directly accessed except for jstring
, which requires a subroutine call to convert a Java Unicode string (2 bytes) to a C-style char*
string (1 byte UTF-8 format). To convert a jstring
to a C-style string, you might write code like the following:
JNIEXPORT void JNICALL Java_MyJavaClass_printName(JNIEnv *env, jobject obj, jstring name) { const char *str = (*env)->GetStringUTFChars(env, name, 0); printf("%s", name); //Need to release this string when done with it //to avoid memory leak (*env)->ReleaseStringUTFChars(env, name, str); }
You can use the (*env)->NewStringUTF()
function to create a new jstring
from a C-style string. For example, a C function can contain the following code:
JNIEXPORT jstring JNICALL Java_MyJavaClass_getName(JNIEnv *env, jobject obj) { return (*env)->NewStringUTF(env, "Fred Flintstone"); }
Continuing with MathLib
, I compile my C code into a library (a Dynamic Link Library (DLL) since my code runs under Windows). I use Borland C++ Builder to create my DLL. If you use C++ Builder, make sure you create a DLL project (named MathLib
) and then add the Math.c
file to it. You also need to add the following to your compiler's include
path:
\javadir\include \javadir\include\win32
C++ Builder creates a library named MathLib.dll
when it builds this project.
If you compile a Windows C library using Visual C++, you compile it like so:
cl -Ic:\javadir\include -Ic:\javadir\include\win32 -LD Math.c -FeMathLib.dll
And if you compile a C library under Solaris, you use the following command:
cc -G -I/javadir/include -I/javadir/include/solaris Math.c -o MathLib.so
The final step entails adding my DLL directory to my Java runtime library path (alternatively, I can copy the DLL to my Java program's working directory). If the Java code can't find the library, you will get a java.lang.UnsatisfiedLinkError
. Try running your Java program:
java -classpath . com.softtechdesign.math.MathLib
The benchmark code in your main method should execute. If you want to compare the execution speed with the StrictMath
versions, change the calls in MathLib.java
from MathLib.xxx()
to Math.xxx()
. On my Pentium 4, 2-GHz machine, the benchmark took 24.5 seconds with the StrictMath
routines and 7 seconds with the MathLib
(JNI) routines. Not a bad performance boost!
The MathLib
(JNI) routines are about half as fast as the Math routines in J2SE 1.3.1—a result of the overhead involved in making a JNI call. Note that when you make numerous calls to the MathLib
routines in a tight loop, you sometimes get an exception:
Unexpected Signal : EXCEPTION_FLT_STACK_CHECK occurred at PC=0x9ED0D2
I submitted a bug report to Sun about this problem, but I have not received a response. The good news is that in practice, I have never gotten the error when running any of my hologram program code.
JNI enables programmers to employ fast C/C++ math routines in their Java programs. It solves problems for which Java is poorly suited: when you need the speed of C or assembler, or when you need to write low-level code to communicate directly with hardware. It can also be used to access legacy applications or interface with libraries written in other languages.
This MathLib
example barely scratches the surface of what you can do with JNI. JNI native methods can create and manipulate Java objects such as strings and arrays and accept Java objects as parameters. JNI can even catch and throw exceptions from native methods and handle these exceptions from the native code or from your Java application. This almost seamless sharing of objects makes it very easy to incorporate native methods in your Java applications.