by Govind Sashadri
The topic of native methods has been called as one of the "final frontiers" of Java, and rightfully so! That's only because this potent feature empowers the Java developer to potentially do just about anything that can be program-atically done on a computer--assuming that a Java implementation exists for it, of course. Native methods allow progammers to extend the reach of Java, by selectively implementing methods in other programming languages like C or C++, when the problem cannot be solved using Java alone.
Prior to the evolution of JDK 1.1, native methods was one of those foreboding,
"rocket-science" Java topics that was seldom discussed and even less understood.
And not without ample reason, either! Then, the very imple- mentation of the native
method interface within JVMs had differed quite drastically, depending on the vendor
implementing it. Due to myriad shortcomings, Sun's native method specification under
JDK 1.02 was superseded by proprietary interfaces like Netscape's Java Runtime Interface
(JRI), Microsoft's Raw Native Interface (RNI), and the Java/COM interface. The lack
of a unified interface meant that the developer had to implement a separate native
method binary for each JVM used, even though they may all have targeted the same
platform!
Given the prevailing situation, it was none too surprising that the native method
specification was thoroughly overhauled under the JDK 1.1. The new and improved version,
called the Java Native Interface (JNI), simplifies matters by presenting a unified
interface for incorporating native methods, irrespective of the JVM used, assuming
that the vendor conforms to the JNI specification for supporting native methods.
JNI was developed following extensive consultations between JavaSoft and other Java
licensees, and bears very close resemblance to Netscape's JRI specification.
NOTE: There is still some confusion as to whether Microsoft will be adopting the JNI standard within their JVM implementation. So far, Microsoft has insisted that they will not be adopting JNI, but rather will support only the RNI and Java/COM interfaces.
Java purists may consider incorporating C or C++ code within Java programs as an act of heresy! Why in the world would someone want to program in anything but Java, given the power and flexibility of the platform-independent JDK? The reasons are many:
Incorporating native methods may bring certain benefits--but there is certainly a price to be paid:
As mentioned before, JVM's full compliance with the JDK 1.1 specification presents the same standard native method interface--the JN--irrespective of the platform. The highlights of the JNI are as follows:
If you still think native methods are the way to go, then it's time to dive into the mysterious waters of the sea of JNI.
NOTE: All of the JNI examples given here are written with the Solaris operating environment in mind, and the native methods are implemented using "C." The examples should work fine on other platforms, as long as the shared library is created properly for that platform.
All native methods are implemented in JNI by closely following a basic six-step program.
Looking at Listing 41.1 you see that the keyword native, within the declaration of the method greet (see below), indicates that it is implemented outside Java, in a different programming language. Additionally, the static block tells Java to load the shared library (libsayhello1.so, in the case of Solaris), within which you can find the actual implementation of the method at runtime.
public class SayHello1 { public native void greet(); static { System.loadLibrary("sayhello1"); } public static void main(String[] args) { new SayHello1().greet(); } }
The main method simply invokes a new instance of the class SayHello1 and invokes the native method greet. Notice that the native method is invoked just like any ordinary instance method.
No surprises here. Simply compile the Java source file using the javac compiler as usual.
javac SayHello1.java
Apply javah--the C header and stub file generator given to you as part of the JDK--on the compiled class file to generate the JNI header file for the class. JNI, unlike the native method interface specification under JDK 1.02, does not make use of stub files.
javah -jni SayHello1
This generates the corresponding header file, SayHello1.h, to the local directory (see Listing 41.2).
NOTE: The generated header file always has the naming convention SomeClassFile.h, where the class SomeClassFile contains the native method declaration, within your Java source.
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class SayHello1*/ #ifndef _Included_SayHello1 #define _Included_SayHello1 #ifdef __cplusplus extern "C" { #endif /* * Class: SayHello1 * Method: greet * Signature: ()V */ JNIEXPORT void JNICALL Java_SayHello1_greet (JNIEnv *, jobject); #ifdef __cplusplus } #endif #endif
The native method can now be developed, after making sure to include the generated header file within the native method implementation file. The native method prototypes can be taken from the header file, to simplify matters (see Listing 41.3).
TIP: There are no restrictions for naming the native method implementation file. But it is better to choose a file name that has some relationship to the actual Java file within which the native method is declared.
#include "SayHello1.h" #include <studio.h> JNIEXPORT# void JNICALL# Java_SayHello1_greet ( JNIEnv *env, jobject this) { printf("Hi folks! Welcome to the netherworld of native methods!\n"); }
This step varies quite a bit depending on the target platform, and requires the availability of a compiler that allows you to create a shared library. For Solaris, a shared object is created quite easily by using the appropriate compiler option. It is important that the path of the standard JDK 1.1 includes files that are properly denoted during the compilation process.
For example, the following command has the effect of creating the shared object libsayhello1.so on Solaris platforms.
cc -G MyHello1.c -I $JAVAHOME/include -I $JAVAHOME/include/solaris -o libsayhello1.so
For Windows 95 and Windows NT, you can create a DLL for the native implementation as follows:
cl MyHello1.c -I$JAVAHOME\include -I$JAVAHOME\include\win32 -Fesayhello1.dll -MD -LD -nologo $JAVAHOME\lib\javai.lib
NOTE: For Windows 95 and Windows NT, you can use any "C" or "C++" compiler that lets you create DLLs. You have to make sure that you include the path of the relevant JDK include files when you create the DLL.
Running the application as:
java SayHello1
produces the output:
Hi folks! Welcome to the netherworld of native methods!
TIP: On Solaris platforms, please note that the environment variable LD_LIBRARY_PATH should contain the path of the newly created shared object.
JNI methods can easily access and even alter the member fields of the invoking object. This is done by using the various JNI accessor functions that are made available to programmers via the interface pointer that is passed by default as the first argument to every JNI method. The JNI interface pointer is of type JNIEnv. The second argument differs on the nature of the native method. For a non-static native method, the argument is a reference to the object, whereas for a static method, it is a reference to its Java class.
Accessing Java object members through the accessor functions is what ensures the portability of the native method implementation. Assuming the vendor of a VM implements the JNI, your native methods should work irrespective of how the Java objects are maintained internally.
Take a look at the function prototype in Listing 41.3.
JNIEXPORT# void JNICALL# Java_SayHello1_greet ( JNIEnv *env, jobject this)
You see that the first two arguments for this method are passed by default by the Java environment. The interface pointer env gives you access to the accessor functions and the object reference this refers to the instance that invoked the native method.
Listing 41.4 is a modification of Listing 41.1. Here, you have added a couple of public fields to class, and you see how they can be accessed and changed in Listing 41.5.
public class SayHello2 { public String aPal = "Java Joe"; public int age=0; public native void greet(); static { System.loadLibrary("sayhello2"); } public void howOld() { System.out.println(aPal + " is " + age + "years old today!"); } public static void main(String[] args) { SayHello2 mySayHello2 = new SayHello2(); mySayHello2.greet(); mySayHello2.howOld(); } }
#include "SayHello2.h" #include <studio.h> JNIEXPORT# void JNICALL# Java_SayHello2_greet ( JNIEnv *env, jobject this) { jfieldID jf; jclass jc; jobject jobj; const jbyte *pal; jint new_age=2; jc = (*env)->GetObjectClass(env, this); jf = (*env)->GetFieldID(env,jc,"aPal","Ljava/lang/String;"); jobj = (*env)->GetObjectField(env,this,jf); pal = (*env)->GetStringUTFChars(env,jobj,0); printf("Hi %s! Welcome to the netherworld of native methods!\n", pal); jf = (*env)->GetFieldID(env,jc,"age","I"); (*env)->SetIntField(env,this,jf,new_age); }
In Listing 41.5, you see how you can not only access the value of a Java field, but can also reset Java field values from within the native method itself.
The Java class file and shared library are created as before, and on execution, you get the output:
Hi Java Joe! Welcome to the netherworld of native methods! Java Joe is 2 years old today!
TROUBLESHOOTING:
Why am I getting all the exception errors? If you get a ton of Java exception errors, the most likely cause could be an incorrect type signature for your GetMethodID() function call. Verify that the signature is correct and recompile your shared library.
Object fields are accessed and used within native methods by following these four steps:
The GetObjectClass is used to determine the class of the object invoking the native method. You see that before the value of an object member field is accessed, you first need to obtain a fieldID for it. The GetFieldID accessor function needs to know the exact field name, as well as its type signature. Table 41.1 denotes the possible Java VM type signatures.
Type Signature | Java Type |
Z | boolean |
B | byte |
C | char |
S | short |
I | int |
J | long |
F | float |
D | double |
Lfully-qualified-class; | fully-qualified-class |
[type | type[] |
(arg-types)ret-type | method type |
The JNI interface pointer provides many functions to access the actual member field, depending on its type.
The various GetField() functions available are shown in Table 41.2.
GetField Routine Name | Native Type | Java Type |
GetObjectField() | jobject | Object |
GetBooleanField() | jboolean | boolean |
GetByteField() | jbyte | byte |
GetCharField() | jchar | char |
GetShortField() | jshort | short |
GetIntField() | jint | int |
GetLongField() | jlong | long |
GetFloatField() | jfloat | float |
GetDoubleField() | jdouble | double |
Lastly, you see that object fields can have their values altered by making use of the appropriate SetField() function. You make use of the SetIntField() function since the target field set is of type int. As before, it is important to declare the correct native type to store the value of the native field that is passed to the Java environment.
The various SetField() functions available are shown in Table 41.3.
SetField Routine Name | Native Type | Java Type |
SetObjectField() | jobject | Object |
SetBooleanField() | jboolean | boolean |
SetByteField() | jbyte | byte |
SetCharField() | jchar | char |
SetShortField() | jshort | short |
SetIntField() | jint | int |
SetLongField() | jlong | long |
SetFloatField() | jfloat | float |
SetDoubleField() | jdouble | double |
Java Type | Native Type | Description |
boolean | jboolean | unsigned 8 bits |
byte | jbyte | signed 8 bits |
char | jchar | unsigned 16 bits |
short | jshort | signed 16 bits |
int | jint | signed 32 bits |
long | jlong | signed 64 bits |
float | jfloat | 32 bits |
double | jdouble | 64 bits |
void | void | N/A |
The process of invoking a Java method from within a native method is a little complicated. But, using the techniques demonstrated below, you will also be able to call other native methods from within a native method. In fact, a native method can even call itself recursively, if need be. Listing 41.6 is a good example of how a Java method can be invoked from a native method, with arguments passed to it. Listing 41.7 shows the "C" implementation and demonstrates the actual JNI syntax for accessing Java methods from native methods.
public class SayHello3 { public String rectArea(int l, int b ) { return "The area of the rectangle is " + l*b + " units" ; } public native void calc(); static { System.loadLibrary("sayhello3"); } public static void main(String[] args) { new SayHello3().calc(); } }
#include "SayHello3.h" #include <studio.h> JNIEXPORT# void JNICALL# Java_SayHello3_greet ( JNIEnv *env, jobject this) { jmethodID jm; jclass jc; jobject jo; const jbyte *area; jint x,y ; x =20; y =20; jc = (*env)->GetObjectClass(env, this); jm = (*env)->GetMethodID(env,jc,"rectArea","(II)Ljava/lang/ String;",x,y); jo= (*env)->CallObjectMethod(env,this,jm,x,y); area = (*env)->GetStringUTFChars(env,jo,0); printf("%s\n",area); }
The Java class file and shared library is created as before, and on execution, you get the output:
The Area of the Rectangle is 400 units
Object methods can be called from within native methods by following these five steps:
Looking at the type signatures in Table 41.1, you can deduce that the type signature for the Java method rectArea() in Listing 41.6 is (II)Ljava/lang/String; This is because the method takes in two ints and returns back a String object.
The various CallMethod() accessor functions available are shown in Table 41.5.
CallMethod Routine Name | Native Type | Java Type |
CallVoidMethod() | void | void |
CallObjectMethod() | jobject | Object |
CallBooleanMethod() | jboolean | boolean |
CallByteMethod() | jbyte | byte |
CallCharMethod() | jchar | char |
CallShortMethod() | jshort | short |
CallIntMethod() | jint | int |
CallLongMethod() | jlong | long |
CallFloatMethod() | jfloat | float |
CallDoubleMethod() | jdouble | double |
Thus far you have seen how you've worked with functions that work on instance fields and methods. Static fields and methods of Java objects can be accessed in much the same way by using some of the special functions shown below.
GetStaticFieldID()--The signature of the field accessed can be obtained from Table 41.1. Once the signature has been obtained, the GetStaticFieldID() is used exactly as the GetFieldID() function.
GetStaticField()--Table 41.6 shows the various GetStaticField() functions that can be used to access static fields. The selected function will depend upon the data type of the Java field accessed.
GetStaticField Routine Name | Native Type | Java Type |
GetStaticObjectField() | jobject | Object |
GetStaticBooleanField() | jboolean | boolean |
GetStaticByteField() | jbyte | byte |
GetStaticCharField() | jchar | char |
GetStaticShortField() | jshort | short |
GetStaticIntField() | jint | int |
GetStaticLongField() | jlong | long |
GetStaticFloatField() | jfloat | float |
GetStaticDoubleField() | jdouble | double |
SetStaticField()--Table 41.7 shows the available accessor functions that can be used to modify the value of static Java fields. The chosen function will depend on the type of the target static field.
SetStaticField Routine Name | Native Type | Java Type |
SetStaticObjectField() | jobject | Object |
SetStaticBooleanField() | jboolean | boolean |
SetStaticByteField() | jbyte | byte |
SetStaticCharField() | jchar | char |
SetStaticShortField() | jshort | short |
SetStaticIntField() | jint | int |
SetStaticLongField() | jlong | long |
SetStaticFloatField() | jfloat | float |
SetStaticDoubleField() | jdouble | double |
Static Java methods can be invoked just like instance methods, but by making use of the functions GetStaticMethodID() and CallStaticMethod().
GetStaticMethodID()--The usage of this function is identical to that of GetMethodID(). The developer will have to determine the method signature using Table 41.1, before invoking the function.
CallStaticMethod--Table 41.8 shows a listing of the available CallStaticMethod() accessor functions. The developer will have to choose the appropriate one based on the Java-type of the data returned from the function call.
CallStaticMethod Routine Name | Native Type | Java Type |
CallStaticVoidMethod() | void | void |
CallStaticObjectMethod() | jobject | Object |
CallStaticBooleanMethod() | jboolean | boolean |
CalStaticByteMethod() | jbyte | byte |
CallStaticCharMethod() | jchar | char |
CallStaticShortMethod() | jshort | short |
CalStaticIntMethod() | jint | int |
CallStaticLongMethod() | jlong | long |
CallStaticFloatMethod() | jfloat | float |
JNI methods can throw exceptions that can then be handled within the invoking Java object. Following are some of the important functions that are available within the JNI interface pointer for exception handling:
Throw
jint Throw(JNIEnv *env, jthrowable obj);
causes a java.lang.Throwable object to be thrown.
Parameters:
env--the JNI interface pointer obj--a java.lang.Throwable object
Returns:
0--on success
negative number--on failure
ThrowNew
jint ThrowNew(JNIEnv *env, jclass classz, const char *msg);
initializes and constructs a new exception object instance of type classz with the diagnostic msg and throws it.
Where:
env--the JNI interface pointer classz--a subclass of java.lang.Throwable
msg--a diagnostic message for the class constructor
Returns:
0--on success
negative number--on failure
FatalError
void FatalError(JNIEnv *env, char *msg);
raises an unrecoverable fatal error
Where:
env--the JNI interface pointer
msg--an error message