Embed Java code into your native apps

Integrate a Java user interface with legacy code on Unix

Like many developers, you want to incorporate new Java technologies into your projects. But currently your code is written in C or in another legacy language, and rewriting your entire application in Java is not a realistic option. In this article you will learn how to embed Java code into your native application and how to integrate GUIs of multiple worlds: Motif, the XToolKit, X Window and OpenGL for Unix, and Swing for Java.

After reading this article you will be able to:

  • Start a JVM from a legacy application
  • Create Java objects from a legacy application
  • Integrate a Swing GUI with the Motif window manager
  • Call back legacy code from Java using a thread-safe architecture

Where to start?

Consider a simple C program with a Motif user interface. By browsing through its menus, currently the user can create OpenGL windows that contain some simple 3D rendering. You have been asked to add a new menu to this application; you want to implement it using the Swing package from the JDK. The Java Native Interface (JNI) API from the Java Runtime Environment provides you with the tools you need to start a JVM from a C program and accomplish the task at hand. Using that JVM, JNI also allows you to create Java objects and call the objects' methods. Assuming that you have written the code needed for a Java class that implements the required menu (see SwingMenu.java for the full Java code, available for download from Resources), you now have to access the JNI from the C code to start the JVM and load your class. Listing 1 illustrates those last two tasks:

Listing 1: Start the JVM and load a class

/** File main.c **/
#include <stdlib.h>
#include <jni.h>
int main (int argc, char *argv[])
{
JNIEnv          *env;
JavaVM          *jvm;
JavaVMInitArgs  vm_args;
JavaVMOption    options[3];
int             nbOptions;
jint            res;
jclass          myJavaClass;
jobject         javaGUI;
jmethodID       constructorID;
char            *classPath, *libraryPath;
/** Code to be added here **/ 
/** Start the Java Virtual Machine **/
classPath = malloc(29);
strcpy(classPath, "-Djava.class.path=/myClassDir");   /* Path of the
SwingManu.class file */
libraryPath = malloc(29);
strcpy(libraryPath, "-Djava.library.path=/myLibDir");     /* Path of
the libNativeMethods.so library */
nbOptions=0;
options[0].optionString = classPath;   nbOptions++;
options[1].optionString = libraryPath; nbOptions++;
vm_args.version  = JNI_VERSION_1_2;                   /* Specifies the JNI
version used */
vm_args.options  = options;
vm_args.nOptions = nbOptions;
vm_args.ignoreUnrecognized = JNI_TRUE;                 /* JNI won't
complain about unrecognized options */
res = JNI_CreateJavaVM(&jvm,(void **)&env,&vm_args);
free(classPath);
free(libraryPath);
/*
 * Create an instance of the Class SwingMenu.
 * Before calling NewObject we must get the constructor
 * of the Java class. This constructor must de passed
 * as an argument to NewObject.
 */
myJavaClass   = (*env)->FindClass(env, "SwingMenu");
constructorID = (*env)->GetMethodID(env,myJavaClass,"<init>", 
"()V");
javaGUI       = (*env)->NewObject(env,myJavaClass,constructorID);
}

Job done! You are now able to start your application. With a few Java method invocations through JNI you can see your Swing menu on the screen. Below is the picture of your application at startup.

A Motif application using a Swing menu. The knight is drawn using OpenGL.

Allow Swing menu to call back legacy app

Now, what happens if you press the Swing buttons Red or Yellow in the figure above? Nothing yet. You would like the knight to change color, but for it to do so, you need to link your Java code to your legacy application. You must find a way to connect the legacy app with the Swing GUI. The java.awt.event package and JNI help complete that task. The AWT event model defines the ActionListener interface, which listens to the user interactions with the menu. That interface includes the actionPerformed() method, which is called when an action event occurs. The SwingMenu class must implement the ActionListener interface, and it must define the actionPerformed() method. In turn, actionPerformed() calls the changeColour() method that you have declared native in the SwingMenu class. Why native? Because changeColour() is actually implemented in C -- hence it can easily communicate with your legacy code. The C implementation is located in a dedicated dynamic library that you have named libNativeMethods.so, and that implementation finally calls your legacy function to change the knight's color.

Below is an example featuring a changeColour() method that ensures you can change the knight's color to red. The code below covers the declaration in the Java code, the actionPerformed() method implementation, and the prototyping in C:

