1.124 Lecture 19 11/19/998

Coupling Java and C++

Contents

1. The Java Native Interface

(Ref. Java Tutorial:  http://java.sun.com/docs/books/tutorial/native1.1/index.html, Core Java Vol II Ch 10)

The Java Native Interface (JNI) provides a mechanism for integrating Java and native languages, such as C and C++.  The JNI defines a standard naming and calling convention so that the Java Virtual Machine (VM) can locate and invoke native methods from a Java program.  The JNI also provides a standard interface for accessing Java objects and calling Java methods from native code.  To use the JNI, you should be running JDK version 1.1 or later.
 

2. Basic Steps: Hello World in a Native Method

(Ref. Java Tutorial: http://java.sun.com/docs/books/tutorial/native1.1/stepbystep/index.html)

Here are the basic steps involved in calling a native method from a Java program.

a) Write the Java code

The Java code needs to declare the native method using the native keyword.   No implementation is provided here, since this will be done in C++ code, which we will compile and archive as a shared library. The shared library is dynamically loaded at runtime using the System.loadLibrary command.

HelloWorld.java

class HelloWorld {
    // Define a native method.  The implementation is provided in a separate
    // native language source file.
    public native void displayHelloWorld();
 
    // Load a shared library named libhello.so (Unix) or hello.dll (Windows)
    // which has been compiled from native code.  The static initializer is
    // executed by the runtime system when the class is loaded.
    static {
        System.loadLibrary("hello");
    }
}
 

Main.java

class Main {
    public static void main(String[] args) {
        // Instantiate the class HelloWorld and call its native method.
        // This combines the following two steps
        //     HelloWorld foo = new HelloWorld();
        //     foo.displayHelloWorld();
        new HelloWorld().displayHelloWorld();
    }
 

b) Compile the Java code

The Java code is compiled as usual using javac:

    javac HelloWorld.java
    javac Main.java
 

c) Generate the header file

As a result of step b), we will have a class file called HelloWorld.class.  We will use this class file
to generate a header file, HelloWorld.h, which we can include in our C++ code.  This header file will provide us with a function prototype for the native method.

The header file is generated as follows:
    javah -jni HelloWorld
 

HelloWorld.h

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

d) Implement the native method

We may now create a .C file to implement the native method.  This file must include both the generated header file, HelloWorld.h, as well as the JNI header file, jni.h.  The function definition is based on the
prototype in the generated header file.  Note that there are two arguments to the native method even though the function declation in Java has no arguments.

We will see how to use these arguments in a later example.
 

HelloWorldImp.C

#include "HelloWorld.h"  // Generated header file.
#include <iostream.h>
 
#include <jni.h>         // Required for native code-Java runtime interaction.
 
// The native code implementation.
JNIEXPORT void JNICALL
Java_HelloWorld_displayHelloWorld(JNIEnv *env, jobject obj)
{
    cout << "Hello world!" << endl;
}

 
e) Create a shared library

We must now compile the C++ code into an object file and archive it as a shared library.   On the Solaris platform on Athena, we can do this as follows:

    g++ -c -I/mit/java/java_v1.1.3/include -I/mit/java/java_v1.1.3/include/solaris HelloWorldImp.C
    g++ -G -o libhello.so HelloWorldImp.o
 
 
f) Run the program

The program can now be run using the Java interpreter:

    java Main
 

3. Mapping Between Java and C++ Types

