JNI++ Generator: Input, Java Proxies, JNI Stubs

Klaus Wiederänders

previous  home  next 

last revision: Tue Jun 13 17:32:27 2000

1. Overview

This paper describes in short the generator tool of the JNI++ open-source project. Since the JNI++ is, at present, not much more than an idea and vision, it takes the only thing already materialized: a simple but working example of couple of native code classes subclassed by a Java subclass. This paper describes the code patterns to be generated by the JNI++ generator tool to connect the native C++ superclasses with the Java subclass to arrange for both:
  • calls to native C++ methods out of the Java subclass
  • callbacks to overriding Java methods from out of C++ superclasses
It is supposed that the C++ classen are given in a native code library and the native class definitions are available as C++ header files. On the other hand, the implementation of the C++ classes is considered unavailable or cannot be changed.

The envisioned JNI++ generator should generate all the code necessary for linking a newly created Java subclass to the given native superclasses.

2. JNI++ Generator's Input

The most important input to the JNI++ generator are the definition files for native C++ classes. The sample class definitions are shown here. At the moment, it is yet unclear if all necessary information can be derived from the C++ header files. Furthermore, parsing C++ headers is quite a hard job todo and there is no generic C++ parser around, which would fit with JNI++. That's why, for the moment, a simple generator written in Perl is used to generate all the JNI++ code from some handmade interface description language. This generator will be described in a future paper.

3. Type Mapping Information

Since Java types and C++ types differ, some kind of type mapping is required. This topic needs further investigation.

4. Generator Output

As turns out to be, the JNI++ generator has to produce 4 different kinds of output for each native C++ class, namely
  • a Java proxy class defining native functions
  • the JNI stubs for each native function defined in the Java proxy
  • a C++/Java adpater class for callbacks (overridden virtual functions)
  • a Java/C++ wrapper class for C++ instances passed to Java functions.
Java proxy classes and JNI stubs are explained below. Wrapper classes will be described in JNI++ part 3.

4.1. Generated Java Proxy Classes

In order to make the native superclasses appear and behave like Java classes, a Java proxy class is generated for each native C++ class in question. It should be no supprise that these proxy classes contain a JNI interface.

And this is how the example's proxy classes look like:



class Foo : Java proxy

package FooBar;

/*
Summary: Java proxy for C++ class Foo
Purpose:
  This is the JNI++ Java proxy for the native C++ class Foo. 
A Java proxy for a C++ class is a Java class which declares a native 
Java function for each C++ member function exported by the C++ class.
Furthermore it does some book keeping stuff in order to make JNI++ work.

Todo:
  DocJet does not recognize Java native functions yet.
Note:
  Do not touch this file! It was entirely generated by Metaphor 
at Tue Jun 13 17:17:18 2000 from its IDL source. 
*/
public class Foo
extends Bar
{
/*
Summary: Constructor with same signature as native C++ constructor
Parameters:
	message - see Foo::Foo() for doc
Purpose:
  Use this constructor to create direct instances of class Foo.
Consult doc of native C++ class for a description of the purpose 
constructor and meaning of the parameters.
*/
    public Foo( String message )
    {
        super( 0 );
        create( jni_create( message ) );
    }

/*
Summary: Create the native C++ part of an instance of class Foo
Returns: the handle to the C++ instance
Purpose:
  This native function creates the C++ part of an instance of Java class
  Foo.
*/
    private native int jni_create( String message );
    
    Foo( int nativeSelf )
    {
        super( nativeSelf );
    }

    public native Bar foo( String message, Bar object );
    public native Bar getBar(  );
    public native int passCount( int count );
}
First of all, since the native class Foo is a subclass of native class Bar, the Java proxy Foo is a subclass of Java proxy Bar.

Furthermore, for each member function of the native class Foo there is a Java native method declaration with corresponding signature (the problems of Java/C++ type conversion are discussed later).

Then, there is a constructor with a signature corresponding to the native class Foo's constructor. This constructor is called for the creation of direct instances of Java Foo. It is implemented via a native function jni_create() with the constructor's signature. This JNI function serves for creation of the C++ wrapper which constitutes the native C++ part of the newly created instance (explained later). But before this can be done, the superclass is to be constructed.