Listing 2: Call a C function from Java

// File SwingMenu.java
// Implements the SwingMenu class
import javax.swing.*;
class SwingMenu extends JFrame implements ActionListener {
  JButton      redButton;
  // Declaring a method native
  private native void changeColour(int colour);
  static { system.loadLibrary("NativeMethods");  }
  // Method to create the Java GUI.
  public void initGUI(int mainWindow) {
     // Create the "Red" button.
     redButton = new JButton("Red");
     redButton.addActionListener(this);
  }
  // Method to handle button events.
  public void actionPerformed(ActionEvent e) {
      String string = e.getActionCommand();
      if (string.equals("Red")) { changeColour(1);  }
  }
}
/**  File nativeMethods.c **/
#include <jni.h>
#include <SwingMenu.h>
/*
 * Functions which is actually called when the changeColour() Java method
is called
 */
JNIEXPORT void JNICALL Java_SwingMenu_changeColour(JNIEnv env, jobject
obj, jint colour)
{
  legacyChangeColour((int)colour);   /* the legacyChangeColour() function
is defined in our legacy application */
  return;
}
jint JNI_Onload(JavaVM *vm, void *reserved)
{
   return (JNI_VERSION_1_2);
}

In Listing 2, the file nativeMethods.c implements the methods declared native. You can automatically generate the included file SwingMenu.h by using the javah tool on the SwingMenu class file. You should not modify that file; SwingMenu defines the prototypes of the C functions implementing the Java methods declared native. As you can see in Listing 2, the method name is slightly more complicated than changeColour().

Note that in the SwingMenu.java file, you must load the library libNativeMethods.so by calling System.loadLibrary(). Performing this call in a static block ensures that the library loads as soon as the Java class loads.

Finally, nativeMethods.c also implements a JNI_Onload() method, which the JVM calls when libNativeMethods.so loads. JNI_Onload() must return the JNI version used by this library. Use JNI 1.2 as specified when starting the JVM from the main program (see Listing 1).

Integrate with the window manager

The window manager is the application that controls the appearance of windows on the screen. This includes stacking order, focus behavior, and the appearance of minimized windows. A Motif application uses a shell widget to communicate with the window manager. That widget accepts the window manager dressing -- borders and buttons that enable the user to move or minimize windows. When a classic application uses many shells independent of one another and the user minimizes the main window, the other shells disappear from the screen. However, if you try this with your application, the Swing shell and its included menu stay on the screen.

To fix that situation, you need to notify the window manager that the shell widget used by the Swing menu is attached to your legacy application's shell. Since X11 window properties are the means of communication with the window manager, you need to change those properties on the underlying window associated with the Swing menu shell. This is classical X Window/Motif programming so I will not provide those details in this article (refer to the attachShell() method in nativeMethods.c available for download in Resources).

But, you may ask, how do I receive access to the underlying window of the Swing menu shell? Use a new Java 2 API: the AWT Native Interface (NI). This API allows you to access the drawing surface of an AWT Canvas. On Unix, this drawing surface is an X11 window. Once you have access to an X11 window, obtaining the shell widget window, which stands at the top of the window's hierarchy, is somewhat easy. So let's focus on how to obtain the drawing surface using the AWT NI.

Strictly speaking, the AWT NI is not a Java interface; C code calls it. In our case, calls to the AWT NI complete in the attachShell() method, which also handles the work needed to attach the Swing shell to the main shell. Since attachShell() is written in C and called from Java code, it is declared native in the SwingMenu class located in the nativeMethods.c file and built into the libNativeMethods.so library. As a result, on Solaris, you must build libNativeMethods.so with the jre/lib/libjawt.so library, which implements the AWT NI.

As mentioned earlier, the drawing surface is only available for an AWT Canvas, and you did not use AWT Canvas when building your Swing menu; only JButton components included in a JFrame container make up that menu. The solution: create a dummy Canvas and add it into the JFrame. As that Canvas shouldn't interest the user, give it a size of 1 pixel by 1 pixel.

Now let's return to your main purpose: Listing 3 includes the C code that implements the attachShell() method. The AWT Canvas passes as an argument from the Java code:

Listing 3: Obtain the underlying windowing information from an AWT Canvas