(Ref. Java Tutorial: http://java.sun.com/docs/books/tutorial/native1.1/integrating/types.html)

Here is a list of Java types and the corresponding C/C++ types that the JNI provides.
Primitive types are straightforward, since they can be directly accessed by the native method.   Java Object types, however, need to be manipulated via accessor and modifier functions that belong to the JNIEnv interface (see example below.)

Java Primitive Types and Native Equivalents
 

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
 
Note that the JNI redefines the primitive native types.  This is because the definitions of native types are not always standard across computing platforms e.g an int is 32 bits long on Unix and Windows 95/NT, but it is only 16 bits long on Windows 3.1.

Java Object Types

Java objects are passed by reference. All references to Java objects have type jobject. For convenience and to reduce the chance of programming errors, the JNI introduces a set of types that are conceptually all "subtypes" of jobject:


4. Doing More: Passing Objects and Calling Back Into Java

(Ref. Java Tutorial: http://java.sun.com/docs/books/tutorial/native1.1/implementing/index.html, Core Java Vol II CH 10)

In general, we will want to pass objects back and forth between Java and C++, and we may also need to call Java methods from native code.   The following example illustrates how this is done.

Main.java

class Main {
    public static void main(String[] args) {
        Automobile auto[] = new Automobile[3];
 
        // Create some new objects.
        auto[0] = new Automobile("Ford", 55.0);
        auto[1] = new Automobile("GM", 60.0);
        auto[2] = new Automobile("Chrysler", 50.0);

        // Print them out.
        for (int i = 0; i < 3; i++)
            auto[i].print();

        // Now call a native method to change the object properties.
        // This method will also call back into Java by invoking the
        // Automobile.print method.
        auto[0].change("Mercedes", 75.0);
        auto[1].change("BMW", 70.0);
        auto[2].change("Porsche", 65.0);
    }
}
 

Automobile.java

public class Automobile {
    double mdSpeed;
    String mMake;
    static int miCount = 0;

    // Automobile constructor.
    Automobile(String make, double dSpeed) {
        mdSpeed = dSpeed;
        mMake = make;
        miCount++;
    }

    // A print method.
    void print() {
        System.out.println("In Java:");
        System.out.println("Make = " + mMake);
        System.out.println("Speed = " + mdSpeed + "\n");
    }

    // Define a native method.  The implementation is provided in a separate
    // native language source file.
    public native void change(String newMake, double newSpeed);

    // Load a shared library named libauto.so (Unix) or auto.dll (Windows)
    // which has been compiled from native code.  The static initializer is
    // executed by the runtime system when the class is loaded.
    static {
        System.loadLibrary("auto");
    }
}
 

AutomobileImp.C

#include "Automobile.h"  // Generated header file.
#include <iostream.h>

#include <jni.h>         // Required for native code-Java runtime interaction.

// The native code implementation.  Note that this function has four arguments
// on the C++ side, even though there were only two arguments on the Java side.
// The first argument, env, is the JNI interface, which gives us access to
// Java methods from the C++ code.  The second argument, obj, is a reference
// to the Automobile object itself, and is analogous to the "this" pointer.
JNIEXPORT void JNICALL
Java_Automobile_change(JNIEnv *env, jobject obj,
                       jstring newMake, jdouble newSpeed)
{
    jclass cls = env->GetObjectClass(obj);  // The Java class that this method
                                                                                  // belongs to i.e. Automobile.
    jfieldID fid;
    jdouble jdSpeed;                  // A jdouble type (same as a C double.)
    jstring jstr;                             // A pointer to a java.lang.String (not the
                                                      // same as a C-style string.)
    const char *str, *newstr;   // C-style strings.
    jint jiCount;                            // A jint type (same as a C int.)
    jmethodID jmid;                   // A Java method ID.

    cout << "In C++:" << endl;

    // Get the speed of the automobile (a double), and change it to newSpeed.
    // The last argument to GetFieldID is the field signature, which can be
    // found by typing javap -s Automobile, once Automobile.class has been
    // generated.
    fid = env->GetFieldID(cls, "mdSpeed", "D");
    if (fid == 0)
        return;
    jdSpeed = env->GetDoubleField(obj, fid);
    cout << "Old speed = " << jdSpeed;
    env->SetDoubleField(obj, fid, newSpeed);
    cout << ";  New speed = " << newSpeed << endl;
 

    // Get the make of the automobile (a java.lang.String).  The last argument
    // to GetFieldID is the field signature, which can be found by typing
    // javap -s Automobile, once Automobile.class has been generated.
    fid = env->GetFieldID(cls, "mMake", "Ljava/lang/String;");
    if (fid == 0)
        return;
    jstr = (jstring)env->GetObjectField(obj, fid);

    // Convert the java.lang.String objects to a C-strings.  The last argument
    // to GetStringUTFChars indicates that we do not wish to get a copy of the
    // string.
    str = env->GetStringUTFChars(jstr, 0);
    newstr = env->GetStringUTFChars(newMake, 0);
    cout << "Old make = " << str;
    env->ReleaseStringUTFChars(jstr, str);
    env->ReleaseStringUTFChars(newMake, newstr);

    // Now set the new make.  We don't need to create a new jstring in this
    // case, but if we needed to, we could use
    //     newMake = env->NewStringUTF("Ford");
    env->SetObjectField(obj, fid, newMake);
    cout << ";  New make = " << newstr << endl;
 

    // Get the number of automobiles (a static int).  The last argument is
    // the field signature, which can be found by typing javap -s Automobile,
    // once Automobile.class has been generated.  Note that GetStaticIntField
    // takes the class as an argument, instead of the object.
    fid = env->GetStaticFieldID(cls, "miCount", "I");
    if (fid == 0)
        return;
    jiCount = env->GetStaticIntField(cls, fid);
    cout << "Total number of automobiles = " << jiCount << endl;
    cout << endl;

    // Finally, lets call the print method in Java, just to be sure that
    // the fields have really been changed.  Note: if the print method took
    // any arguments, then those arguments would be passed into CallVoidMethod
    // after the method ID.
    jmid = env->GetMethodID(cls, "print", "()V");
    if (jmid == 0)
        return;
    cout << "Calling Java from C++..." << endl;
    env->CallVoidMethod(obj, jmid);
}