Integrate Java and C++ with Jace

The open source Jace toolkit helps simplify JNI programming

If you didn't know better, you might believe that Sun Microsystems had designed the JNI (Java Native Interface) API with the goal of discouraging Java developers from using it. After all, type safety is nearly nonexistent, error checking is absent, and making a single Java method call requires four or more JNI calls. In addition, you have to manage JNIEnv pointers, you can't use JNI references from multiple threads, you must choose between nine different functions for each possible operation, and retrieving exception information is difficult. I've probably left out a few other problems as well.

Many of these limitations result from JNI's binding to the C language, which itself suffers from poor support for type safety, exception-handling mechanisms, and genericity. While most of today's developers can develop in the more robust language C++, Sun couldn't leave those poor C developers out in the cold, which explains why JNI is the way it is today. Unfortunately, that leaves developers with a difficult and unwieldy API—but there is hope.

Jace is a free, open source toolkit designed to make JNI programming easy. It supports automatic generation of C++ proxy classes from Java classfiles and C++ integration of Java exceptions, arrays, packages, and objects. It manages the lifetimes and thread bindings of Java references transparently. Most importantly, it reduces development costs by allowing you to write code modules that are smaller, easier to understand, and compile-time type safe.

Warning: This article is not for the JNI uninitiated. I make references to JNI's nitty-gritty details, from function calls like NewGlobalRef() and GetFieldID() to the nuances of JNI thread and exception safety. If you're new to JNI, I strongly suggest you check out The Java Tutorial, the JNI specification, and JavaWorld's JNI resources (see Resources for links).

The JNI type system

Jace's fundamental strength is its use of C++ proxy classes to represent Java types. To truly understand the benefit of proxy classes, you first need to review the JNI type system. Sun uses 24 C types in JNI to represent the entire set of possible Java types. JNI has nine primitive types:

  • jboolean
  • jbyte
  • jchar
  • jshort
  • jint
  • jlong
  • jdouble
  • jfloat
  • void

JNI has 14 reference types, as Figure 1 illustrates.

Figure 1. The 14 JNI reference types (Source: Sun Microsystems)

And, finally, JNI has the composite type jvalue, which represents the entire set of primitive and reference types.

The Jace type system

Figure 2 shows a class diagram representing the basic set of Jace types. These classes are your primary interface into the Jace runtime. Not surprising, Jace classes correspond closely with JNI types.

Figure 2. The Jace object model. Click on thumbnail to view full-size image.

The Jace type system is built directly on the 24 JNI types. For every JNI type, Jace has a matching C++ proxy class. The nine JNI primitive types, along with jvalue, jclass, jobject, jstring, and jthrowable, all map directly to corresponding Jace proxy classes. The JNI type jarray and its nine derivative array types all collapse down to one template-based JArray type. In the following sections, I examine each of these C++ proxy classes in detail.

The primitive classes

The nine primitive classes act as the wrappers for the nine primitive JNI values. You use these classes as arguments and return values from other C++ proxy classes:

  /* Retrieve the hashcode for the java.lang.String, "A String"
   */
  JInt hashCode = String( "A String" ).hashCode();

You also use these classes as template parameters to JArray:

  /* Create a byte buffer of size 512
   */
  JArray<JByte> buffer( 512 );

JValue

JValue is the base class for all the proxy classes. It represents the total set of Java primitive and reference types. Therefore, each JValue has a JClass instance that represents the corresponding jclass for the jvalue. You can only construct a JValue by providing it with a JNI jvalue type. JValue then acts as a holder for that jvalue. Most developers don't need to interact with JValues directly.

JClass

JClass represents the JNI type jclass. JClass provides methods to retrieve its jclass and the strings that represent that jclass in different JNI calls (for example, java/lang/Object and Ljava/lang/Object;). The framework uses JClass instances to provide the information necessary to make JNI calls, like GetMethodID(), GetFieldID(), and NewObjectArray(). Most developers don't need to interact with JClasses directly.

JObject

The JObject class, which represents the JNI type jobject, serves as the base class for all reference types. In addition to overriding JValue::getJavaValue(), JObject also provides getJavaObject(). These two methods are functionally equivalent, except that getJavaObject() unwraps the jvalue and places it into a jobject.

