Mixing Java and C/C++ can give you the best of both worlds, once you learn how.
In my career writing C, and now C++, programs, I have seen operating systems and Graphical User Interfaces (GUIs) come and go. As the GUI is typically not portable this can present quite a problem for longer projects and for new releases of existing programs. As a long project progresses the target operating system may change, which may require the GUI to be rewritten. Similarly, as products mature, developers often want to release new versions on new operating systems, once again requiring the GUI code to be rewritten.
A software designer can either use a non-GUI interface, vow to never change operating systems, plan to do large rewrites, or use a portable GUI framework. This article looks at the last option using Java and its Abstract Windowing Toolkit (AWT) as the GUI framework.
Java to the Rescue
Two design goals of Java make it ideal for use as the GUI portion of C++ programs. Java was designed to be portable (both source and binary), and familiar to C/C++ programmers. Java also has quite a rich and rapidly evolving GUI model, and with the attention and books that have grown up around it, there should be no lack of Java expertise available to your company.
Using Java as a GUI front-end is a little different from the approach taken by most other GUI toolkits (see "Platform Independent C++ GUI Toolkits," CUJ, January 1997, for example). Instead of linking your code with a C++ library containing the portable GUI code, you leave the GUI code running in a separate thread under a Java Virtual Machine (JVM). This method allows changes to be made to the GUI code without requiring any changes to the C++ binary.
The method discussed in this article splits a program into two parts. One part is pure Java code that handles all GUI chores. This code consists of complete, self-contained Java classes. The other part consists of pure C++ code that does everything except the GUI. When it must communicate with the user, it instructs the Java side to create a class to display and/or gather the required information.
The Java Native Interface
What makes it possible for Java and C++ to be used co-operatively is the Java Native Interface (JNI). This library provides a set of functions that C++ programs can call to interact with the JVM and Java programs. To use the JNI, a C++ program needs only to link with the appropriate library from Sun's freely available Java Development Kit (JDK). If the Java code alone invokes C++ functions but the C++ code doesn't invoke Jave, this step is unnecessary, but I prefer to have the C++ code start the GUI (Java) code. The JDK also supplies the header file jni.h (which must be included by all C++ files that use JNI routines) and complete documentation for Java and the JNI. Currently the JDK is available at http://java.sun.com/products/jdk. The JDK must be release 1.1 or greater.
Before your C++ program can interact with Java, you must initialize the JVM and save pointers to it. These pointers will be used when the C++ code invokes GUI windows and when it terminates the JVM. The following C++ code fragment initializes the JVM. Note that the classpath must be set to a list of paths where the Java compiled code (class files) resides. For greater portability this path information could be read from a file.
#include <jni.h> /* init argument struct */ JDK1_1InitArgs vm_args; /* native method interface ptr */ JNIEnv* env; /* denotes a Java VM */ JavaVM* jvm; /* 1.1.x JDK */ vm_args.version = 0x00010001; JNI_GetDefaultJavaVMInitArgs(&vm_args); /* change the following for your system */ vm_args.classpath = ".;g:/java/jdk1.1.3/lib/classes.zip;"; /* load and init a Java VM */ JNI_CreateJavaVM(&jvm, &env, &vm_args);Raising a GUI Window
Now that the JVM is running, the C++ program can send it requests (via the JNI) to invoke Java classes. These portable classes use the capabilities of the AWT to create windows, handle mouse and keyboard messages, and perform all the other duties of a GUI. For example, the screen in Figure 1 was created by the Java code in Listing 1. The Java class, GuessGui, inherits from Frame (not Applet) so when it is shown, it has all the components of a window without requiring a browser or applet viewer.
The C++ code that causes the window to be displayed uses the env variable that was filled in by the JNI when the JVM was being initialized. Using this variable, the C++ code gets a series of IDs that refer to individual objects and methods. You use these IDs to invoke the Java methods. For example, the following code uses the env variable from the code fragment above to retrieve an ID to a Java method.
jclass cls = env->FindClass("GuessGui"); jmethodID show = env->GetMethodID(cls, "showWindow", "()V");In the JNI function GetMethodID, the third parameter describes the signature of the method. This parameter is needed to differentiate between two or more methods with the same name, since Java supports method overloading. Signatures are constructed by placing argument codes inside the parentheses and a code representing the return type immediately after. Table 1 shows a list of these codes. For example, showWindow is a method that takes no arguments and has a void return type, so its signature is "()V". A more complicated method that takes a boolean and a float as arguments and returns an int would have a signature of "(ZF)I".
Once a method ID has been returned from the JNI, you can use it, along with an object ID, to invoke a Java method. The first step in creating a Java object is to find the class ID, then the method ID for a constructor of the class, which has the special name "<init>". (The default constructor, with a signature of "()V" will be created by Java if necessary.) Armed with these two IDs, you can call the JNI routine NewObject to create the object and return an object ID.
Putting all of this together raises the Window in Figure 1:
jclass cls = env->FindClass("GuessGui"); jmethodID cons = env->GetMethodID(cls, "<init>", "()V"); jobject obj = env->NewObject(cls, cons); jmethodID show = env->GetMethodID(cls, "showWindow", "()V"); env->CallVoidMethod(obj, show);Invoking the method is accomplished with a different JNI call depending on the return type, static or not, and final or not.
Passing Information Between the Two Layers
Passing information to Java is quite easy. You can either create a Java constructor that takes the information when the Java object is being created, or you can provide a Java method that takes the information, changes the object's state, and then calls repaint(). The results method in Listing 1 provides an example of the second approach.
Passing information back to C++ can be a little harder. One approach is to store the information in the Java object. The object provides methods for the C++ program to check if the information is available; if it is, the Java object returns it. This amounts to polling and often constrains the C++ program if it needs to provide the user with a quick response.
Another, often better, approach is to have the Java object call a C++ function when it has information to pass. To use this approach you need three things: a method prototype in the Java class marked native, the corresponding C++ function in a shared library, and a Java method explicitly loading the shared library.
The native method is just like any other Java method except it is marked native and cannot have a body, just a semi-colon. When it is invoked, the corresponding C++ function is called. All parameters passed to the native method are passed to the C++ method. This convention provides an asynchronous way to pass information to the C++ layer.
The function on the C++ side must reside in a shared library (a DLL under Windows) and must have a name and signature that the JNI can find. The name of the function is created by prepending "Java_", a package name (if used) followed by "_", the class name followed by "_", and then the method name. If, through overloading, two or more native methods exist with the same name then you must add "__" (two underscores) and the parameter codes from Table 1.
The JNI passes two extra parameters to the C++ function. The first is a pointer to JNIEnv and the second is the ID of the Java object. (If the native method is static then this will be an ID of the Java class). The remaining parameters are the Java native method parameters. Since Java's types may not exactly map into C++ types (for example, the type int is not necessarily the same in both languages) the header file "jni.h" defines a set of C++ types that map to Java types. This list appears in Table 2.
Here's an example. A native method defined in Java like this:
package cpp.callback; class comms { public native void signal(string id, int signalNumber); ...would correspond to the C++ function:
JNIEXPORT void JNICALL Java_cpp_callback_comms_signal(JNIEnv* env, jobject obj, jstring id, jint num);The JDK includes a tool, javah, that you can use to produce the C++ function prototypes. In the C++ code you can use primitive types, such as jint, directly. To handle the more complex types, such as jstring, you must use the routines in the JNI.
A Simple Example: GuessGui
The following simple guessing example demonstrates C++ raising a GUI window and information being passed between the GUI and non-GUI components of the application.
The C++ code in Listing 2 raises the window seen in Figure 1. After raising the window the program waits until the user enters a number between 1 and 6, which is passed to the C++ function submitGuess in Listing 3. The C++ program then generates a random number and passes the result back to the Java object through the results method in Listing 1. This process repeats until the user presses the "Bored" button, which generates a call to the C++ function done in Listing 3.
Conclusion
It is relatively easy to use the JNI to separate the GUI and non-GUI code, but you must be careful about several things: lifetime of IDs, cleanup of objects, multiple processes and threads, synchronziation, and exceptions, to name a few. You must then master the AWT to actually interact with the user. This step may seem daunting, but I have found that all GUIs have a steep learning curve. Add to this the popularity of Java, the work being done on graphical tools for Java, the portability of Java, and the ability to change the GUI without re-compiling or re-linking, and we have a powerful contender in the GUI wars. o
Further Reading
Java Native Interface Specification in the documentation for the JDK. http://java.sun.com/products/jdk
Java Tutorial - has a section on Java Native Interface programming. http://www.javasoft.com:80/nav/read/Tutorial/index
Brian Danilko is a director of Formal Solutions in Adelaide, South Australia, where he develops software and writes and teaches courses in C, C++, Java, and OO development. Current projects include embedded TCL, research into literate programming, and C++ source preprocessing. He may be reached at bdanilko@formalsolutions.com.au.