Each Java proxy provides an additional constructor with a single integer as argument. This constructor is called the proxy constructor. It is used in Java proxy classes extending another Java proxy class. Foo itself provides such a constructor, too. And Foo calls Bar's proxy constructor before creating the native part. The role of the proxy constructor's argument and the create() function are explained later.

The code for the Java Foo proxy is fairly straight forward and can be derived easily from the native C++ class definition: The implementation of the proxy constructor uses always the above simple pattern. For each native C++ constructor there is a Java constructor and a native jni_create() function with corresponding signature. For each member function declared in the native C++ class a Java JNI native function declaration is generated. That's all.



class Bar : Java proxy

package FooBar;

/*
Summary: Java proxy for C++ class Bar
Purpose:
  This is the JNI++ Java proxy for the native C++ class Bar. 
A Java proxy for a C++ class is a Java class which declares a native 
Java function for each C++ member function exported by the C++ class.
Furthermore it does some book keeping stuff in order to make JNI++ work.

Todo:
  DocJet does not recognize Java native functions yet.
Note:
  Do not touch this file! It was entirely generated by Metaphor 
at Tue Jun 13 17:17:19 2000 from its IDL source. 
*/
public class Bar
extends Base
{
/*
Summary: Constructor with same signature as native C++ constructor
Parameters:
	message - see Bar::Bar() for doc
Purpose:
  Use this constructor to create direct instances of class Bar.
Consult doc of native C++ class for a description of the purpose 
constructor and meaning of the parameters.
*/
    public Bar( String message )
    {
        super( 0 );
        create( jni_create( message ) );
    }

/*
Summary: Create the native C++ part of an instance of class Bar
Returns: the handle to the C++ instance
Purpose:
  This native function creates the C++ part of an instance of Java class
  Bar.
*/
    private native int jni_create( String message );
    
    Bar( int nativeSelf )
    {
        super( nativeSelf );
    }

    public native Bar bar( String message, Bar object );
    public native Bar foo( String message, Bar object );
    public native String getMessage(  );
}
The code for the Java proxy Bar differs only in its superclass. The native C++ class Bar has no superclass. Java proxy Bar extends Base (see below). All the other code should be already familiar: There is the Java constructor corresponding to the native C++ constructor together with its native jni_create() function. Then there is the proxy constructor. Finally, there is a Java native JNI function for each member function declared in the native C++ class.

A few words about Base: This class is part of JNI++ and provides some services useful for all JNI++ Java proxies, namely some bookeeping for JNI++ and the finalize() function which is an equivalent of native C++ destructors. All destructors have a common implementation in JNI++. Base is explained in detail later.

The generation of proxy classes from C++ class definitions following the above patterns seems to be straight forward. All necessary information can be derived by parsing the class definition. There is a one-to-one correspondence between the native C++ members and the Java proxy members. Only a handful of JNI++ specific functions is to be created. Note that private members of the C++ classes are not mirrored in Java. Handling of non-private member attributes is discussed later.

Once these proxies are generated, Java subclasses can be derived easily and in genuine Java style. Native C++ code does not show up in Java subclasses. Subclass programmers need not care about C++, provided the accessed JNI code works correctly.

4.2. Generated JNI Stub Code

Next the JNI code is to be generated. At this point it is important to meet the JNI conventions in full extent.

4.2.1. The JNI Stub Function Definitions

JNI itself comes with the javah tool to generate C stub function declarations for Java classes containing native functions. Whether javah is used as is or a module of the JNI++ generator produces similar code can left be open for the moment. Beside the header files, JNI++ generates the stub implementations, too. These are the C++ stub headers generated for the example's Java proxy classes by javah:


class Bar : JNI stub definition

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class FooBar_Bar */

#ifndef _Included_FooBar_Bar
#define _Included_FooBar_Bar
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     FooBar_Bar
 * Method:    bar
 * Signature: (Ljava/lang/String;LFooBar/Bar;)LFooBar/Bar;
 */
JNIEXPORT jobject JNICALL Java_FooBar_Bar_bar
  (JNIEnv *, jobject, jstring, jobject);

