TOC
BACK
FORWARD
HOME

Java 1.1 Unleashed

- 32 -
Integrating Native Code with the Native Method Interface

by Stephen Ingram

IN THIS CHAPTER

  • The Native Method Interface
  • Native Methods from the Java Side
  • Native Methods from the Library Side
  • Putting It All Together
  • The Invocation API


This chapter covers techniques for integrating nonJava code into your Java applications. The initial release of the JDK provided a mechanism for calling methods from C language libraries, but it was largely undocumented and extremely complicated. With the advent of Java 1.1, however, Sun decided to formalize and simplify the entire process. The result of all this work is embodied in the Java Native Method Interface (JNI). This chapter teaches you the five core techniques of the JNI:

  • Working with Java arrays

  • Working with Java strings

  • Reading and writing Java Object fields

  • Calling Java Object methods

  • Throwing exceptions

These techniques are used to integrate both new and existing code libraries into your Java applications. In addition, this chapter introduces you to a powerful new feature that allows a standalone program to dynamically load and use Java classes. The Java Invocation API provides these capabilities. You'll learn how to use the API to add Java access to existing applications.

The Native Method Interface

Historically, native method programming required an intimate knowledge of the internal structures and representations of the virtual machine. Because Java was envisioned as a platform-independent system, Sun did not specify the internal design of a compliant Java virtual machine. Only external capabilities such as the class file format and bytecodes were rigidly dictated. This allowed vendors considerable latitude in the actual implementation of the Java runtime system.

The popularity of Java has caused a number of vendors to market their own Java virtual machine implementations. In fact, Sun's VM is a reference implementation, not a standard. Obviously, the internal structures of each vendor's implementation differ, so a standard methodology for accessing internal features was required. The earlier reliance on internal structures does not allow a native library to run on all implementations; native libraries for the Windows platform should operate correctly with any vendor's Windows VM. With this goal in mind, Sun's designers invented the Java Native Method Interface.

Overall Architecture

Figure 32.1 contrasts the general layout of the Java 1.0 and Java 1.1 runtime systems. The left side of the figure shows the old-style access; the right side shows the new-style access. Notice how Java 1.1 uses the interface layer to isolate the VM internals from the native library.

Figure 32.1.

Java 1.0 and Java 1.1 runtime architectures.

The Java Native Method Interface (JNI) is passed into all native libraries as a C pointer. It is actually a pointer to a pointer. The interface is structured much like a C++ virtual function table. This is the biggest change to old-style native method programming. Previously, the Java VM called your native method and passed only object pointers. The VM functions were accessed directly through shared library calls. This meant that the Java VM shared library had to be linked into the native library. With the JNI, all that has changed. Now, the Java VM calls your native method and passes an additional item: the JNI pointer. This extra parameter eliminates your having to link with the shared library. Every function you may need is now embedded in the JNI pointer.

The interface consists of a series of function calls that native methods use to access and exchange data with the Java runtime system. Before exploring the specifics of the interface, you should be fully introduced to native methods within Java classes.

Native Methods from the Java Side

Native methods within a Java class are very simple. Any Java method can be transformed into a native method--simply delete the method body, add a semicolon at the end, and add as a prefix the native keyword. Consider the following Java method:

public int myMethod(byte[] data)
{
    ...
}

This method becomes a native method that looks like this:

public native int myMethod(byte[] data);

Where is the method body implemented? In a Java-called native library that is loaded into Java at run time. The class of this example method has to cause the library to be loaded. The best way to accomplish the load is to add a static initializer to the class:

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

Static code blocks are executed once by the system when the class is first introduced. Any operations can be specified, but library loading is the most common use of the block. If the static block fails, the class is not loaded. This ensures that no native methods are executed without their underlying libraries. What happens if the class doesn't load the native library? An UnsatisfiedLinkError exception is generated when the first native method is called.

The Java VM appends the correct library extension to the name you provide. In the case of Microsoft Windows, Java adds .dll; for UNIX, it adds .so.

That's all there is to Java-side native methods. All the complexity is hidden within the native library. A native method appears to Java just like all other real Java methods. In fact, all the Java modifiers (public, private, and so forth) apply to native methods as well.

Rather than encapsulating a complex native library, this chapter develops a straightforward library that attempts to exercise the major features of the Java Native Method Interface. Listing 32.1 contains the sample class that is developed in the remainder of this chapter. You can also find this file on the CD-ROM that accompanies this book.

Listing 32.1. Demonstration.java: The sample library.


public class Demonstration
{
    public String[] strs;
    public int[] vals;

