Adapter Classes for the Java Native Interface

Copyright © Gordon R. Durand, 1999
Technical or typographical errors? Please email me.

You can download the code for this article.

Introducing JNI

The Java Native Interface API allows Java objects to call native routines written in C++.

The Java class must declare the routine with the reserved word native:

class HelloWorld { public native void sayHello(); public static void main(String[] args) { System.loadLibrary("HelloWorld") new HelloWorld().sayHello(); } }
Compile the class with javac

>javac HelloWorld.java
and pass it to javah

>javah HelloWorld
which will generate a C/C++ header file (HelloWorld.h) containing the native routine prototype:

/* 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_sayHello(JNIEnv *, jobject); #ifdef __cplusplus } #endif #endif
Include the generated header file in a C/C++ source file which defines the routine

#include "HelloWorld.h" JNIEXPORT void JNICALL Java_HelloWorld_sayHello(JNIEnv *, jobject) { printf("Hello, JNI World"); }
and compile the C/C++ source into a DLL:

>bcc32 -tWDE -eHelloWorld.dll -I.\include -L.\lib HelloWorld.cpp
Finally, run the Java program:

>java HelloWorld Hello, JNI World
You may already have noticed a few of JNl's limitations. First, in order to call native code from Java, you have to own the native code.To link to code in a pre-compiled DLL, you would have to write a wrapper DLL that called the pre-compiled DLL. Second, you can only link to code in a DLL; you can't link to code in an EXE. If for some reason you can't put your native code in a DLL, your only choice may be to use CORBA. Third, when a Java object calls a native method it only calls a C++ function. For a Java object to send a message to a C++ object (or vice versa) you'll have to write a little more code.

Linking Java Objects to C++ Objects

Consider two object running in the same process, such as two C++ objects, CBoard and JBoard. JBoard has a pointer to CBoard so that it can send setLength and getLength messages to CBoard. and CBoard has a pointer to JBoard so that it can send levelChanged messages to JBoard. These two objects, JBoard and CBoard, have a simple bi-directional relationship. They work together.

Now suppose you must relocate JBoard to Java land. CBoard will stay behind in C++ land, in a DLL rather than an EXE, but otherwise the same. How can you preserve their relationship across the great divide? Can Java objects send messages to C++ objects and vice versa?

A qualified yes.

If JBoard can still keep a pointer to CBoard (a C++ pointer will fit nicely into a Java int), JBoard can simply pass the pointer as a parameter to the native routine and the native routine can cast the pointer to a CBoard pointer and pass the message along to CBoard.

JNIEXPORT jint JNICALL Java_JBoard_getLength(JNIEnv *, jobject, jint cBrd) { return (reinterpret_cast<CBoard*>(cBrd))->getLength(); }
Simple and effective.

What about the reverse? Can you give CBoard JBoard's address so that CBoard can send JBoard messages? Sort of.

Linking C++ Objects to Java Objects

You may have noticed that every JNI call adds two more parameters than you specify in the Java code:

(JNIEnv * jEnv, jobject jObj)
jEnv points to the JNI Environment and jObj references the Java object that called the native routine. The JNI Environment provides a number of functions which you can use to access a Java object's methods and variables. Using jEnv and jObj, you can send a message to JBoard:

jclass jClass; jmethodID jMethID; jClass = jEnv->GetObjectClass(jObj); jMethID = jEnv->GetMethodID(jClass, "levelChanged", "(I)V"); jEnv->CallVoidMethod(jObject, jMethID, level);
So great! CBoard can just keep a copy of jEnv and jObj, and use them to send messages to JBoard, right? Wrong. The JNIEnv* and jobject parameters are only good for the duration of the JNI function call. They may work after that, and they may not. The Java object might get garbage collected, or the JVM may be in a different context. If you try to cache jEnv and jObj, sooner or later they will crash your application.

Fortunately, there's a way around this. First, you can get a permanent reference to the jobject by calling

jobject jObject; jObject = jEnv->NewGlobalRef(jObj);
NewGlobalRef tells the JVM that you have a reference to JBoard so JBoard won't get garbage collected. Naturally, if you ever want to get rid of JBoard, you'll have to release that reference

jEnv->DeleteGlobalRef(jObject);
but until you do, your global reference to JBoard is good as gold. But what about JNIEnv? Well, you can't keep a pointer to the environment, but you can do the next best thing, and keep a pointer to the JVM itself:

    JavaVM* pJavaVM;
    pEnv->GetJavaVM(&pJavaVM);
Using that, you can get a reference to a valid JNIEnv whenever you need one:

JNIEnv* jEnv; pJavaVM->AttachCurrentThread((void**) &jEnv, NULL):
Like any other JNIEnv pointer, this one is only good for the duration. And as soon as you're done with it, you have to release it:

pJavaVM->DetachCurrentThread();
Since we don't want to burden each C++ object with all this code just to let it send messages to a Java object, we could encapsulate it in a class.

The JavaAdapter Class

We typically use Adapter or Proxy classes ("wrappers" is another terrn) in these situations. Here we need a JBoardAdapter object to stand in for the displaced JBoard object. JBoardAdapter will receive messages from CBoard and pass them on to JBoard. Since most of the functionality of JBoardAdapter could apply to any C-to-Java adapter, we'll make JBoardAdapter a subclass of JavaAdapter, and let JavaAdapter handle all the common behavior.

JavaAdapter.h:

#include <jni.h> class JavaAdapter { private: JavaAdapter() {}; protected: JavaVM* pJavaVM; jobject jObject; JNIEnv * getJNIEnv(); void releaseJNIEnv(); public: JavaAdapter(JNIEnv * jEnv, jobject jObj); virtual ~JavaAdapter(); };
JavaAdapter.cpp:

#include "JavaAdapter.h" JavaAdapter::JavaAdapter(JNIEnv* jEnv, jobject jObj) { pJavaVM = NULL; int retVal = jEnv->GetJavaVM(&pJavaVM); if (retVal) { printf("GetJavaVM error %d", retVal); } jObject = jEnv->NewGlobalRef(jObj); }; JavaAdapter::~JavaAdapter() { getJNIEnv()->DeleteGlobalRef(jObject); }; JNIEnv* JavaAdapter::getJNIEnv() { JNIEnv* jEnv = NULL; int retVal = pJavaVM->AttachCurrentThread((void**) &jEnv, NULL); if (retVal) printf("AttachCurrentThread error %d", retVal); } return jEnv; }; void JavaAdapter::releaseJNIEnv() { int retVal = pJavaVM->DetachCurrentThread(); if (retVal) { printf("DetachCurrentThread error %d", retVal); } };
With JavaAdapter as a base class, JBoardAdapter only needs to implement its particular functions.

JBoardAdapter.h:

#include "JavaAdapter.h" class JBoardAdapter : public JavaAdapter { private: jmethodID levelChangedID; public: JBoardAdapter(JNIEnv * pEnv, jobject jObj); void levelChanged(int howMany); };
and JBoardAdapter.cpp:

#include "JBoardAdapter.h" JBoardAdapter::JBoardAdapter(JNIEnv * pEnv, jobject jObj) : JavaAdapter(pEnv, jObj) { jclass jClass = pEnv->GetObjectClass(jObject); levelChangedID = pEnv->GetMethodID(jClass, "levelChanged", "(I)V"); } void JBoardAdapter::levelChanged(int level) { JNIEnv* pEnv = getJNIEnv(); pEnv->CallVoidMethod(jObject, levelChangedID, level); releaseJNIEnv(); }
Notice that we have JBoardAdapter cache a jmethodID for the Java method it will call. Calls to GetObjectClass and GetMethodlD are expensive, so it's better to cache your results.

We didn't specify exactly how JBoard and CBoard get pointers to each other. Presumably, you would use a mechanism similar to what you would have used when they were both C++ objects. If JBoard creates CBoard, it can pass itself as a reference to CBoard's constructor and receive a pointer to CBoard in return. If CBoard creates JBoard (which it can if it has a pointer to the JVM), then the two can trade references in a similar manner. Another option is to have a third party create both of them, and set their references explicitly.

The Next Best Thing to Being There

So with very little additional code, C++ and Java objects can have as complex a relationship with each other as they might have with their own native objects. JavaAdapters can mediate for C++ to Java messages, and static C++ code can implement Java to C++ messages. You can put your objects in the environment that suits them best, and let them work together.

You can download the code for this article.

UML Diagram:

UML Drawing to Accompany JNI Article