/*
 * Class:     FooBar_Bar
 * Method:    foo
 * Signature: (Ljava/lang/String;LFooBar/Bar;)LFooBar/Bar;
 */
JNIEXPORT jobject JNICALL Java_FooBar_Bar_foo
  (JNIEnv *, jobject, jstring, jobject);

/*
 * Class:     FooBar_Bar
 * Method:    getMessage
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_FooBar_Bar_getMessage
  (JNIEnv *, jobject);

/*
 * Class:     FooBar_Bar
 * Method:    jni_create
 * Signature: (Ljava/lang/String;)I
 */
JNIEXPORT jint JNICALL Java_FooBar_Bar_jni_1create
  (JNIEnv *, jobject, jstring);

#ifdef __cplusplus
}
#endif
#endif



class Foo : JNI stub definition

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class FooBar_Foo */

#ifndef _Included_FooBar_Foo
#define _Included_FooBar_Foo
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     FooBar_Foo
 * Method:    foo
 * Signature: (Ljava/lang/String;LFooBar/Bar;)LFooBar/Bar;
 */
JNIEXPORT jobject JNICALL Java_FooBar_Foo_foo
  (JNIEnv *, jobject, jstring, jobject);

/*
 * Class:     FooBar_Foo
 * Method:    getBar
 * Signature: ()LFooBar/Bar;
 */
JNIEXPORT jobject JNICALL Java_FooBar_Foo_getBar
  (JNIEnv *, jobject);

/*
 * Class:     FooBar_Foo
 * Method:    jni_create
 * Signature: (Ljava/lang/String;)I
 */
JNIEXPORT jint JNICALL Java_FooBar_Foo_jni_1create
  (JNIEnv *, jobject, jstring);

/*
 * Class:     FooBar_Foo
 * Method:    passCount
 * Signature: (I)I
 */
JNIEXPORT jint JNICALL Java_FooBar_Foo_passCount
  (JNIEnv *, jobject, jint);

#ifdef __cplusplus
}
#endif
#endif

This is pure JNI stub code which needs no further comment. The more interesting point is the implementation of these functions, described below.

4.2.2. Calls of C++ Members from Java

An instance of a Java subclass extending a native C++ class consists of a Java part and a C++ part. The Java proxy serves for setting up the Java part. Now let's care for the C++ part.

First for construction. Whenever a Java instance is created, the appropriate constructor of the Java instance calls the jni_create() function with matching signature. This function is responsible for creating the C++ part of the instance. As long as no virtual C++ function is to be overridden, it suffices to create and attach an instance of the respective C++ superclass (called the native self) to the already constructed Java object and to use the native self in the JNI stubs for the native C++ member functions.

An implementation without virtual function callbacks could look like this:



class Foo: JNI stub create sample

JNIEXPORT jint JNICALL Java_Foo_jni_1create(JNIEnv * env, jobject self, jstring message )
{
    jniString jni_messageString( env, message );
    const char * jni_message = jni_messageString.jni_getNative();
    jniFooBase * nativeSelf = new jniFooJava( env, self, jni_message );
    return (jint) dynamic_cast<jniBase *>(nativeSelf);
};
Beside type conversion, the jni_create() function simply creates an instance of the respective C++ class and returns a handle to the newly created C++ object back to Java. Looking back at the constructor in the Java proxy, one can see that this handle is passed to the function create() implemented in Base. So, it's time to have a closer look at Base:


class Base : definition

package FooBar;

