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 { 
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 { 
kit
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.


  
kit.c
#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.


  
handler::dispatch
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 > { 
java_vm
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.