This section presents information on accessing classes, methods, and fields,
and covers threading, memory, and Java1 virtual machine
issues.
Language issues
So far, the native method examples have covered calling standalone C and
C++ functions that either return a result or modify parameters
passed into the function. However, C++ like the Java language uses
instances of classes. If you create a class in one native method, the
reference to this class does not have an equivalent class in the Java language,
which makes it difficult to call functions on the C++ class that was
first created.
One way to handle this situation is to keep a record of the C++ class
reference and pass that back to a proxy or to the calling
program. To ensure the C++ class persists across native method calls, use
the C++ new
operator to create a reference to the C++
object on the stack.
The following code provides a mapping between the Xbase database and Java
language code. The Xbase database has a C++ API and uses an initialization
class to perform subsequent database operations. When the class object is
created, a pointer to this object is returned as a Java language
int
value. You can use a long
or larger value for
machines with greater than 32 bits.
public class CallDB {
public native int initdb();
public native short opendb(String name, int ptr);
public native short GetFieldNo(
String fieldname, int ptr);
static {
System.loadLibrary("dbmaplib");
}
public static void main(String args[]) {
String prefix=null;
CallDB db=new CallDB();
int res=db.initdb();
if(args.length>=1) {
prefix=args[0];
}
System.out.println(db.opendb("MYFILE.DBF", res));
System.out.println(db.GetFieldNo("LASTNAME", res));
System.out.println(db.GetFieldNo("FIRSTNAME", res));
}
}
The return result from the call to the initdb
native
method, the int
value,
is passed to subsequent native method calls. The native code
included in the dbmaplib.cc
library de--references the
Java language object passed in as a parameter and retrieves the object
pointer. The line xbDbf* Myfile=(xbDbf*)ptr;
casts the
int
pointer value to be a pointer of Xbase type
xbDbf
.
#include <jni.h>
#include <xbase/xbase.h>
#include "CallDB.h"
JNIEXPORT jint JNICALL Java_CallDB_initdb(
JNIEnv *env, jobject jobj) {
xbXBase* x;
x= new xbXBase();
xbDbf* Myfile;
Myfile =new xbDbf(x);
return ((jint)Myfile);
}
JNIEXPORT jshort JNICALL Java_CallDB_opendb(
JNIEnv *env, jobject jobj,
jstring dbname, jint ptr) {
xbDbf* Myfile=(xbDbf*)ptr;
return((*Myfile).OpenDatabase( "MYFILE.DBF"));
}
JNIEXPORT jshort JNICALL Java_CallDB_GetFieldNo
(JNIEnv *env, jobject jobj,
jstring fieldname,
jint ptr) {
xbDbf* Myfile=(xbDbf*)ptr;
return((*Myfile).GetFieldNo(
env->GetStringUTFChars(fieldname,0)));
}
Calling Methods
The section on arrays highlighted some reasons for calling Java
language methods from within native code; for example, when you need to
free the result you intend to return. Other uses for calling Java native
methods from within your native code would be if you need to return more
than one result or you just simply
want to modify Java language values from within native code.
Calling a Java language method from
within native code involves the following three steps:
- Retrieve a class reference
- Retrieve a method identifier
- Call the Methods
Retrieve a Class Reference
The first step is to retrieve a reference to
the class that contains the methods you want to access.
To retrieve a reference, you can either use the FindClass
method
or access the jobject
or jclass
argument to
the native method.
Use the FindClass method:
JNIEXPORT void JNICALL Java_ArrayHandler_returnArray
(JNIEnv *env, jobject jobj){
jclass cls = (*env)->FindClass(env, "ClassName");
}
Use the jobject argument:
JNIEXPORT void JNICALL Java_ArrayHandler_returnArray
(JNIEnv *env, jobject jobj){
jclass cls=(*env)->GetObjectClass(env, jobj);
}
or
Use the jclass argument:
JNIEXPORT void JNICALL Java_ArrayHandler_returnArray
(JNIEnv *env, jclass jcls){
jclass cls=jcls;
}
Retrieve a Method Identifier
Once the class has been obtained, the second step is to call
the GetMethodID
function to retrieve an identifier
for a method you select in the class. The identifier is needed when calling
the method of that class instance. Because the Java language supports
method overloading, you also need to specify the particular method
signature you want to call. To find out what signature your Java
language method uses, run the javap
command as
follows:
javap -s Class
The method signature used is displayed as a comment after each
method declaration as shown here:
bash# javap -s ArrayHandler
Compiled from ArrayHandler.java
public class ArrayHandler extends java.lang.Object {
java.lang.String arrayResults[];
/* [Ljava/lang/String; */
static {};
/* ()V */
public ArrayHandler();
/* ()V */
public void displayArray();
/* ()V */
public static void main(java.lang.String[]);
/* ([Ljava/lang/String;)V */
public native void returnArray();
/* ()V */
public void sendArrayResults(java.lang.String[]);
/* ([Ljava/lang/String;)V */
}
Use the GetMethodID
function to call instance methods
in an object instance, or use the GetStaticMethodID
function
to call static method. Their argument lists are the same.
Call the Methods
Third, the matching instance method is called using a
Call<type>Method
function. The type
value
can be Void
, Object
, Boolean
,
Byte
, Char
, Short
, Int
,
Long
, Float
, or Double
.
The parameters to the method can be passed as a comma-separated list, an
array of values to the Call<type>MethodA
function,
or as a va_list
. The va_list
is a construct often
used for variable argument lists in C. CallMethodV
is
the function used to pass a va_list ()
.
Static methods are called in a similar way except the method naming includes an
additional Static identifier, CallStaticByteMethodA
, and the
jclass
value is used instead of jobject
.
The next example returns the object array by calling the
sendArrayResults
method from the ArrayHandler
class.
// ArrayHandler.java
public class ArrayHandler {
private String arrayResults[];
int arraySize=-1;
public native void returnArray();
static{
System.loadLibrary("nativelib");
}
public void sendArrayResults(String results[]) {
arraySize=results.length;
arrayResults=new String[arraySize];
System.arraycopy(results,0,
arrayResults,0,arraySize);
}
public void displayArray() {
for (int i=0; i<arraySize; i++) {
System.out.println("array element
"+i+ "= " + arrayResults[i]);
}
}
public static void main(String args[]) {
String ar[];
ArrayHandler ah= new ArrayHandler();
ah.returnArray();
ah.displayArray();
}
}
The native C++ code is defined as follows:
#include <jni.h>
#include <iostream.h>
#include "ArrayHandler.h"
JNIEXPORT void JNICALL Java_ArrayHandler_returnArray
(JNIEnv *env, jobject jobj){
jobjectArray ret;
int i;
jclass cls;
jmethodID mid;
char *message[5]= {"first",
"second",
"third",
"fourth",
"fifth"};
ret=(jobjectArray)env->NewObjectArray(5,
env->FindClass("java/lang/String"),
env->NewStringUTF(""));
for(i=0;i<5;i++) {
env->SetObjectArrayElement(
ret,i,env->NewStringUTF(message[i]));
}
cls=env->GetObjectClass(jobj);
mid=env->GetMethodID(cls,
"sendArrayResults",
"([Ljava/lang/String;)V");
if (mid == 0) {
cout <<Can't find method sendArrayResults";
return;
}
env->ExceptionClear();
env->CallVoidMethod(jobj, mid, ret);
if(env->ExceptionOccurred()) {
cout << "error occured copying array back" <<endl;
env->ExceptionDescribe();
env->ExceptionClear();
}
return;
}
To build this on Linux, run the following commands:
javac ArrayHandler.java
javah -jni ArrayHandler
g++ -o libnativelib.so
-shared -Wl,-soname,libnative.so
-I/export/home/jdk1.2/include
-I/export/home/jdk1.2/include/linux nativelib.cc
-lc
If you want to specify a super class method; for example,
to call the parent constructor,
you can do so by calling the CallNonvirtual<type>Method
functions.
One important point when calling Java language methods or fields from
within native code is you need to catch any raised exceptions. The
ExceptionClear
function clears any pending
exceptions while the ExceptionOccured
function checks
to see if an exception has been raised in the current JNI session.
Accessing Fields
Accessing Java language fields from within native code is similar to
calling Java language methods. However, the set or field is retrieved
with a field ID, instead of a method ID.
The first thing you need to do is retrieve a field ID. You can
use the GetFieldID
function, but specify the field name
and signature in place of the method name and signature.
Once you have the field ID, call a Get<type>Field
function
to set the field value. The <type>
is the
same as the native type being returned except the j
is dropped
and the first letter is capitalized. For example, the
<type>
value
is Int
for native type jint
, and
Byte
for native type jbyte
.
The Get<type>Field
function result is returned as the
native type. For example, to retrieve the arraySize
field in the ArrayHandler
class, call GetIntField
as shown in the following
example.
The field can be set by calling the env->SetIntField(jobj,
fid, arraysize)
functions. Static fields can be set by calling
SetStaticIntField(jclass, fid, arraysize)
and retrieved
by calling GetStaticIntField(jobj, fid)
.
#include <jni.h>
#include <iostream.h>
#include "ArrayHandler.h"
JNIEXPORT void JNICALL Java_ArrayHandler_returnArray
(JNIEnv *env, jobject jobj){
jobjectArray ret;
int i;
jint arraysize;
jclass cls;
jmethodID mid;
jfieldID fid;
char *message[5]= {"first",
"second",
"third",
"fourth",
"fifth"};
ret=(jobjectArray)env->NewObjectArray(5,
env->FindClass("java/lang/String"),
env->NewStringUTF(""));
for(i=0;i<5;i++) {
env->SetObjectArrayElement(
ret,i,env->NewStringUTF(message[i]));
}
cls=env->GetObjectClass(jobj);
mid=env->GetMethodID(cls,
"sendArrayResults",
"([Ljava/lang/String;)V");
if (mid == 0) {
cout <<Can't find method sendArrayResults";
return;
}
env->ExceptionClear();
env->CallVoidMethod(jobj, mid, ret);
if(env->ExceptionOccurred()) {
cout << "error occured copying
array back" << endl;
env->ExceptionDescribe();
env->ExceptionClear();
}
fid=env->GetFieldID(cls, "arraySize", "I");
if (fid == 0) {
cout <<Can't find field arraySize";
return;
}
arraysize=env->GetIntField(jobj, fid);
if(!env->ExceptionOccurred()) {
cout<< "size=" << arraysize << endl;
} else {
env->ExceptionClear();
}
return;
}
Threads and Synchronization
Although the native library is loaded once per class, individual threads
in an application written in the Java language use their own interface
pointer when calling the native method. If you need to restrict access
to a Java language object from within native code, you can either ensure
that the Java language methods you call have explicit synchronization or
you can use the JNI MonitorEnter
and MonitorExit
functions.
In the Java langauge, code is protected by a monitor whenever
you specify the synchronized
keyword. In the Java programming
language, the monitor enter and exit routines
are normally hidden from the application developer.
In JNI, you need to explicitly delineate the entry and exit pointws
of thread safe code.
The following example uses a Boolean
object to restrict
access to the CallVoidMethod
function.
env->ExceptionClear();
env->MonitorEnter(lock);
env->CallVoidMethod(jobj, mid, ret);
env->MonitorExit(lock);
if(env->ExceptionOccurred()) {
cout << "error occured copying array back" << endl;
env->ExceptionDescribe();
env->ExceptionClear();
}
You may find that in cases where you want access to a local system resource
like a MFC window handle or message queue, it is better to use one
Java Thread
and access the local threaded native event queue or
messaging system from within the native code.
Memory Issues
By default, JNI uses local references when creating objects inside
a native method. This means when the method returns, the references
are eligible to be garbage collected. If you want an object to persist
across native method calls, use a global reference instead. A global
reference is created from a local reference by calling
NewGlobalReference
on the the local reference.
You can explicitly mark a reference for garbage collection by calling
DeleteGlobalRef
on the reference. You can also create a weak
style Global reference that is accessible outside the method, but can
be garbage collected. To create one of these references, call
NewWeakGlobalRef
and DeleteWeakGlobalRef
to mark the reference for garbage collection.
You can even explicitly mark a local reference for garbage collection
by calling the env->DeleteLocalRef(localobject)
method.
This is useful if you are using a large amount of temporary data.
static jobject stringarray=0;
JNIEXPORT void JNICALL Java_ArrayHandler_returnArray
(JNIEnv *env, jobject jobj){
jobjectArray ret;
int i;
jint arraysize;
int asize;
jclass cls, tmpcls;
jmethodID mid;
jfieldID fid;
char *message[5]= {"first",
"second",
"third",
"fourth",
"fifth"};
ret=(jobjectArray)env->NewObjectArray(5,
env->FindClass("java/lang/String"),
env->NewStringUTF(""));
//Make the array available globally
stringarray=env->NewGlobalRef(ret);
//Process array
// ...
//clear local reference when finished..
env->DeleteLocalRef(ret);
}
Invocation
The section on calling methods showed you how to call a method
or field in a Java language program using the
JNI interface and a class loaded using the FindClass
function.
With a little more code, you can create a standalone
program that invokes a Java virtual machine and includes its own JNI interface pointer
that can be used to create instances of Java language classes. In the Java 2
release, the runtime program named java
is a small JNI
application that does exactly that.
You can create a Java virtual machine with a call to JNI_CreateJavaVM
,
and shut the created Java virtual machine down with a call to
JNI_DestroyJavaVM
.
A Java vitual machine might also need some additional environment properties.
These properties can be passed to the JNI_CreateJavaVM
function
in a JavaVMInitArgs
structure.
The JavaVMInitArgs
structure contains a pointer to a
JavaVMOption
value used to store environment information
such as the classpath and Java virtual machine version, or system properties that would
normally be passed on the command line to the program.
When the JNI_CreateJavaVM
function returns, you can
call methods and create instances of classes using the FindClass
and NewObject
functions the same way you would
for embedded native code.
Note: The Java virtual machine invocation used to be only used
for native thread Java virtual machines. Some older Java
virtual machines have a green threads
option that is stable for invocation use. On a Unix platform, you
may also need to explicitly link with -lthread
or -lpthread
.
This next program invokes a Java virtual machine, loads the ArrayHandler
class, and retrieves the arraySize
field which should contain
the value minus one. The Java virtual machine options include the current path
in the classpath and turning the Just-In-Time (JIT) compiler off
-Djava.compiler=NONE
.
#include <jni.h>
void main(int argc, char *argv[], char **envp) {
JavaVMOption options[2];
JavaVMInitArgs vm_args;
JavaVM *jvm;
JNIEnv *env;
long result;
jmethodID mid;
jfieldID fid;
jobject jobj;
jclass cls;
int i, asize;
options[0].optionString = ".";
options[1].optionString = "-Djava.compiler=NONE";
vm_args.version = JNI_VERSION_1_2;
vm_args.options = options;
vm_args.nOptions = 2;
vm_args.ignoreUnrecognized = JNI_FALSE;
result = JNI_CreateJavaVM(
&jvm,(void **)&env, &vm_args);
if(result == JNI_ERR ) {
printf("Error invoking the JVM");
exit (-1);
}
cls = (*env)->FindClass(env,"ArrayHandler");
if( cls == NULL ) {
printf("can't find class ArrayHandler\n");
exit (-1);
}
(*env)->ExceptionClear(env);
mid=(*env)->GetMethodID(env, cls, "<init>", "()V");
jobj=(*env)->NewObject(env, cls, mid);
fid=(*env)->GetFieldID(env, cls, "arraySize", "I");
asize=(*env)->GetIntField(env, jobj, fid);
printf("size of array is %d",asize);
(*jvm)->DestroyJavaVM(jvm);
}
Attaching Threads
After the Java virtual machine is invoked, there is one local thread running
the Java virtual machine. You can create more threads in the local operating
system and attach the Java virtual machine to those new threads. You might
want to do this if your native application is multi-threaded.
Attach the local thread to the Java virtual machine with a call to
AttachCurrentThread
. You need to supply pointers to
the Java virtual machine instance and JNI environment. In the Java 2
platform, you can also specify in the third parameter the thread name
and/or group you want this new thread to live under. It is important
to detach any thread that has been previously attached; otherwise, the
program will not exit when you call DestroyJavaVM
.
#include <jni.h>
#include <pthread.h>
JavaVM *jvm;
void *native_thread(void *arg) {
JNIEnv *env;
jclass cls;
jmethodID mid;
jfieldID fid;
jint result;
jobject jobj;
JavaVMAttachArgs args;
jint asize;
args.version= JNI_VERSION_1_2;
args.name="user";
args.group=NULL;
result=(*jvm)->AttachCurrentThread(
jvm, (void **)&env, &args);
cls = (*env)->FindClass(env,"ArrayHandler");
if( cls == NULL ) {
printf("can't find class ArrayHandler\n");
exit (-1);
}
(*env)->ExceptionClear(env);
mid=(*env)->GetMethodID(env, cls, "<init>", "()V");
jobj=(*env)->NewObject(env, cls, mid);
fid=(*env)->GetFieldID(env, cls, "arraySize", "I");
asize=(*env)->GetIntField(env, jobj, fid);
printf("size of array is %d\n",asize);
(*jvm)->DetachCurrentThread(jvm);
}
void main(int argc, char *argv[], char **envp) {
JavaVMOption *options;
JavaVMInitArgs vm_args;
JNIEnv *env;
jint result;
pthread_t tid;
int thr_id;
int i;
options = (void *)malloc(3 * sizeof(JavaVMOption));
options[0].optionString = "-Djava.class.path=.";
options[1].optionString = "-Djava.compiler=NONE";
vm_args.version = JNI_VERSION_1_2;
vm_args.options = options;
vm_args.nOptions = 2;
vm_args.ignoreUnrecognized = JNI_FALSE;
result = JNI_CreateJavaVM(&jvm,(void **)&env, &vm_args);
if(result == JNI_ERR ) {
printf("Error invoking the JVM");
exit (-1);
}
thr_id=pthread_create(&tid, NULL, native_thread, NULL);
// If you don't have join, sleep instead
//sleep(1000);
pthread_join(tid, NULL);
(*jvm)->DestroyJavaVM(jvm);
exit(0);
}
_______
1 As used on this web site,
the terms "Java virtual
machine" or "JVM" mean a virtual machine
for the Java platform.
[TOP]