/*
Summary: ultimate Java base class for Java JNI proxies
Purpose:
 This is the base class for all (generated) Java    proxies. There is one Java
proxy for each native C++ class exported by the native library. The Java class
base provides technical support for the Java/C++ connection accross JNI++.
Todo:
This class provides the native function jni_destroy(int nativeSelf) to destroy
the C++ part of the instance. DocJet does not recognize native functions yet.
*/
class Base
{
/*
Summary: the handle to the C++ instance
Purpose:
 JNI++ is aimed at subclassing C++ by Java. For this purpose, each Java 
instance has a handle to its C++ part (actually a pointer to a C++ object).
This member stores this handle on behalf of the JNI mechanism.
*/
    private int jni_NativeSelf;

/*    
Summary: create base part of the Java instance
Parameters:
	nativeSelf - the native self, a handle to the C++ part of the instance
Purpose:
 Stores the handle to the C++ instance.
*/
    public Base( int nativeSelf )
    {
        create( nativeSelf );
    }

/*
Summary: stores the handle to the C++ instance
Parameters:
	nativeSelf - the native self, a handle to the C++ part of the instance
Purpose:
 Call this method in Java proxies to store
the native self, which is in fact a handle
to the underlying native adapter object.
This function does the dirty job for the 
constructor but can be called by subclasses 
directly, too.
*/
    protected void create( int nativeSelf )
    {
        jni_NativeSelf = nativeSelf;
    }

/*
Summary: destroy the Java instance
Purpose:
 This function is called (during garbage collection
or manually) in order to get rid of the underlying
adapter objects.
*/
    protected void finalize()
    {
        jni_destroy( jni_NativeSelf );
    }

/*
Summary: destroy the C++ instance
Purpose: 
 This native function does the dirty job for finalize(),
i.e. it destructs the native adapter object identified
by native self.
*/
    private native void jni_destroy( int nativeSelf );
}
There is no mistery about Base: It stores the native self (the handle to the C++ part) handed over by the create() function in a private variable named jni_NativeSelf. As usual in C++, the destruction of dynamically constructed C++ objects must be done explicitely. This holds for the C++ object behind native self, too. The finalize() function of Base serves for this purpose: it calls the JNI native function jni_destroy() with the native self handle as argument, which, in turn, deletes the C++ object.

With this bookeeping background, a JNI stub for a native member function (without virtual callbacks) could then look like this:



class Bar: JNI stub member function sample

JNIEXPORT jint JNICALL Java_Foo_getCount(JNIEnv* env, jobject self, jint count )
{
    int jni_count = count;
    jniFooBase * nativeSelf = dynamic_cast<jniFooBase *>( 
        reinterpret_cast<jniBase *>( jniBase::jni_getNativeSelf( env, self ) ) );
    int reply = reinterpret_cast<Foo *>(
        nativeSelf->jni_getNative())->Foo::getCount( jni_count );
    return reply;
}
The JNI stub first inquires the native self handle stored in the Java part of the instance. Since JNI allwos the access even to private members, it's easy to get the native self stored in the Java attribute jni_NativeSelf. Then the member function of the C++ object is called, it's return value is converted from C++ to Java, and the result is passed back to Java.

This is one of the implementations usually found for accessing C++ member functions from out of Java. It's easy, it's straight, it works. But it fails to handle calls to virtual functions from inside C++ code but overridden in Java subclasses.

4.2.3. Callback Virtual C++ Functions Overridden by Java Subclasses

When a virtual function is called, this call is directed to the farest subclass providing an override with matching signature to that function. In the mixed language case, this override of a C++ virtual function might be provided by a Java subclass. Special measures must be taken to emulate this situation across language boundaries.

JNI++ takes the following approach: An C++/Java wrapper class is generated for each native C++ class in question. This wrapper class is a C++ class which

  • is a subclass of the original native C++ class
  • overrides all virtual functions provided by the original C++ class
The wrapper class catches calls of virtual functions and transforms these calls into Java proxy calls. Since all Java functions are virtual, the Java proxy calls end up in calls to methods of Java subclasses overriding the Java proxy methods. This is the basic idea. Java/C++ wrapper classes are explained in detail in JNI++ part 3. At this point, a sample Java/C++ wrapper class definition and a short functional description should suffice.


class Bar: Java wrapper definition

#ifndef jniBarJava_h
#define jniBarJava_h

#include "jniBarBase.h"
#include "Bar.h"

class Bar;

