Java Tip 54: Returning data in reference arguments via JNI

How to use the JNI to populate an object’s data fields

A Java method, when passed an object reference, can easily populate that object’s data fields and call that object’s methods. The Java method can even create an object, populate its fields, and return a reference to that object. However, when Java is talking to C/C++ via the Java Native Interface (JNI), how does a C/C++ native method talk to the object reference in order to set its data fields or call its methods? Suppose that the native method must create an object, set its data fields to specific values, and then return a reference to that object. How is this done?

The C/C++ world defines an entity known as a pointer. A pointer is nothing more than a variable that holds the machine address of another variable.

Functions defined in C or C++ can take pointers to variables as arguments. For example, the C/C++ program listed below contains an add function that takes two integer arguments, sums them, and returns their total via an integer pointer:

#include <stdio.h>
void add (int, int, int *);
void main (void)
{
   int total;
   add (3, 5, &total);
   printf ("total = %dn", total);
}
void add (int a, int b, int *sum)
{
   *sum = a + b;
}

Java does not allow you to use pointers because they are not part of the language. However, it is possible to simulate a pointer to a sequence of characters with some help from the Java Native Interface (JNI).

If we can simulate a pointer, then we can communicate with an object reference. One way to simulate a pointer is to call one of the object’s methods. For example, suppose that a reference to a StringBuffer object was passed as an argument. If the StringBuffer‘s append method was called then it would be possible to append characters to the StringBuffer. This is shown here:

// ---------------
// useSysInfo.java
//
// SysInfo Driver
// ---------------
public class useSysInfo
{
   public static void main (String [] args)
   {
      System.out.println ("----------nuseSysInfon----------n");
      // ----------------------------------------------------
      // Get and print the PATH environment variable setting.
      // ----------------------------------------------------
      System.out.println ("1. Get PATH environment variable setting.");
      System.out.println ("   Setting returned as a String object.n");
      System.out.println ("PATH = " + SysInfo.getenv ("PATH") + "n");
      // ------------------------------------------------------
      // Get the PATH environment variable setting by returning
      // setting in StringBuffer object referenced by evValue.
      // ------------------------------------------------------
      StringBuffer evValue = new StringBuffer ("");
      SysInfo.getenv ("PATH", evValue);
      // --------------------------------------------
      // Print the PATH environment variable setting.
      // --------------------------------------------
      System.out.println ("2.  Get PATH environment variable setting.");
      System.out.println ("    Setting returned in StringBuffer ref-");
      System.out.println ("    erence object.n");
      System.out.println ("PATH = " + evValue + "n");
      // -------------------------------------------
      // Get and print the native OS version number.
      // -------------------------------------------
      Version v = SysInfo.getVersion ();
      System.out.println ("3. Display native OS version info.");
      System.out.println ("   Info returned in Version object.n");
      System.out.println ("Version Major = " + v.major);
      System.out.println ("Version Minor = " + v.minor);
   }
}

The following statements, taken from the code above, create a new StringBuffer object, evValue, initialize it to the empty string, and pass an evValue reference to the getenv method:

StringBuffer evValue = new StringBuffer ("");
SysInfo.getenv ("PATH", evValue);

Where do you think that getenv will place the value associated with the PATH environment variable? Give up? This value will appear in evValue.

The next listing contains the Java code that loads the SysInfo native DLL and defines two overloaded getenv native methods for obtaining an environment variable’s value along with a getVersion method for obtaining the host operating system’s version number.

public class SysInfo
{
   static
   {
      try
      {
         System.loadLibrary ("SysInfo");
      }
      catch (java.lang.UnsatisfiedLinkError e)
      {
         System.out.println (e);
      }
   }
   public static native String getenv (String evName);
   public static native void getenv (String evName, StringBuffer evValue);
   public static native Version getVersion ();
}

The next chunk of code contains C++ code that defines the bodies for these native methods:

// ==========================
// SysInfo.cpp
//
// Obtain system information.
// ==========================
#include <windows.H>
#include "SysInfo.H"
// ========================================================================
// Java_SysInfo_getenv__Ljava_lang_String_2
//
// Obtain the value of an environment variable.
//
// Arguments:
//
// env    - pointer to pointer to JNI function table
// clazz  - handle of class that contains this method
// EVName - handle of Java String object that contains environment variable 
//          name
//
// Return:
//
// Java String object containing environment variable value
// ========================================================================
JNIEXPORT jstring JNICALL 
        Java_SysInfo_getenv__Ljava_lang_String_2 
        (JNIEnv *env, jclass clazz, jstring EVName)
{
        // Check Java String object argument (environment variable name) to see
    // if null reference passed.
    if (EVName == 0)
            return 0;
    // Get ASCII representation of environment variable name.
    const char *_EVName = env->GetStringUTFChars (EVName, 0);
    // Get environment variable value.
    const char *EVValue = getenv (_EVName);
    // Release memory used to hold ASCII representation.
        env->ReleaseStringUTFChars (EVName, _EVName);
    // Return a null reference if there is no environment variable value 
    // otherwise return this value.
    return (EVValue == 0) ? 0 : env->NewStringUTF (EVValue);
}
// =========================================================================
// Java_SysInfo_getenv__Ljava_lang_String_2Ljava_lang_StringBuffer_2
//
// Obtain the value of an environment variable.
//
// Arguments:
//
// env     - pointer to pointer to JNI function table
// clazz   - handle of class that contains this method
// EVName  - handle of Java String object that contains environment variable 
//           name
// EVValue - handle of Java StringBuffer object that will hold environment 
//           variable value
//
// Return:
//
// none
// =========================================================================
JNIEXPORT void JNICALL 
    Java_SysInfo_getenv__Ljava_lang_String_2Ljava_lang_StringBuffer_2 
        (JNIEnv *env, jclass clazz, jstring EVName, jobject EVValue)
{
    // Check Java String object argument (environment variable name) to see 
    // if null reference passed.
    if (EVName == 0)
            return;
    // Check Java StringBuffer object argument (environment variable value) 
    // to see if null reference passed.
    if (EVValue == 0)
            return;
    // Get ASCII representation of environment variable name.
    const char *_EVName = env->GetStringUTFChars (EVName, 0);
    // Get environment variable value.
    const char *_EVValue = getenv (_EVName);
    // Release memory used to hold ASCII representation.
        env->ReleaseStringUTFChars (EVName, _EVName);
    // Return if there is no environment variable value.
    if (_EVValue == 0)
            return;
    // Obtain the Java StringBuffer class handle that corresponds to the 
    // Java StringBuffer object handle.
    clazz = env->GetObjectClass (EVValue);
    // Obtain the method ID for the StringBuffer append method which takes 
    // a StringBuffer object reference argument and returns a String object 
    // reference.
    jmethodID mid = 
        env->GetMethodID (clazz, 
                          "append", 
                                  "(Ljava/lang/String;)Ljava/lang/StringBuffer;");
    // If this method does not exist then return.
    if (mid == 0)
            return;
    // Create a new Java String object and populate it with the environment
    // variable value.  Obtain a handle to this object.
    jstring _jstring = env->NewStringUTF ((const char *) _EVValue);
    // Call the StringBuffer object's append method passing the handle to 
    // the new Java String object.
    env->CallObjectMethod (EVValue, mid, _jstring);
}
// ====================================================
// Java_SysInfo_getVersion
//
// Get the native operating system's version.
//
// Arguments:
//
// env     - pointer to pointer to JNI function table
// clazz   - handle of class that contains this method
//
// Return:
//
// Version object containing native version information
// ====================================================
JNIEXPORT jobject JNICALL Java_SysInfo_getVersion (JNIEnv *env, jclass clazz)
{
    OSVERSIONINFO osVersionInfo;
    // Get operating system version information.
    osVersionInfo.dwOSVersionInfoSize = sizeof (OSVERSIONINFO);
    GetVersionEx (&osVersionInfo);
    // Attempt to find the Version class.
    clazz = env->FindClass ("Version");
    // If this class does not exist then return null.
    if (clazz == 0)
            return 0;
    // Allocate memory for a new Version class object.  Do not bother calling
    // the constructor (the default constructor does nothing).
    jobject obj = env->AllocObject (clazz);
    // Attempt to find the major field.
    jfieldID fid = env->GetFieldID (clazz, "major", "I");
    // If this field does not exist then return null.
    if (fid == 0)
            return 0;
    // Set the major field to the operating system's major version.
    env->SetIntField (obj, fid, osVersionInfo.dwMajorVersion);
    // Attempt to find the minor field.
    fid = env->GetFieldID (clazz, "minor", "I");
    // If this field does not exist then return null.
    if (fid == 0)
            return 0;
    // Set the minor field to the operating system's minor version.
    env->SetIntField (obj, fid, osVersionInfo.dwMinorVersion);
    return obj;
}

The following Java signature:

public static native void getenv (String evName, StringBuffer evValue);

maps to the following C++ function:

Java_SysInfo_getenv__Ljava_lang_String_2Ljava_lang_StringBuffer_2

This function contains a call to CallObjectMethod, a JNI function, which calls the StringBuffer object’s append method. The append method gives us the ability to append characters to the end of a StringBuffer object. Note that it is important to initialize the StringBuffer object to the empty string before calling this getenv method. If this is not done, then it is possible that the StringBuffer object will have the value of the environment variable appended to whatever is already in the object.

The Java_SysInfo_getVersion function gets the operating system’s version number. This function calls a JNI function called AllocObject to allocate the necessary memory for a Java object. The object’s constructor is not called. A separate JNI function, NewObject could be called to call the constructor after allocating memory (if desired). If you examine this code, you’ll notice that this object is based on a class called Version (see the code snippet below). The major and minor data fields of this object are populated and then a reference to this object is returned.

public class Version
{
   int major;
   int minor;
}

If a class has no constructor, Java gives the class a default constructor that takes no arguments and has no functionality. I do not believe that failing to call a constructor for such a class will result in serious consequences.

For those who are interested, the code that follows contains the Java virtual machine code for an empty constructor.

; 1. Load the "this" reference to this object onto the operand
;    stack.
aload_0 
; 2. Invoke the constructor for the ultimate Object superclass.
invokespecial java/lang/Object/<init>()V 
; 3. Return to the invoker method.
return 

NOTE: This article was built with JDK 1.1.5 and Win32.

Conclusion

This article has shown what happens when a Java native method is called that takes an object reference argument — and C++ code needs to interact with that object via the JNI. Our example showed a getenv method being called with a StringBuffer reference argument. The C++ logic used the JNI to call the append method to append a sequence of characters to the StringBuffer object reference. We also looked at creating an object from C++, populating its data fields, and then returning a reference to this object. We did not call the constructor because there wasn’t one to call besides an empty default constructor. Finally, we looked at the Java virtual machine code that makes up this empty constructor.

Although we could have done all of this in Java without needing to resort to the JNI, you never know when such knowledge might come in handy — especially if your Java application must communicate with legacy code via the JNI.

Jeff is a software engineer employed by EDS
(Electronic Data Systems), a consulting firm founded by Ross Perot
in the 1960s. He specializes in developing security applications
that involve the use of smart cards. He also writes his own Win32
and Java applications that he sells as shareware.

Source: www.infoworld.com