| 1.124 Lecture 19 | 11/19/998 |
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.
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.
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
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 |
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:
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);
}