/*
{group:wrapper}
Summary: Java wrapper for C++ class Bar
Purpose:
  This is the JNI++ Java wrapper for the native C++ class Bar. 
Java wrappers are used whenever a Java subclass is derived from a C++ 
superclass using JNI++. A Java wrapper for a C++ class is a C++ subclass 
of this C++ class. It constitutes the C++ part of a Java instance of 
the Java subclass. It declares a wrapper function for each C++ virtual function 
provided. It organizes the callbacks from C++ back to Java via virtual functions.

Note:
  Do not touch this file! It was entirely generated by Metaphor
at Tue Jun 13 17:17:19 2000 from its IDL source. 
*/
class jniBarJava
: public jniBarBase
, public Bar
{
public:
/*
Summary: Constructor with same signature as native C++ constructor
Parameters:
    env - the JNI environment for that call
    self - the Java self
    message - see Bar:: for doc
Purpose:
  Use this constructor to create direct instances of class Bar.
Consult doc of native C++ class for a description of the purpose 
constructor and meaning of the parameters.
Parameters:
    env -    the JNI environment for the object
    self -   the JNI handle for the Java object
*/
    jniBarJava( JNIEnv * env, jobject self, const char * message );

    virtual ~jniBarJava();

/*
Summary: Override of native C++ virtual function
Returns: see Bar::bar for doc
Parameters:
    env - the JNI environment for that call
    self - the Java self
    message - see Bar::bar for doc
    object - see Bar::bar for doc
Purpose:
  The JNI++ Java wrapper class provides an override for each virtual function
provided by the native C++ class in question. Such an override serves for calling
the appropriate Java function in the Java part of the instance whenever the virtual
function is called in the C++ part. This is on the very heard of JNI++.
*/
    virtual Bar * bar( const char * message, Bar * object );

/*
Summary: Get the native self
Returns: The native self, a handle to the C++ part of the instance (the this pointer)
Purpose:
  This is the Java wrapper's implementation of the interface function declared in 
jniBarBase. JNI++ uses this function to inquire the native self, a pointer to
the C++ part of the instance.
*/
    virtual Bar * jni_getNative();
/*
Summary: Destroy the C++ part of the instance
Purpose:
  This is the Java wrapper's implementation of the interface function declared in 
class jniRoot. JNI++ uses this function to destroy the C++ part of the instance.
*/
    virtual void jni_destroy();
};

#endif
The constructor takes the JNI environment and a handle to the Java part of the instance, the Java self. With these values, a link to the Java proxy can be established via JNI during virtual function callbacks.

The override of the virtual function bar() invokes the Java proxy's function bar() with corresponding signature and thus of any Java subclass overriding this function. Finally, the return value must be converted from Java to C++ and passed back to the native C++ caller.




class Bar : JNI stub implementation

#include "jniext.h"
#include "Bar.h"
#include "jniBar.h"
#include "jniBarJava.h"
#include "jniBarNative.h"
#include "jniString.h"
#include "jniBarNative.h"
/*
{group:JNIstub}
Summary: create an instance of native C++ class Bar
Returns: the native self, a handle to the C++ instance
Parameters:
    env - the JNI environment for that call
    self - the Java self
    message - see constructor of Bar for doc
Purpose:
  This function generates the C++ part of an Instance created by the
JNI++ Java proxy Bar
Note:
  Do not touch this function! It was entirely generated by Metaphor 
at Tue Jun 13 17:17:19 2000 from its IDL source. 
*/
JNIEXPORT jint JNICALL Java_FooBar_Bar_jni_1create(JNIEnv * env, jobject self
, jstring message )
{
    jniString jni_messageString( env, message );
    const char * jni_message = jni_messageString.jni_getNative();
    jniBarBase * nativeSelf = new jniBarJava( env, self, jni_message );
    return (jint) dynamic_cast<jniRoot *>(nativeSelf);
};



/*
{group:JNIstub}
Summary: JNI stub implementation for virtual function
Returns: see Bar::bar for doc
Parameters:
    env - the JNI environment for that call
    self - the Java self
    message - see Bar::bar for doc
    object - see Bar::bar for doc
Purpose:
  This function propagates the function call from Java proxy Bar to 
  native C++ Bar::bar and does the appropriate type 
  conversions for parameters and return values.
Note:
  Do not touch this function! It was entirely generated by Metaphor 
at Tue Jun 13 17:17:19 2000 from its IDL source. 
*/
JNIEXPORT jobject JNICALL Java_FooBar_Bar_bar(JNIEnv* env, jobject self
, jstring message, jobject object )
{
    jniString jni_messageString( env, message );
    const char * jni_message = jni_messageString.jni_getNative();
    Bar * jni_object = dynamic_cast<Bar *>(
        dynamic_cast<jniBarBase *>( reinterpret_cast<jniRoot *>( 
        jniRoot::jni_getNativeSelf( env, object ) ) )->jni_getNative());
    jniBarBase * nativeSelf = dynamic_cast<jniBarBase *>( 
        reinterpret_cast<jniRoot *>( jniRoot::jni_getNativeSelf( env, self ) ) );
    Bar * reply = reinterpret_cast<Bar *>(
        nativeSelf->jni_getNative())->Bar::bar( jni_message, jni_object );
    jniBarBase * replyBase = jniBarBase::FindJNIBase( reply );
	if( replyBase == 0 )
        replyBase = new jniBarNative( env, reply );
	return replyBase->jni_getJava();
}


