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.