Things become interesting with JObject, because Java reference types have some features that Java primitive types don't:

  • Reference types don't own values; they merely refer to them. You can construct JObject subclasses in two ways. First, you can construct them as a reference to an existing Java object. You can do this by using the constructor that takes a jobject (or a jvalue containing a jobject) or by using the C++ copy constructor.

    When you instantiate a JObject subclass, the subclass instance refers itself to the jobject reference provided as the argument. (The instance creates a new global reference to the jobject using NewGlobalRef()):

      using jace::java::net::URL;
      /**
       * @param jurl - A java.net.URL
       */
      JNIEXPORT void JNICALL Java_foo_Bar_someMethod( JNIEnv *env, jobject jURL ) {
        /* You create a reference to jURL; not a new URL
         */
        URL url( jurl );
        /* Now that you've instantiated this C++ proxy object, you can call methods 
         * on jURL with ease—like toString()
         */
        std::string urlString = url.toString();
      }
    

    Second, you can construct a JObject subclass by creating a new Java object. You can create a new object by calling any other subclass constructor. When that happens, the subclass calls the appropriate Java constructor using JNI. After constructing the Java object, the subclass creates a new global reference to it:

      /* Creates a new FileOutputStream on foo.txt.
       */
      jace::java::io::FileOutputStream output( "foo.txt" );
    

    Regardless of how you create a JObject subclass, the subclass's only action upon leaving scope is to call DeleteGlobalRef() on the global reference it created at construction.

  • Reference types may be null. You can detect if a C++ proxy class refers to a null Java object by calling JObject::isNull():

      /**
       * @param jstring - A java.lang.String
       */
      JNIEXPORT void JNICALL Java_foo_Bar_someMethod( JNIEnv *env, jstring
    javaString ) {
        String str( javaString );
        if ( str.isNull() ) {
          cout << "Error - The argument, javaString, must not be
    null." << endl;
        }
      }
    

Throwable and String

Both the Throwable and String C++ proxy classes derive from JObject (as do all reference-type proxy classes). These classes are included (along with a few others) as a core part of the Jace library to provide users with a tighter integration between the C++ and Java languages. (I discuss this in more detail in the "C++ Integration" section.)

Jace features

Jace sports several features, including thread management, exception management, and automatic type conversions, among others. Let's examine those features and more.

Thread management

In JNI, a few thread issues exist:

  • JNIEnv pointers are only valid on the thread from which they are obtained
  • Similarly, most JNI types are only valid on the thread from which they are obtained
  • C++ threads must be attached to the JVM before calling JNI functions

Jace addresses all these issues. First, every function in the Jace library automatically retrieves a JNIEnv pointer that is valid for the current thread.

Second, Jace creates global references to the necessary JNI types. For example, upon construction, a JClass creates a global reference to its jclass member, and a JObject creates a global reference to its jobject member. Unlike local references, which are only valid for the current thread, global references are valid across all threads.

Finally, every function in Jace ensures that the current thread is attached to the JVM before calling any JNI functions.

Exception management

Exception handling is one bane of JNI programming. Jace has two policies for exception handling:

  1. Jace checks the return code of every JNI function it executes. If an error occurs, Jace clears the JNI exception and then throws Jace's JNIException.
  2. If Jace determines that a Java method's invocation has failed because the method has thrown an exception, Jace clears the JNI exception, examines the thrown exception, creates a C++ proxy instance for that exception, and throws the C++ proxy:
      using namespace jace::java::net;
      void readGoogle() {
        try {
          /* When Jace internally executes NewObject, it checks to 
           * see if an exception is thrown. If the JNI function 
           * ExceptionOccurred, returns an exception, Jace clears the 
           * exception, creates a corresponding C++ proxy, and throws it.
           */
          URL url( "http://www.google.com" );
        }
        /* Here, you catch the Jace thrown C++ proxy exception.
         */
        catch ( MalformedURLException& e ) {
          cout << e;
        }
      }
    

Automatic type conversion

Jace provides automatic type conversions between C++ and Java primitive types. You may use a C++ std::string or char* anywhere a C++ proxy requires a java::lang::String. You may also use C++ types, like bool, int, and char, where C++ proxy methods require primitive JNI types, like JBoolean, JInt, and JChar:

  using jace::javax::swing::JFrame;
  JFrame createFrame( const std::string& title, int x, int y ) {
    /* The prototype for JFrame is JFrame( java::lang::String str );.
     * Jace automatically converts between std::string and java::lang::String.
     */
    JFrame frame( title );
    /* The prototype for setLocation is setLocation( JInt x, JInt y );.
     * Jace automatically converts between int and JInt.
     */
    frame.setLocation( x , y ); 
    return frame;
  }