    public native String[] createStrs(int siz);
    public native int[] createVals(int siz);
    public native void sortStrs();
    public native void reverseArray(int[] val);
    public native void genException();

    public void outputVals(String mess)
    {
        System.out.println(mess);
        for (int x = 0; x < vals.length; x++)
        {
            System.out.println("vals[" + x + "] = " + vals[x]);
        }
    }

    public void outputStrs(String mess)
    {
        System.out.println(mess);
        for (int x = 0; x < strs.length; x++)
        {
            System.out.println("strs[" + x + "] = " + strs[x]);
        }
    }

    // a static method that provides a convenient entry point
    // for external processes (who are using the Invocation API)
    public static void start()
    {
        // Create the Demonstration class
        Demonstration demo = new Demonstration();

        demo.vals = demo.createVals(3);
        demo.outputVals("After Creation");
        demo.reverseArray(demo.vals);
        demo.outputVals("After reversing");

        demo.strs = demo.createStrs(3);
        demo.outputStrs("After creation");
        demo.sortStrs();
        demo.outputStrs("After sorting");
        demo.genException();
    }

    public static void main(String args[])
    {
        start();
    }

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

}


There are five native methods in this Demonstration class. The first two are used to create and fill arrays. The created arrays are stored within the class. The next two native methods alter the array data--either by sorting or by simple member reversal. To facilitate monitoring, two class methods print the contents of the arrays. The final native method generates an exception. The native methods within this class allow each of the five core techniques of the JNI to be demonstrated.

After compiling the class, the first step is to run javah on the class.

Using the javah Tool

javah is the tool you use to generate C header files for Java classes. Here's how you use it:

javah [options] class

Table 32.1 briefly lists the options available for use with javah. Java 1.1 adds a new option to accommodate the new native interface. You must use the -jni option to create the newer native interface header. Although old-style native methods are still supported by the javah tool, there are no guarantees for future compatibility. By default, javah creates a C header (.h) file in the current directory for each class listed on the command line. Class names are specified without the trailing .class. Therefore, to generate the header for SomeName.class, use the following command:

javah -jni SomeName

Table 32.1. The options for the javah tool.

Option Description
-jni Creates a JNI header file
-verbose Causes progress strings to be displayed
-version Displays the version of javah
-o outputfile Overrides default file creation; uses only this filename
-d directory Overrides placement of output in current directory
-td tempdirectory Overrides default temporary directory use
-stubs Creates a C code module instead of a header module
-classpath path Overrides the default class path


NOTE: If the class you want is within a package, you must specify the package name along with the class name: javah java.net.Socket. In addition, javah prefixes the package name to the output filename: java_net_Socket.h.

To run javah on the compiled Demonstration class, use this command:

javah -jni Demonstration

The result of this command is the C header file Demonstration.h (see Listing 32.2; the header file can also be found on the CD-ROM that accompanies this book). With this last action, you made the jump to the native library side of the fence.

Listing 32.2. The Demonstration.h file.


/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class Demonstration */

