Section 13.8. RMI and Native Method Calls


13.8. RMI and Native Method Calls

RMI is a Java-only remote object scheme, so it doesn't provide a direct connection between objects implemented in different languages, as 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 the withdrawal, deposit, and transfer operations for a particular banking system. We can create an implementation of our Account interface that uses this native code to implement the deposit( ), withdraw( ), and TRansfer( ) methods on our remote interface. The implementation for a NativeAccountImpl is shown in Example 13-4. The only significant difference between this implementation and our original AccountImpl is that the deposit( ), withdraw( ), and TRansfer( ) methods are declared native, so the method bodies are not provided here.

Example 13-4. Remote object using a native method implementation
 import java.rmi.server.UnicastRemoteObject; import java.rmi.RemoteException; import java.util.List; import java.util.ListIterator; public class NativeAccountImpl extends                                     UnicastRemoteObject implements Account {   // Our current balance   private float mBalance = 0;   // Name on account   private String mName = "";   // Create a new account with the given name   public NativeAccountImpl(String name) throws RemoteException {     mName = name;   }   public String getName( ) throws RemoteException {     return mName;   }   public float getBalance( ) throws RemoteException {     return mBalance;   }   // Withdraw some funds   native public void withdraw(float amt)     throws RemoteException, InsufficientFundsException;   // Deposit some funds      native public void deposit(float amt) throws RemoteException;   // Move some funds from another (remote) account into this one   native public void transfer(float amt, Account src)     throws RemoteException, InsufficientFundsException;   // Remainder of implementation is identical to AccountImpl... 

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 native implementations for the withdraw( ), deposit( ), and transfer( ) methods. To start, we can generate a C/C++ header file for the native methods using the javah tool:

     % javah -jni -d . NativeAccountImpl 

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 com_oreilly_jent_rmi_NativeAccountImpl */     #ifndef _Included_com_oreilly_jent_rmi_NativeAccountImpl     #define _Included_com_oreilly_jent_rmi_NativeAccountImpl     #ifdef _  _cplusplus     extern "C" {     #endif     // NOTE: Some preprocessor directives deleted for clarity...     /*      * Class: com_oreilly_jent_rmi_NativeAccountImpl      * Method: withdraw      * Signature: (F)V      */     JNIEXPORT void JNICALL Java_com_oreilly_jent_rmi_NativeAccountImpl_withdraw      (JNIEnv *, jobject, jfloat);     /*      * Class: com_oreilly_jent_rmi_NativeAccountImpl      * Method: deposit      * Signature: (F)V      */     JNIEXPORT void JNICALL Java_com_oreilly_jent_rmi_NativeAccountImpl_deposit      (JNIEnv *, jobject, jfloat);     /*      * Class: com_oreilly_jent_rmi_NativeAccountImpl      * Method: transfer      * Signature: (FLcom/oreilly/jent/rmi/Account;)V      */     JNIEXPORT void JNICALL Java_com_oreilly_jent_rmi_NativeAccountImpl_transfer      (JNIEnv *, jobject, jfloat, jobject);     #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 actual method declarations. 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 declarations have a signature that corresponds to the native methods declared on our Java class. When you invoke the withdraw( ) method on the NativeAccountImpl, for example, the Java VM looks for a native method that matches the signature (shown here) for the Java_com_oreilly_jent_rmi_NativeAccountImpl_withdraw function.

Now all we need to do is implement the C/C++ functions declared in our JNI-generated header file. This is where we tie our Java methods to some legacy native code. In this case, suppose the native code is wrapped up in a single C/C++ function called dbInvoke( ). 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 to perform various operations on the database holding our account information, so we can implement the Java_NativeAccountImpl_withdraw( ) function along these lines:

     #include <jni.h>     #include "com_oreilly_jent_rmi_NativeAccountImpl.h"     JNIEXPORT void JNICALL Java_com_oreilly_jent_rmi_NativeAccountImpl_withdraw       (JNIEnv * env, jobject me, jfloat amt) {       // Call the native method that will withdraw the money from the account       // First, get the name of the account holder from the account object       jclass acctCls =          (*env)->FindClass(env, "com/oreilly/jent/rmi/NativeAccountImpl");       jmethodID nameMID = env.GetMethodID(env, acctCls,                                           "getName", "( )Ljava/lang/String;");       jobject nObj = (*env)->CallObjectMethod(env, me, nameMID);       const char* name = (*env)->GetStringUTFChars(env, (jstring)nObj, 0);       // Now call the native function to withdraw money from the account       // stored in the non-JDBC database...       int err = dbInvoke("update ACCOUNT ...");       if (err > 0) {         // IF an error occurs, assume it's a balance problem and throw an         // InsufficientFundsException       jclass excCls =         (*env)->FindClass(env,                           "com/oreilly/jent/rmi/InsufficientFundsException");         (*env)->ThrowNew(env, excCls, "Error during withdrawal.");       }      } 

The first part of the function gets the name of the Account that is the target of the withdrawal. From native code, this simple method call is rather convolutedfirst, we locate the Java class definition through the JNI environment, then we get a handle on the method we want to call, and finally we call the method on the reference to the NativeAccountImpl object passed in as a function argument. The Java String return value of the getName( ) method is returned to us in the native environment as a jobject structure, which we convert to a C/C++ char array. When this is complete, we can then invoke the native dbInvoke( ) function to pass SQL commands to the account database. If some kind of error occurs during the database call, then we throw an exception back to the Java environment.

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

Note that in order for the server object to find its native method, the native library containing the dbInvoke( ) function has to be loaded into the server object's VM using the System.loadLibrary( ) method. You can do this either 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 NativeAccountImpl class is referenced:

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

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

13.8.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 Java IDL, or any third-party Java CORBA implementation (see Chapter 14 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 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.



Java Enterprise in a Nutshell
Java Enterprise in a Nutshell (In a Nutshell (OReilly))
ISBN: 0596101422
EAN: 2147483647
Year: 2004
Pages: 269

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net