Combining Java and C++
Instructor's Guide
intro,
components,
case study,
crossing boundaries,
styles,
platform,
summary,
Q/A,
literature
The designers of the Java language have created an elegant
facility for incorporating native C/C++ code
in Java applications, the Java Native Interface (JNI).
Elegant, since native methods can be mixed freely
with ordinary methods.
When qualifying methods as native,
the implementer must provide a dynamically loadable library
that contains functions, of which the names and signatures
must comply with the JNI standard,
defining the functionality of the methods.
Nevertheless, the JNI does not provide for
generic means to establish a direct correspondence
between an object class hierarchy in C++
that (partially) implements a corresponding object class
hierarchy in Java.
In this section, we will study how
such a correspondence is realized in the hush framework,
using the Java Native Interface.
The solution to establishing corresponding object class hierarchies
in Java and C++ that we have adopted relies on
storing a reference to the native C++ object in the Java object
and the conversion of this reference to a smart pointer
encapsulating access to the native C++ object.
Upcalls, which occur for example when Java handlers are invoked
in response to an event, require some additional machinery,
as will be explained shortly.
Each Java class in hush is derived from the obscure
class, which contains an instance variable _self
that may store a C++ object reference, encoded as an integer.
package hush.dv.api;
class obscure {
public int _self; peer object pointer
...
};
The class obscure has been introduced so as not to pollute
the handler class, which is the base
class for almost every hush class.
The (Java) handler class is derived from obscure.
As an example, look at the (partial) Java class description for
kit below.
package hush.dv.api;
public class kit extends handler {
public kit() { _self = init(); }
protected kit(int x) { }
private native int init();
public native void source(String cmd);
public native void eval(String cmd);
public String result() {
String _result = getresult();
if (_result.equals("-")) return null;
else return _result;
}
private native String getresult();
public native void bind(String cmd, handler h);
...
};
Recall that the kit class is used to encapsulate
an embedded interpreter, such as a Tcl or Prolog interpreter.
When a kit is constructed,
the instance variable _self is initialized
with the reference obtained from the native init method,
which will be given below.
The other methods of kit are either native
or result in invoking a native method, possibly with some
additional processing.
Each native method must be implemented as a function,
of which the name and signature are fixed by
the JNI conventions, as illustrated below.
#include <hush/hush.h>
#include <hush/java.h>
#include <native/hush_dv_api_kit.h>
#define method(X) Java_hush_dv_api_kit_##X
JNIEXPORT jint JNICALL method(init)(JNIEnv *env, jobject obj)
{
jint result = (jint) kit::_default; // (jint) new kit();
if (!result) {
kit* x = new kit("tk");
session::_default->_register(x);
result = (jint) x;
}
return result;
}
The init method, the full name of which is
obtained by expanding the macro call method(init),
results in an integer-encoded reference to a kit object,
which is newly created if it doesn't already exist.
JNIEXPORT jstring JNICALL method(getresult)(JNIEnv *env, jobject obj)
{
java_vm vm(env,obj);
char *s = vm->result();
if (s) return vm.string(s);
else return vm.string("-");
}
In the getresult method, we see how a smart pointer,
instantiated for the kit class,
is used to obtain the result from the C++ kit object.
The smart pointer takes care of converting the reference
stored in the Java object to an appropriate pointer.
JNIEXPORT void JNICALL method(bind)(JNIEnv *env, jobject obj,
jstring s, jobject o)
{
java_vm vm(env,obj);
java_vm* vmp = new java_vm(env,o,"Handler");
const char *str = vm.get(s);
handler* h = new handler();
session::_default->_register(h);
h->_vmp = vmp;
h->_register(vmp);
vm->bind(str,h);
vm.release(s, str);
}
In the bind method, which is used to bind
a (Java) handler object to some (Tcl or Prolog) command,
a new C++ handler is created.
This handler is modified to contain a reference
to the smart pointer, which (indeed) also gives
access to the Java handler object.
Notice that calling the Java handler object is an upcall,
when viewed from the native implementation.
In somewhat more detail,
the Java handler object is invoked through the
C++ handler object created in the bind method
of the kit.
The C++ handler is activated when an event occurs, or a Tcl
or Prolog command is given.
Activating the handler amounts to calling the dispatch
method with an appropriate event.
To decide whether the activation must be passed through
to the Java handler object,
the handler::dispatch
method checks for the availability of a smart pointer,
as illustrated below.
event* handler::dispatch(event* e) {
_event = e;
if (_vmp) {
return ((vm*)_vmp)->dispatch(e);
} else {
int result = this->operator()();
if (result != OK) return 0;
else return _event;
}
}
When the C++ handler contains a smart pointer,
the dispatch method is called for that pointer.
The Java smart pointer template class for the Java/C++
binding is derived from the smart pointer
template class introduced in the previous (sub)section.
#include <hush/vm.h>
#include
template< class T >
class java_vm : public vm< T > {
public:
java_vm(JNIEnv* env_, jobject obj_) {
_env = env_;
_obj = obj_;
_self = self();
}
...
event* dispatch(event* e) { java dispatch
call("dispatch",(int)e);
return e;
}
T* operator->() { return _self; }
T* self() {
jfieldID fid = fieldID("_self","I");
return (T*) _env->GetIntField( _obj, fid);
}
void call(const char* md, int i) { // void (*)(int)
jmethodID mid = methodID(md,"(I)V");
_env->CallVoidMethod(_obj, mid, i);
}
private:
JNIEnv* _env;
jobject _obj;
T* _self;
};
Notice how the value of the _self reference field
is obtained from the _self attribute
of the Java object.
Also notice that calling dispatch for the Java
handler is mediated by an additional call
function, which obtains an explicit reference
to the method that must be invoked.
In general, there are many possible
method signatures for which such a call function
could be supplied, but in our case
we only need one, to invoke dispatch.
Discussion
Interfacing Java and C++ is at first sight not very
difficult,
especially not when the majority of calls consists
of downcalls (from Java to C++) only.
The smart pointer device may then be used as a handy
abbreviation.
The problems occur, however, when upcalls come into play.
Due to the simple design of hush,
upcalls occur (almost) exclusively through
the dispatch method.
This is not the result of explicit design,
but in retrospect just sheer luck.
When upcalls are spread over the code and may
vary in signature, they will most likely bring along
significant software engineering and maintenance effort.