#ifndef _Included_Demonstration
#define _Included_Demonstration
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     Demonstration
 * Method:    createStrs
 * Signature: (I)[Ljava/lang/String;
 */
JNIEXPORT jobjectArray JNICALL Java_Demonstration_createStrs
  (JNIEnv *, jobject, jint);

/*
 * Class:     Demonstration
 * Method:    createVals
 * Signature: (I)[I
 */
JNIEXPORT jintArray JNICALL Java_Demonstration_createVals
  (JNIEnv *, jobject, jint);

/*
 * Class:     Demonstration
 * Method:    sortStrs
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_Demonstration_sortStrs
  (JNIEnv *, jobject);

/*
 * Class:     Demonstration
 * Method:    reverseArray
 * Signature: ([I)V
 */
JNIEXPORT void JNICALL Java_Demonstration_reverseArray
  (JNIEnv *, jobject, jintArray);

/*
 * Class:     Demonstration
 * Method:    genException
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_Demonstration_genException
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

Native Methods from the Library Side

The Demonstration.h header file created by javah provides you with the function prototypes for your native methods and also tells you what to name your functions so that Java can find them at run time. Normally, the function name is created by prefixing Java_ to the class and method names. If you have an overloaded method, the derived name also includes the argument list. Adding another createStrs() function with different arguments to the Demonstration class causes the derived name to look like this:

JNIEXPORT jobjectArray JNICALL Java_Demonstration_createStrs_I
  (JNIEnv *, jobject, jint);

This naming convention is used to allow Java to identify the correct implementation of createStrs() depending on the argument list of the caller.

JNI Data Types

To maintain consistency across multiple vendors, the JNI uses nine specific data types, contained in the JNI header file. Table 32.2 shows all these data types and their Java equivalents.

Table 32.2. JNI representation of Java basic types.

Java Type JNI Representation
boolean jboolean
byte jbyte
char jchar
short jshort
int jint
long jlong
float jfloat
double jdouble
void void


In addition to these basic types, Java objects are addressed and stored as pointers:

struct _jobject;

typedef struct _jobject *jobject;
typedef jobject jclass;
typedef jobject jthrowable;
typedef jobject jstring;
typedef jobject jarray;
typedef jarray jbooleanArray;
typedef jarray jbyteArray;
typedef jarray jcharArray;
typedef jarray jshortArray;
typedef jarray jintArray;
typedef jarray jlongArray;
typedef jarray jfloatArray;
typedef jarray jdoubleArray;
typedef jarray jobjectArray;

You may be wondering what the difference is between jobject and jclass. A class pointer (jclass) is a description or template for a class. It is used to construct a new object (jobject) of that class.

The second argument of a native method is of type jobject. This is a pointer to the Java object under which this method is acting. For you C++ programmers, jobject is equivalent to the this pointer. If the native method had been a static function, the second argument would have been of type jclass.

All native methods have as their first argument the Java Native Method Interface pointer, which is described in the next section.

Java Native Method Interface Pointer

With version 1.1--for the first time--the JDK now works with C++ directly! JNIEnv is a pointer to an interface structure which is defined differently depending on whether you are using standard C or C++. Both definitions are contained in the Java include file, jni.h. The JNIEnv structure provides all the interaction with Java; you will make constant use of its members. For standard C files, JNIEnv is defined as follows:


typedef const struct JNINativeInterface *JNIEnv;
struct JNINativeInterface {
    void *reserved0;
    void *reserved1;
    void *reserved2;

    void *reserved3;
    jint (JNICALL *GetVersion)(JNIEnv *env);
    jclass (JNICALL *DefineClass)
      (JNIEnv *env, const char *name, jobject loader, const jbyte *buf,
       jsize len);
    jclass (JNICALL *FindClass)
      (JNIEnv *env, const char *name);
     ...
};

In C++, JNIEnv is defined as a C++ structure with inline functions:

typedef JNIEnv_ JNIEnv;
struct JNIEnv_ {
    const struct JNINativeInterface *functions;
    void *reserved0;
    void *reserved1[6];
    jint GetVersion() {
        return functions->GetVersion(this);
    }
    jclass DefineClass(const char *name, jobject loader, const jbyte *buf,
               jsize len) {
        return functions->DefineClass(this, name, loader, buf, len);
    }
    jclass FindClass(const char *name) {
        return functions->FindClass(this, name);
    }
         ...

};


NOTE: You may be wondering about the three reserved pointers at the top of the JNIEnv interface. These are present to allow the interface to conform to Microsoft's COM object model. Once COM cross-platform support is implemented, the JNI can become a COM interface to the Java VM!

The main advantage of this dual definition is in the calling convention used to invoke the interface functions. For standard C, you must use the pointer and call through the interface, like this:

(*env)->FindClass(env, "java/lang/String");

In C++, the same invocation can be accomplished in a much cleaner fashion:

env->FindClass("java/lang/String");

The extra calling complexity is hidden by the inline function. Ultimately, the C++ inline function resolves to exactly the same reference as its standard C cousin. The underlying functionality of the interface is not compromised in any way by the more streamlined C++ style.

Native Interface Functions

The functions available in the JNIEnv interface are too numerous to list in this chapter. I would rather convey the underlying concepts of the interface so that you can then understand any of the individual functions. The purpose of the JNI is to isolate the details of the Java VM from the native code. By providing a middle layer, the JNI allows native methods to run on any vendor's Java VM. It will always help your understanding if you keep the JNI's middle-layer interface nature in mind.

Perhaps the best place to start is with the representation of Java objects.

Object References

Scalar quantities, such as integers and characters, are copied between the Java VM and the native library. In contrast, Java objects are passed by reference. The native method is literally working with the same object as the Java VM. Allowing the Java garbage collector to function seamlessly with native methods is paramount. The interface solves this problem by dividing all object references into two distinct types: global and local.

A local reference has duration only for the life of the native method call and is freed when the native method returns. All arguments passed to, and returned from, a native method are local references. Global references must be explicitly freed. Any local reference can be turned into a global reference by using the interface function NewGlobalRef(). Both types of references are legal wherever a reference is needed. The only difference is in their scope of existence.

To facilitate efficient garbage collection, the interface contains a function that allows a native method to notify the VM that it no longer needs a particular local reference. Usually, this functionality is used only with a native method that is performing a particularly long computation. Rather than tie up the object for the entire length of the method, the native code can notify the VM that it no longer needs a local reference and the VM is free to garbage collect it before the method returns. Interface functions DeleteGlobalRef() and DeleteLocalRef() accomplish the removal notifications.

The JNI's ID System

I like to think of the JNI as having a distinct ID system for managing its parts. Whenever you want to interact with an object, you need an object reference (jobject). Similarly, whenever you want to interact with a specific field or method within a class, you need a field ID (jfieldID) or method ID (jmethodID). Identifying a specific class is accomplished with a class ID (jclass).

Together, these four types allow the native code to uniquely identify any individual feature of the Java hierarchy.

Java Arrays

The first major point to remember when dealing with Java arrays is that arrays are themselves Java objects. Arrays do have their own identifier (jobjectArray), but that is only to aid in readability. A jobjectArray reference can be passed and used by any routine expecting a jobject reference.

All Java arrays have a length parameter. The JNI provides the function GetArrayLength() to access any array's size:

jsize GetArrayLength(JNIEnv *env, jarray array);

To address the individual members of array objects, the JNI contains two major groups of functions. The group you use depends on the type of data held by the array. The first group allows access to arrays of Java objects or references. The second group allows access to arrays of scalar quantities.

In the case of arrays of objects, each array index can be set or retrieved by an interface function:

jobject GetObjectArrayElement(JNIEnv *env, jarray array, jsize index);
void SetObjectArrayElement(JNIEnv *env, jarray array, jsize index, jobject value);

Accessing each index with a function is very inefficient when dealing with a scalar quantity such as integers. Performing matrix calculations is horribly slow. To solve this problem, the JNI provides a set of functions that allow a scalar array to be accessed in the native address space.

Each scalar type has functions for manipulating arrays of that type. The following statement gives the calling format:

NativeType GetArrayElements(JNIEnv *env, jarray array, jboolean *isCopy);

There is no actual function called GetArrayElements(). Instead, there are variants for each scalar type. Table 32.3 lists all the flavors of GetArrayElements().

The third argument (isCopy), is a boolean set by the VM depending on whether the array was originally stored as a C array. If the data of the Java array is stored contiguously, a pointer to that data is returned and isCopy is set to false. If, however, the internal storage is not contiguous, the VM makes a copy of the actual data and sets isCopy to true. The significance of this flag is that if the flag is false, you know you are manipulating the actual array data. Any changes you make are permanent changes. If, on the other hand, you are working with a copy, your changes can be released without saving.

Table 32.3. GetArrayElements() function types.

Function Native Return Type Java Array Type
GetBooleanArrayElements() jboolean * boolean[]
GetByteArrayElements() jbyte * byte[]
GetCharArrayElements() jchar * char[]
GetShortArrayElements() jshort * short[]
GetIntArrayElements() jint * int[]
GetLongArrayElements() jlong * long[]
GetFloatArrayElements() jfloat * float[]
GetDoubleArrayElements() jdouble * double[]


Releasing the local copy back to the Java object is accomplished with the various versions of ReleaseArrayElements(). This is its calling format:

void ReleaseArrayElements(JNIEnv *env, jarray array, NativeType elems, jint mode);

Again, the actual function name is specific to each scalar type. Table 32.4 lists the types of release functions. The fourth argument (mode) to ReleaseArrayElements() controls the release mode. It has three possible values: 0 Copy back the data and release the local storage

JNI_COMMIT Copy back the data but do not release the storage

JNI_ABORT Release the storage without copying back the data

Obviously, if the local data is not a copy, the mode parameter has no effect.

Table 32.4. ReleaseArrayElements() function types.

Function Native Return Type Java Array Type
ReleaseBooleanArrayElements() jboolean * boolean[]
ReleaseByteArrayElements() jbyte * byte[]
ReleaseCharArrayElements() jchar * char[]
ReleaseShortArrayElements() jshort * short[]
ReleaseIntArrayElements() jint * int[]
ReleaseLongArrayElements() jlong * long[]
ReleaseFloatArrayElements() jfloat * float[]
ReleaseDoubleArrayElements() jdouble * double[]


NOTE: If you want to work with scalar array data in an unobtrusive manner, the JNI provides a second set of functions that allow the scalar array members to be copied into local storage allocated and managed by the native method. GetArrayRegion() and SetArrayRegion() operate on a subset of an array and use a locally allocated buffer.

Native methods can also create a new Java array. The NewArray() functions perform the work:


jarray NewObjectArray(JNIEnv *env, jsize length, jclass elementClass,
                      jobject initialElement);
jarray NewScalarArray(JNIEnv *env, jsize length);

Enough theory. It's time to apply what you have learned. You should now have enough knowledge to implement the createVals() and reverseArray() native methods from the Demonstration class. Listing 32.3 shows the completed methods; the code is also located on the CD-ROM that accompanies this book. In this example, the methods manipulate an integer array, so the scalar array functions are used.

Listing 32.3. The native methods createVals() and reverseArray().


/*
 * Class:     Demonstration
 * Method:    createVals
 * Signature: (I)[I
 */
JNIEXPORT jintArray JNICALL Java_Demonstration_createVals
  (JNIEnv *env, jobject DemoObj, jint len)
{
    jintArray RetArray;
    int x;
    jint *localArray;

    RetArray = env->NewIntArray(len);
    localArray = env->GetIntArrayElements(RetArray, NULL);
    for ( x = 0; x < len; x++)
        localArray[x] = len - x - 1;
    env->ReleaseIntArrayElements(RetArray, localArray, 0);
    return RetArray;
}


/*
 * Class:     Demonstration
 * Method:    reverseArray
 * Signature: ([I)V
 */
JNIEXPORT void JNICALL Java_Demonstration_reverseArray
  (JNIEnv *env, jobject DemoObj, jintArray vals)
{
        jint x, temp;
        jsize len;
        jboolean isCopy;
        jint *localArray;

        len = env->GetArrayLength(vals);
        localArray = env->GetIntArrayElements(vals, &isCopy);
        for (x = 0; x < len/2; x++)
        {
            temp = localArray[x];
            localArray[x] = localArray[len - x - 1];
            localArray[len - x - 1] = temp;
        }
        env->ReleaseIntArrayElements(vals, localArray, 0);

}

createVals()
uses NewArray() to allocate the integer array. Because NewArray() creates a proper Java object, local access to the data must be acquired using GetIntArrayElements(). After the array has been initialized, the local elements are released back to the Java VM.

Reversing the array is similar to creating it. First, the array's length is determined. After the length is known, the array's contents can be acquired and manipulated as a standard C array. Notice that the isCopy parameter is not required. createVals() passes null instead of a pointer because it doesn't care whether the data is a copy. reverseArray() passes a valid pointer, although it never uses the information. Either technique is valid.

Java Strings

Unlike C, Java treats strings as first-class objects. In C, a string is nothing more than a zero-terminated array of characters. This dichotomy is addressed by several string functions within the JNI.

String length is determined in a manner similar to the way array length is determined:

jsize GetStringLength(JNIEnv *env, jstring string);

Because Java supports strings in Unicode format, access to string data can be acquired in two distinct ways. Most C string routines can't work with Unicode, so the JNI provides translation functions that are more natural for C to use. Table 32.5 lists the string functions with brief comments. It is important to remember that strings in Java are immutable. This means that, unlike an array, you cannot change the data within a String object.

String data in Java is stored in UTF-8 format. This format represents all characters up to 0x7F (hexadecimal) in a single byte. Characters above 0x7F use an encoding mechanism that may take up to three bytes of storage. Standard ASCII data looks like C strings, but be aware of Unicode representation in international situations. Characters from 0x80 to 0x7FF consume two bytes, while characters from 0x800 to 0xFFFF use three bytes.

Table 32.5. String interface functions.

Function Description
NewString() Creates a String object from a jchar array (Unicode)
GetStringLength() Returns the number of jchars in a string (Unicode)
GetStringChars() Gets a jchar array of string characters (Unicode)
ReleaseStringChars() Releases an acquired jchar array (Unicode)
GetStringUTFLength() Returns the number of bytes in a string (UTF-8)
NewStringUTF() Creates a String object from a byte array (UTF-8)
GetStringUTFChars() Gets a byte array of string characters (UTF-8)
ReleaseStringUTFChar() Releases an acquired byte array (UTF-8)


Listing 32.4 shows the code for creating an array of strings in the Demonstration class. The code is also located on the CD-ROM that accompanies this book.

Listing 32.4. The string array creation routine.


/*
 * Class:     Demonstration
 * Method:    createStrs
 * Signature: (I)[Ljava/lang/String;
 */
JNIEXPORT jobjectArray JNICALL Java_Demonstration_createStrs
  (JNIEnv *env, jobject DemoObj, jint len)
{
    jobjectArray RetArray;
    jobject StringObj;
    jclass StringClass;
    int x;
    char str[80];

    StringClass = env->FindClass("java/lang/String");
    RetArray = env->NewObjectArray(len, StringClass, NULL);
    for (x = 0; x < len; x++)
    {
        sprintf(str, "This is string #%04d", len - x - 1);
        StringObj = env->NewStringUTF(str);
        env->SetObjectArrayElement(RetArray, x, StringObj);
    }
    return RetArray;

}

This routine is considerably more complex than the integer array creation routine. To create arrays of objects, the class type of the array data must be known. The first step of the routine is to acquire a jclass reference to the String class. Once the array is constructed, each individual String object must be created and inserted into the array. Once each object is created, SetObjectArrayElement() is used to place the string into the array.

The string sort method is supposed to access the string array directly from the Demonstration object. To accomplish this, however, you must first learn how to access Java fields, as described in the following section.

Reading and Writing Java Object Fields

The variables within a Java object are referred to as fields. Before you can access an object field, you must first have a field ID. GetFieldID() takes a field name and signature and returns the variable's ID. The trick to the whole procedure is determining the field signature. Table 32.6 lays out the characters and their signature meanings.

Table 32.6. Signature symbols.

Type Signature Character
array [
byte B
char C
class L
end of class ;
float F
double D
function (
end of function )
int I
long J
short S
void V
boolean Z


Using the information in Table 32.6, construct the field signatures of these two public variables within the Demonstration class:

public String[] strs;
public int[] vals;

The first variable is an array of strings, so its signature is [Ljava/lang/String;. The second variable is an array of integers, so its signature is [I.


NOTE: Signatures can be very confusing. The viewer tool described in Chapter 33, "Java Under the Hood: Inside the Virtual Machine," is very useful for displaying field and method signatures. Additionally, the JDK's javap tool can be used with the -s option to reveal signatures for a given class.

GetFieldID() requires one additional piece of information. It has to know the class that contains the desired field. If you know only the class name, use FindClass() to obtain the rest of this data. Most of the time, you already have an object reference to the class that contains the field. In this case, use GetObjectClass() to acquire the jclass reference of an existing object. Using the class, name, and signature yields the field ID:


demoClass = env->GetObjectClass(DemoObj);
strsArrayID = env->GetFieldID(demoClass, "strs", "[Ljava/lang/String;");

The field ID is valid for any instance of the class. If you have two instances of Demonstration objects, the same field ID can be used to reference the strs array in both objects.

Once the field ID is known, use the various types of GetField() functions to access the contents of the variable. Table 32.7 lists the various flavors of the GetField() function.

Table 32.7. The varieties of the GetField() function.

Function Return Type Field 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
Accessing the strs field in the Demonstration class is simple--once the field ID is known:

strsArray = env->GetObjectField(DemoObj, strsArrayID);

As you do when you read the field contents, when you set the field contents, you also use the field ID:


env->SetObjectField(DemoObj, strsArrayID, newArrayObj);

Table 32.8 lists the SetField() functions.

Table 32.8. The varieties of the SetField() function.

Function Return Type Field 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

Calling Java Object Methods

Like field operations, method operations use the concept of the central prominence of the method ID. As you do with fields, you acquire the method ID by using a class, name, and signature. In fact, the signature symbols in Table 32.6 are used to construct both field and method signatures.

There is a whole stable of functions for invoking Java methods. First you must recognize whether the target method is static. If it is not static, you must decide whether you are calling a virtual function or a specific function. Normal Java calls are always virtual. If a class overrides a function, you always call that new version when you have an object of that class or one of its descendants. If, however, you want to call a specific version of the function, you must issue a nonvirtual method invocation. This situation can arise when you want to execute the base class version of a virtual function but you have a descendant class object reference.

Once you decide which type of method invocation to use, you still must choose how you want to pass method arguments. There are three ways to pass arguments:

  • Add them to the end of the method invocation

  • Pass a va_arg structure

  • Pass a jvalue array of arguments

The approach you use is based on personal preference. I favor the first method because it makes for the cleanest code. The generic calling formats are as follows:

  • CallMethod(jobject obj, jmethodID methodID, ...);

  • CallMethodV(jobject obj, jmethodID methodID, va_list args);

  • CallMethodA(jobject obj, jmethodID methodID, jvalue *args);

Each return type has its own representative function. Table 32.9 lists the functions for invoking a virtual method.

Table 32.9. Virtual method invocations based on return type.

Function Method Return Type
CallVoidMethod() jvoid
CallObjectMethod() jobject
CallBooleanMethod() jboolean
CallByteMethod() jbyte
CallCharMethod() jchar
CallShortMethod() jshort
CallIntMethod() jint
CallLongMethod() jlong
CallFloatMethod() jfloat
CallDoubleMethod() jdouble


At this point, you are probably feeling a bit overwhelmed. That is understandable. Maybe an example would clear things up? Let's return to the sortStrs problem.

For simplicity, I chose an insertion sort. The sort compares two String objects. If you pull out your API reference, you will notice that the String class has a compareTo(String) function that seems tailor made for our purpose. The next step is to determine the method's signature. The method prototype is shown here:

public int compareTo(String anotherString);

By using Table 32.6, we determine that the method signature is (Ljava/lang/String;)I. Listing 32.5 shows the code to implement the sortStrs() native method; the code is also located on the accompanying CD-ROM.

Listing 32.5. The sortStrs() method from Demonstration.cpp.


/*
 * Class:     Demonstration
 * Method:    sortStrs
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_Demonstration_sortStrs
  (JNIEnv *env, jobject DemoObj)
{
    jobjectArray valsArray;
    jclass demoClass;
    jfieldID valsArrayID;
    jclass StringClass;
    jmethodID compID;

    demoClass = env->GetObjectClass(DemoObj);
    valsArrayID = env->GetFieldID(demoClass, "strs", "[Ljava/lang/String;");
    if ( valsArrayID == NULL ) return;

    valsArray = (jobjectArray)env->GetObjectField(DemoObj, valsArrayID);
    if ( valsArray == NULL ) return;

    StringClass = env->FindClass("java/lang/String");
    if (StringClass == NULL) return;

    compID = env->GetMethodID(StringClass, "compareTo", "(Ljava/lang/String;)I");
    if (compID == NULL) return;

    insertSort(env, valsArray, compID);
}

void insertSort(JNIEnv *env, jobjectArray valsArray, jmethodID compID)
{
    int i, j, len;
    jobject tmp, cobj, pobj;

    len = env->GetArrayLength(valsArray);

    for ( i = 1; i < len; i++ )
    {
        cobj = env->GetObjectArrayElement(valsArray, i);
        pobj = env->GetObjectArrayElement(valsArray, i - 1);
        if (env->CallIntMethod(cobj, compID, pobj) < 0)
        {
            for ( j = i - 1; j >= 0; j-- )
            {
                tmp = env->GetObjectArrayElement(valsArray, j);
                env->SetObjectArrayElement(valsArray, j + 1, tmp);
                if ( j == 0 ) break;
                tmp = env->GetObjectArrayElement(valsArray, j - 1);
                if ( env->CallIntMethod(tmp, compID, cobj) < 0 ) break;
            }
            env->SetObjectArrayElement(valsArray, j, cobj);
        }
    }

}

Most of the work is done in the insertSort() function. Essentially, the routine parses the entire array, pulling each String object out and using compareTo() calls to insert the strings into the proper location. Although it would be much more efficient to create a local copy of the array, I wanted to exercise as many array calls as possible.

Throwing Exceptions

Java's rich support for exceptions also extends into the native method realm. A number of interface functions allow native code to issue, clear, retrieve, and describe exceptions. Table 32.10 lists all the exception functions for the JNI.

Table 32.10. The JNI exception functions.

Function Description
Throw() Throws an exception object
ThrowNew() Creates a new exception object and throws it
ExceptionOccurred() Determines whether an exception is pending and retrieves it
ExceptionDescribe() Prints a description as a debugging convenience
ExceptionClear() Clears a pending exception
FatalError() Raises a fatal error (stops execution)


For the final native method in the Demonstration class, we have to generate an exception. Listing 32.6 displays the method used to do so (the code is located on the accompanying CD-ROM).

Listing 32.6. Generating an exception in native code.


/*
 * Class:     Demonstration
 * Method:    genException
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_Demonstration_genException
  (JNIEnv *env, jobject DemoObj)
{
    jclass exceptClass;

    exceptClass = env->FindClass("java/lang/Exception");
    env->ThrowNew(exceptClass, "genException()");

}

To generate an exception, you have to know only the class name of the exception you want to throw.

Putting It All Together

The final step is to actually create the native library. I used Microsoft Visual C++ 4.0 in a Windows 95/NT environment for this chapter. The JNI is generic enough to allow all the code in this chapter to be used on a different platform (such as many UNIX variants). That is one of the big advantages to the JNI's middle-layer status: All platforms are capable of hosting the interface.

To create the library, I used the following command-line statement:

cl Demonstration.cpp -FeDemonstration.dll -MD -LD

Invoking this command creates the library Demonstration.dll. You can test out the class by running the Demonstration class as an application:

java Demonstration

Before showing you the class output, I want to go one step further and execute the class from a compiled executable! For that, you have to learn to use the Invocation API.

The Invocation API

The JNI contains a facility for loading a Java VM into any existing native application. This facility presents exciting possibilities to application developers. Many developers agonize over how to add scripting or extensibility to their applications. By using the Invocation API, you can now use Java as your scripting language! All you have to supply is a few Java classes that expose their application's internals through native methods. Of course, you must also have some way to call the Java classes of your customers. This latter need is resolved with the Invocation API.

There are three functions for dealing with the VM:

  • jint JNI_CreateJavaVM(JavaVM **p_vm,JNIEnv **p_env, void *vm_args); Loads the Java VM shared library and passes the initialization arguments.

  • jint DestroyJavaVM(JavaVM *vm); Unloads the Java VM shared library.

  • void JNI_GetDefaultJavaVMInitArgs(void *vm_args); Retreives the default initialization arguments passed into CreateJavaVM().

Listing 32.7 shows the code for a small executable program that uses these three functions to run the Demonstration class. The code is also located on the accompanying CD-ROM.

Listing 32.7. Dynamically loading the Java VM in Invoke.cpp.


#include <jni.h>               /* where everything is defined */
#include <stdio.h>

JavaVM *jvm;                   /* denotes a Java VM */
JNIEnv *env;                   /* pointer to native method interface */
JDK1_1InitArgs vm_args;        /* VM initialization arguments */

void main(int argc, char **argv)
{
        jobject exceptObj;

        printf("Creating the Java VM\n");

        /* The default arguments are usually good enough. */
        JNI_GetDefaultJavaVMInitArgs(&vm_args);

        /* load and initialize a Java VM, return a native method interface 
        * pointer in env */
        JNI_CreateJavaVM(&jvm, &env, &vm_args);

        /* invoke the Demonstration.main method using the JNI */
        jclass cls = env->FindClass("Demonstration");
        jmethodID mid = env->GetStaticMethodID(cls, "start", "()V");

        printf("Calling Demonstration.start()\n");
        env->CallStaticVoidMethod(cls, mid);
        if (env->ExceptionOccurred() != NULL) env->ExceptionDescribe();

        /* We are done. */
        printf("Destroying Java VM\n");
        jvm->DestroyJavaVM();

}

Unlike the JNI interface, the functions of the Invocation API are called directly by a native application. As a consequence, the Java VM library (javai.lib) must be linked with the application. Because the actual functions of the Java VM are contained in a shared library, platforms that do not support dynamic linking are not eligible for the Invocation API.

Notice that because start() is a static method, it can be executed without creating an instance of the Demonstration class. I used the following Visual C++ 4.0 command to create the invoke executable:

cl invoke.cpp javai.lib

Running invoke.exe produces the following output:

Creating the Java VM
Calling Demonstration.start()
After Creation
vals[0] = 2
vals[1] = 1
vals[2] = 0
After reversing
vals[0] = 0
vals[1] = 1
vals[2] = 2
After creation
strs[0] = This is string #0002
strs[1] = This is string #0001
strs[2] = This is string #0000
After sorting
strs[0] = This is string #0000
strs[1] = This is string #0001
strs[2] = This is string #0002
Exception in thread "main" java.lang.Exception: genException()
        at Demonstration.start(Demonstration.java:45)
Destroying Java VM

Because the generated exception is not handled by the Java code, it is caught by the native method. The debugging utility function ExceptionDescribe is used to display the error. If the exception check is omitted from the native code, the exception is not handled by any function. Normally, an unhandled exception is caught by the Java VM before it exits. In this case, the VM is being dynamically accessed, so no checks are made.

Summary

Programming to the Java Native Method Interface is a complicated issue. Take some time to absorb this chapter. The JNI provides a rich set of functions; only a subset of these have been presented in this chapter. All the additional functions operate on the same ID system. Remember to acquire the necessary jobject, jclass, jfieldID, or jmethodID before executing an interface function. This extra step is what enables the JNI to present a standard interface on all platforms and VM implementations. Remember that any operation that can be performed in a Java class can also be performed by a native method. The JNI has standardized and simplified native method programming.

Do not ignore the Invocation API. It enables you to include Java in your compiled applications. Once you link the API into your programs, the rich infrastructure of Java can be leveraged for scripting, information exchange, or remote access. Your programs will be limited only by the imagination of your users! The Invocation API is also useful for creating a single executable for running your Java applications. The coupling is much tighter than using simple batch files or shell scripts to launch your classes. The biggest advantage to executable access is that you can present custom icons and control initial arguments. Additionally, your users can always appreciate the ease of single-command access. I expect the Invocation API to play a large role in forthcoming Java-enabled business applications.

TOCBACKFORWARDHOME


©Copyright, Macmillan Computer Publishing. All rights reserved.