/*
{group:JNIstub}
Summary: JNI stub implementation for non-virtual function
Returns: see Bar::foo for doc
Parameters:
    env - the JNI environment for that call
    self - the Java self
    message - see Bar::foo for doc
    object - see Bar::foo for doc
Purpose:
  This function propagates the function call to Bar::foo and
  does the appropriate type conversion:
Note:
  Do not touch this function! It was entirely generated by Metaphor 
at Tue Jun 13 17:17:19 2000 from its IDL source. 
*/
JNIEXPORT jobject JNICALL Java_FooBar_Bar_foo(JNIEnv* env, jobject self
, jstring message, jobject object )
{
    jniString jni_messageString( env, message );
    const char * jni_message = jni_messageString.jni_getNative();
    Bar * jni_object = dynamic_cast<Bar *>(
        dynamic_cast<jniBarBase *>( reinterpret_cast<jniRoot *>( 
        jniRoot::jni_getNativeSelf( env, object ) ) )->jni_getNative());
    jniBarBase * nativeSelf = dynamic_cast<jniBarBase *>( 
        reinterpret_cast<jniRoot *>( jniRoot::jni_getNativeSelf( env, self ) ) );
    Bar * reply = reinterpret_cast<Bar *>(
        nativeSelf->jni_getNative())->Bar::foo( jni_message, jni_object );
    jniBarBase * replyBase = jniBarBase::FindJNIBase( reply );
	if( replyBase == 0 )
        replyBase = new jniBarNative( env, reply );
	return replyBase->jni_getJava();
}

/*
{group:JNIstub}
Summary: JNI stub implementation for non-virtual function
Returns: see Bar::getMessage for doc
Parameters:
    env - the JNI environment for that call
    self - the Java self
Purpose:
  This function propagates the function call to Bar::getMessage and
  does the appropriate type conversion:
Note:
  Do not touch this function! It was entirely generated by Metaphor 
at Tue Jun 13 17:17:19 2000 from its IDL source. 
*/
JNIEXPORT jstring JNICALL Java_FooBar_Bar_getMessage(JNIEnv* env, jobject self
 )
{
    jniBarBase * nativeSelf = dynamic_cast<jniBarBase *>( 
        reinterpret_cast<jniRoot *>( jniRoot::jni_getNativeSelf( env, self ) ) );
    const char * reply = reinterpret_cast<Bar *>(
        nativeSelf->jni_getNative())->Bar::getMessage(  );
    return env->NewStringUTF( reply );
}


Instead of creating a direct instance of the C++ class in question, the jni_create Function creates an instance of the Java wrapper class. That's all.

Although this is a simplified version, the JNI functions for the class members look rather tricky. But it's not hand coded! It's generated by the JNI++ generator. The main idea is to inquire the handle to the C++ wrapper object created by jni_create() and to call the respective method of the native C++ class. The point is, that the Java wrapper's possible override is explicitely skipped: The override in the Java wrapper class is for callbacks only, not for calls from a subclass. The methods of the original C++ classes are called directely.

Both, Bar::foo() and Bar::bar() return a pointer to a C++ instance of class Bar. In order to give the calling Java program access to such an instance, it must be wrapped by JNI++, if not already done. The FindJJNIBase() call on the reply checks this. In case the reply is not wrapped yet, replyBase gets NULL and the JNI stub creates a Java/C++ wrapper for the returned instance. This is covered by JNI++ part 3.

previous  home  next 

Hosted by www.Geocities.ws

1