/** File nativeMethods.c **/
#include <X11/Xlib.h>
#include <jni.h>
#include "jawt_md.h"
#include "SwingMenu.h"
/* The Java object canvas must be  a java.awt.Canvas. */
JNIEXPORT void JNICALL Java_SwingMenu_attachShell(JNIEnv *env, jobject
obj, jobject canvas, jint shellWindow)
{
  JAWT                       awt;  /* defined in jawt.h which is included
in jawt_md.h */
  JAWT_DrawingSurface        *ds;
  JAWT_DrawingSurfaceInfo    *dsi;
  JAWT_X11DrawingSurfaceInfo *dsi_X11;
  jint    lock;
  Window  win;
  Display *dpy;
  awt.version = JAWT_VERSION_1_3;
  if (JAWT_GetAWT(env,&awt) == JNI_FALSE) {
     fprintf(stderr, "Can't get AWT ! Exiting.\n");
     exit(11);
  }
  ds = awt.GetDrawingSurface(env, canvas);
  if (ds == NULL) {
     fprintf(stderr, "Can't get drawind Surface ! Exiting.\n");
     exit(12);
  }
  lock = ds->Lock(ds);
  if ((lock & JAWT_LOCK_ERROR) != 0) {
    fprintf(stderr, "Can't lock drawing surface ! Exiting.\n");
    exit(13);
  }
  dsi = ds->GetDrawingSurfaceInfo(ds);
  if (dsi == NULL) {
    fprintf(stderr, "Can't get drawing Surface Information. Exiting !\n");
    exit(14);
  }
  dsi_X11 = (JAWT_X11DrawingSurfaceInfo*)dsi->platformInfo;
  dpy = dsi_X11->display;
  win = dsi_X11->drawable;
  ds->FreeDrawingSurfaceInfo(dsi);
  ds->Unlock(ds);
  awt.FreeDrawingSurface(ds);
}

The first thing to do is maintain a handle on an AWT structure that contains all the required information to complete the job. Then you can access a JAWT_DrawingSurface, which is the drawing surface. After two switches to a JAWT_DrawingSurfaceInfo and to a JAWT_X11DrawingSurfaceInfo, you obtain the X11 window and the X11 display. Note that before receiving the information, you must lock your drawing surface because Java is multithreaded, and the drawing surface's implementation might not be multithread-safe. In other words, if two separate threads access the drawing surface concurrently, your application may crash. The locking mechanism forces the threads to access the drawing surface one at a time.

Make the architecture more robust

Your Swing menu is now integrated with your legacy application. You have solved the window management issues, and you can call back your legacy application from the Java code. Now you want to create an OpenGL window by pressing one of the Swing buttons. To create such a window on Unix, you need to call OpenGL and glX functions (glX carefully integrates OpenGL with X11). Why can't you proceed like you did before? Simply implement a C function declared native in your Java code and called from the actionPerformed()method. The native function would handle the OpenGL window creation. Don't spend long hours on this approach, as it will most likely fail because of multithreading issues.

Java uses many threads -- one main thread, one for handling events, one for garbage collection, and so forth. That means different threads could concurrently access a function called from Java. Considering your application, for OpenGL and glX functions, the situation is even more complex as functions can also be called from the main thread of the legacy application.

You can assume that OpenGL and glX are multithread-safe -- i.e., they can actually support concurrent access from different threads. But that is nothing more than an assumption; the reality is implementation-dependent.

You might consider implementing the locking system that you used when accessing the drawing surface, but this time, neither OpenGL nor glX provide such a mechanism. You'll need to implement it yourself, which can prove quite complicated if you're unsure which calls need lock protection.

The best solution to solving this architectural issue is simple: OpenGL and glX functions must always be called from the same thread. In your case, they should always be called from the legacy application's main thread. It seems that you return to the initial question: how do you create an OpenGL document from Java? Well, if Java cannot call OpenGL and glX directly -- that is, synchronously -- you need a messaging mechanism. You will use that messaging mechanism to send instructions from Java to your legacy application's main thread.

One messaging mechanism that comes to mind is sockets, a classical means for communicating between processes. When the different processes are located on the same system, you should have no concerns about communication reliability. In these cases, use datagram sockets. They are fast, use a light protocol, and are easy to implement. You'll use datagram sockets to communicate inside the same process but between two threads. The ActionListener Java interface acts as a socket client. It uses a socket to send messages to the legacy application thread. Listing 4 shows how to implement the client side:

Listing 4: Socket client in Java

// File SwingMenu.java
import javax.swing.*;
import java.awt.event.*;
import java.net.DatagramSocket;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.io.IOException;
class SwingMenu extends JFrame implements ActionListener {
   private int            outPort=9970;   // Must be equal to IN_PORT in
main.c
   private DatagramSocket inSocket;
   private DatagramPacket createOGLWin;
   private DatagramPacket animate;
   private native void    changeColour(int colour);
   // Method to create a socket
   private void initSocket() {
      try {
      inSocket = new DatagramSocket();
      } catch (Exception exc) {
      System.out.println("Unable to create a socket");
      System.out.println("Error: "+exc.toString());
      System.exit(99);
      }
      try {
      InetAddress localAddr = InetAddress.getLocalHost();
      String oglWinStr = new String("CreateOGLWindow");
      createOGLWin     = new DatagramPacket(oglWinStr.getBytes(),
oglWinStr.length(),
                                              localAddr, outPort);
      String animateStr = new String("Animate");
      animate           = new DatagramPacket(animateStr.getBytes(),
animateStr.length(),
                                     localAddr, outPort);
      } catch (UnknownHostException exc) {
      System.out.println("Unable to get local host address");
      System.out.println(exc.toString());
      System.exit(99);
      }
   }
   //  Method to handle button events
   public void actionPerformed(ActionEvent evt) {
      String string = evt.getActionCommand();
      if (string.equals("Red")) {
         changeColour(1);
      } else if (string.equals("Yellow")) {
         changeColour(2);
      } else if (string.equals("Create OpenGL window")) {
         try { inSocket.send(createOGLWin); }
         catch (IOException exc) {System.out.println(exc.toString()); }
      } else if (string.equals("Animate")) {
         try { inSocket.send(animate); }
         catch (IOException exc) {System.out.println(exc.toString()); }
      } else if (string.equals("Exit")) {
         System.exit(0);
      }
   }
}

Calling the DatagramSocket() constructor without any argument creates a socket and binds it to any available port on the local host. That socket sends datagram packets. You have to build those packets using the DatagramPacket class. Packets are sent to the local host through a specific port (the socket server binds its socket to this port).

Finally, you need to ensure that the legacy application listens to the messages sent. In the main.c file, all the events concerning the legacy code proceed into the XtAppMainLoop() method. Fortunately, the XtAppAddInput() method lets you add a socket to the current event sources checked by XtAppMainLoop(). That socket is associated with a native callback method provided as an argument to XtAppAddInput(). That callback is built into the legacy application and called from the Xt main loop and therefore executed in the legacy application main thread. Depending on the message sent, the callback calls the legacy method you want to execute.

Listing 5 illustrates the socket creation for the legacy application and the addition of that socket to the event source:

Listing 5: Add a datagram socket as an input source to XtAppMainLoop()

/** File main.c **/
#include &lt;stdio.h&gt;
#include &lt;sys/socket.h&gt;
#include &lt;netinet/in.h&gt;
#include &lt;X11/Intrinsic.h&gt;
#define IN_PORT  9970 /* Must be the same than outPort in SwingMenu.java */
int initInputSocket() {
   int s;
   struct sockaddr_in addIN;
   /*
    * Creating a socket for listening to the requests coming from the JVM.
    */
   if ( (s = socket(AF_INET, SOCK_DGRAM, 0)) &lt; 0 ) {
      perror ("initInputSocket: socket()");
      exit(2);
   }
   addIN.sin_family = AF_INET;
   addIN.sin_port    = IN_PORT;
   addIN.sin_addr.s_addr = INADDR_ANY;
   if ( bind(s, (struct sockaddr *)&addIN, sizeof(addIN) ) &lt; 0 
) {
      perror ("initInputSocket: bind()");
      close(s);
      exit(3);
   }
  return(s);
}
/*
 * Function to proceed the packet received
 */
