RMI and Native Method Calls (Java Enterprise in a Nutshell) Book Home Java Enterprise in a Nutshell Search this book

3.7. RMI and Native Method Calls

As I mentioned at the beginning of this chapter, RMI is a Java-only remote object scheme, so it doesn't provide a direct connection between objects implemented in different languages, like CORBA does. But, using Java's Native Interface API, it is possible to wrap existing C or C++ code with a Java interface and then export this interface remotely through RMI.

To demonstrate, let's suppose we have some (legacy) native code that implements a service we want to export through RMI to remote clients. We can create an implementation of our ThisOrThatServer interface that uses this native code to implement the doSomething() method on our remote interface. The implementation for a NativeThisOrThatServerImpl is shown in Example 3-6. The only significant difference between this implementation and our original ThisOrThatServerImpl is that the doSomething() method is declared native, so the method body is left empty.

Example 3-6. Remote Object Using a Native Method Implementation

import java.rmi.server.UnicastRemoteObject;
import java.rmi.RemoteException;

public class NativeThisOrThatServerImpl
  extends UnicastRemoteObject implements ThisOrThatServer {

  public NativeThisOrThatServerImpl() throws RemoteException {}

  // Remotely-accessible methods
  public String doThis(String todo) throws RemoteException {
    return doSomething("this", todo);
  }

  public String doThat(String todo) throws RemoteException {
    return doSomething("that", todo);
  }

  // Natively-implemented method
  native private String doSomething(String what, String todo);
}

We can compile this RMI class and generate the stubs and skeletons for it using the RMI compiler, just like with our other RMI examples. But once this is done, we need to provide a native implementation for the doSomething() method. To start, we can generate a C/C++ header file for the native method using the javah tool:

% javah -jni -d . NativeThisOrThatServerImpl

The -jni option tells the javah tool to generate JNI-compliant header files (as opposed to header files based on the earlier native method interface that shipped with Java 1.0). Invoking this command generates a JNI C/C++ header file that looks something like the following:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class NativeThisOrThatServerImpl */
#ifndef _Included_NativeThisOrThatServerImpl
#define _Included_NativeThisOrThatServerImpl
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class: NativeThisOrThatServerImpl
 * Method:    doSomething
 * Signature: (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_NativeThisOrThatServerImpl_doSomething
  (JNIEnv *, jobject, jstring, jstring);
#ifdef __cplusplus
}
#endif
#endif

The only details worth noting in this header file are the inclusion of the jni.h header file, which is provided with the Java SDK, and the single method declaration. The jni.h header file provides declarations and definitions for all of the data structures and utility methods provided by the JNI API. The method declaration has a signature that corresponds to the native method declared on our Java class. When you invoke the doSomething() method on the NativeThisOrThatServerImpl, the Java VM looks for a native method that matches this signature.

Now all we need to do is implement the C/C++ function declared in our JNI-generated header file. This is where we tie our Java method to some legacy native code. In this case, suppose the native code is wrapped up in a single C/C++ function called doSomethingNative(). This function is available in a native library on the server platform (e.g., a DLL file on Windows or a shared library on Unix). We want to use our Java method to invoke this native function, so we can implement the Java_NativeThisOrThatServerImpl_doSomething() function along these lines:

#include <jni.h>
#include "NativeThisOrThatServerImpl.h"
#include "nativeDoSomething.h"
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Native implementation for method doSomething() on class
 * NativeThisOrThatServerImpl.
 */
JNIEXPORT jstring JNICALL Java_NativeThisOrThatServerImpl_doSomething
  (JNIEnv * env, jobject me, jstring what, jstring todo) {
  // Convert the Java strings to native strings
  const char* whatStr = (*env)->GetStringUTFChars(env, what, 0);
  const char* todoStr = (*env)->GetStringUTFChars(env, todo, 0);

  // Call the native method
  char* result = doSomethingNative(whatStr, todoStr);

  // Convert result back to Java string
  jstring res = (*env)->NewStringUTF(env, result);
  return res;
}
#ifdef __cplusplus
}
#endif
#endif

The first part of the function just converts the Java strings (passed in as C jstring data structures) into native char* strings. Then it passes the string arguments into the native doSomethingNative() function, converts the result back into a jstring, and returns it. The JNI system handles the conversion of the jstring into a Java String object in the VM environment.

Once we compile this C/C++ code (linking with the native library that contains the doSomethingNative() function), we can export remote NativeThisOrThatServerImpl objects. Then remote clients can call the doThis() or doThat() methods. These remote method calls in turn cause the invocation of native code on the server, when the object implementation calls its native doSomething() method.

Note that in order for the server object to find its native method, the native library containing the doSomethingNative() function has to be loaded into the server object's VM using the System.loadLibrary() method. You can do this in the application code that uses the native method or by adding a static initializer to the class, you can have the library loaded automatically when the NativeThisOrThatServerImpl class is referenced:

static { System.loadLibrary("methods"); }

The System.loadLibrary() method automatically converts the library name that you provide to a platform-specific file name. So if the previous example is run on a Solaris machine, the Java VM looks for a library file named libmethods.so. On a Windows machine, it looks for methods.dll.

3.7.1. RMI with JNI Versus CORBA

There are pros and cons to using RMI and JNI to export legacy native code using Java remote objects, as opposed to using CORBA. With CORBA, a CORBA object implemented in the same language as the native code (C/C++ for our example) is created and exported on the server. Remote Java clients can get a Java stub to this CORBA object using JavaIDL, or any third-party Java CORBA implementation (see Chapter 4, "Java IDL" for details).

One obvious advantage of the CORBA approach is that you don't need to have Java on the server. Since this is presumably a legacy server, perhaps a mainframe of some sort, finding a stable Java VM and development kit for the platform may be a problem. If a Java implementation isn't available or if installing additional software on the legacy server isn't desirable, CORBA is your only option.

An advantage of the RMI/JNI approach is that you're running Java at both ends of the remote communication and avoiding the use of CORBA entirely. CORBA is a very rich distributed object API, but it may be overkill for your application. Using the simpler RMI API and keeping your code development strictly in Java (with some minimal C/C++ to interface to the legacy code) might be an advantage to you in this case.



Library Navigation Links

Copyright © 2001 O'Reilly & Associates. All rights reserved.