C++ integration

Jace includes a C++ proxy-generating tool, BatchGen. The Jace community used BatchGen on the Java Runtime Environment (JRE)'s rt.jar to generate the C++ proxy classes, which are included in the Jace C++ runtime library. However, the Jace community has modified a few of those generated proxy classes for tighter integration with the C++ language and standard library.

java.lang.Object, for example, has an added operator<<( ostream& out, Object& object );, which lets you write any Object as you would using System.out.println().

java.lang.String has several added methods to integrate itself with std::strings and char*s, including operator+(), operator=(), and operator==().

The community has also modified java.lang.Throwable to derive from std::exception.

Type-safe field and method access

C++ proxy generation is the basis of type-safe access to Java objects. For any given Java classfile, Jace can generate a C++ proxy class that has all the same methods and fields. You call the C++ proxy methods in the same way you would call their Java counterparts. Fields are accessed through methods of the same name:

  /* A Java class
   */
  public class Foo {
    public int aField;
    public String aMethod( URL aURL );
  }
  /* You access the C++ proxy from C++
   */
  Foo foo;
  foo.aField() = 14;
  String result = foo.aMethod( URL( "http://www.google.com" ) );

Jace provides two tools—ProxyGen and BatchGen—that you can use to generate C++ proxy classes from Java classfiles. I describe these tools in this article's "Jace Tools" section.

Type-safe arrays

You can use Jace's template JArray class to access Java arrays type-safely. Behind the scenes, Jace calls the appropriate Get<Type>ArrayElement() and Set<Type>ArrayElement() JNI functions as dictated by the array type:

  JArray<JInt> intArray( 10 ); // Results in a call to NewIntArray
  int i = intArray[ 2 ]; // Results in a call to GetIntArrayElements
  JArray<String> stringArray( 5 ); // Results in a call to NewObjectArray
  std::string str = stringArray[ 2 ]; // Results in a call to GetObjectArrayElement

Jace tools

As mentioned previously, ProxyGen and BatchGen generate C++ proxy classes. ProxyGen generates the header and footer for a classfile. BatchGen generates the headers and footers for all classes in a jar file.

ProxyGen

ProxyGen dumps the header file or source file for a Java classfile to standard output. ProxyGen always includes public methods and fields in the generated C++ proxy class. Depending upon the access level specified, it will also include protected, package, or private fields and methods:

  Usage: ProxyGenerator <class file> <header | source> [ options ]
  Where options can be:
    -protected : Generate protected fields and members
    -package : Generate package fields and members
    -private : Generate private fields and members

BatchGen

BatchGen is similar to ProxyGen in that it generates header and source files for C++ proxy classes. However, instead of reading from one Java classfile, BatchGen reads from a jar or zip file of multiple classfiles. Also, instead of writing the header and source files to standard output, BatchGen writes them to the specified destination directories:

  Usage: BatchGenerate <jar or zip file containing classes>
                       <destination directory for header files>
                       <destination directory for source files>
                       [ options ]
  Where options can be:
    -protected : Generate protected fields and members
    -package : Generate package fields and members
    -private : Generate private fields and members

Where is Jace headed?

Jace's near future involves better support for arrays and improved performance, among other enhancements. For example, Jace will support Java arrays as standard C++ containers, compatible with functions like for_each(). Jace will also support behind-the-scenes caching and prefetching for array elements.

Otherwise, Jace's future is open, based primarily upon the feedback and needs of its user community. Check out its Website and see the benefits yourself.

Toby Reyelts has been working with Java since before its 1.0.2 release. His work has ranged from low-level JVM integration and JNI implementation to high-level distributed application design. He is currently a software architect with Cogita Solutions, where he is responsible for the overall design and implementation of custom enterprise software solutions, primarily built upon the Java 2 Platform, Enterprise Edition (J2EE). He graduated from Georgia Tech with a bachelor's degree in computer science degree and a specialization in operating systems and digital systems design and implementation. With the little free time he has, he likes to research hardware, operating systems, and computer languages.

Learn more about this topic

Join the discussion
Be the first to comment on this article. Our Commenting Policies