proceedJavaClientRequest(XtPointer clientData, int *socket, XtInputId *id)
{
  ssize_t size;
  char buf[256];
  /*
   * Note that when proceedJavaClientRequest() is called there is always something to read on the socket.
   * Hence the call to recv() is not blocking.
   */
  size = recv(*socket, buf, sizeof(buf), 0);
  if ( strncmp("CreateOGLWindow", buf, size)==0 ) createOGLwindow(NULL);
  if ( strncmp("Animate", buf, size)==0 )         toggleMotionFromJVM();
}
static XtAppContext app;
static String       fallbackResources[] = {
    "*title: Motif GUI.",
    "*glxarea*width: 400", "*glxarea*height: 400", NULL
};
main (int argc, char *argv[])
{
   int    inputSocket;
   Widget toplevel;
   /*
     * The main thread of the application will act as a datagram socket server.
     * The JVM will be the client. This way the Java code will be able to call
     * some native functions and make sure they are executed in the main thread.
     */
   inputSocket = initInputSocket();
   /*
    * Create the top level shell.
    */
   toplevel = XtVaAppInitialize(&app,
                                "SWING-MOTIF",
                                NULL,
                                0,
                                &argc,
                                argv,
                                fallbackResources,
                                NULL,0);
   /*
    * Adding our socket to the input sources checked by the Xtoolkit.
    */
   XtAppAddInput(app, inputSocket, XtInputReadMask,
             proceedJavaClientRequest, NULL);
   /*
    * Listen for events.
    * This must be the only blocking call in the main thread.
    */
   XtAppMainLoop(app);
}

Compilation, linking, and runtime requirements

You've browsed through many files throughout this article. I'll quickly summarize the source code required by the application presented here.

All of these files are available for download from Resources:

  • main.c is the main executable and the application's native part. It covers the creation of the Motif GUI, the OpenGL rendering, and the JVM startup using JNI. It also behaves as a socket server.
  • SwingMenu.java implements the Swing part of the GUI. It also behaves as a socket client. Finally, it calls native methods, which are implemented in the nativeMethods.c file. Using the javah tool with SwingMenu.java, you also obtain a SwingMenu.h file. SwingMenu.h defines the prototypes for all the native methods in SwingMenu.java.
  • nativeMethods.c implements the methods defined native in SwingMenu.java. It handles the underlying windowing issues and also calls back functions defined in the main executable.

Now, I'll shed some light on how the application is built and the runtime dependencies:

The main executable named swing-motif is built from main.c and must link with the libjvm.so library, which implements the JNI API and the JVM. At runtime the LD_LIBRARY_PATH must point to the directory where that library is located (on Solaris, it is usually /usr/java/jre/lib/sparc).

Compiling SwingMenu.java with javac produces the SwingMenu.class file. The JVM loads it when you call FindClass() from the main executable. Nevertheless, it is your responsibility to tell the JVM the location of the .class file. To do so, specify the classpath in the arguments provided to the JVM at startup (see Listing 1).

nativeMethods.c is built into a dynamic library named libnativeMethods.so. As mentioned before, this library must link to libjawt.so. Library libnativeMethods.so is loaded explicitly in the SwingMenu class. In fact, the JVM completes the job on behalf of the Java class. That is why you must specify where the libnativeMethods.so library is located in the JVM startup arguments. Do that by setting the libraryPath to the proper value (see Listing 1).

Finally, to build the application on Solaris, just copy the three source files mentioned above and the Makefile file in the same directory. Then run make to build the swing-motif executable, the SwingMenu.h header file, the libnativeMethods.so library, and the SwingMenu.class byte code. To start the application, just run the run.ksh script.

The buildtime/runtime requirements are as follows:

  • Solaris 7 or higher
  • JDK 1.2.2 or higher
  • OpenGL 1.2.1 or higher

Conclusion

You have learned how to start a JVM from a legacy application, create Java objects from a legacy application, call back legacy code from Java using a thread-safe architecture, and integrate a Swing GUI with a legacy application using X11, Motif, and OpenGL. The architecture described above helps you achieve the best of both worlds -- Swing and your legacy apps -- without jeopardizing your current assets. In closing, before starting any new development projects, remember that Java runs on many operating systems and is the state-of-the-art development environment for multiplatform support.

Thierry Manfé is an engineer at Sun Microsystems. He works with independent software vendors, making sure their applications run well on Sun systems. He has 10 years of industry experience, developing, porting, performance tuning, and supporting technical applications. He is also a Sun-certified Java programmer.

Learn more about this topic

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