Saturday, May 29, 2004

 

How to call Java in C/C++

1.
作者:刘冬
出处:http://tech.ccidnet.com/pub/disp/Article?columnID=294&articleID=38068&pageNO=1

Java跨平台的特性使Java越来越受开发人员的欢迎,但也往往会听到不少的抱怨:用Java开发的图形用户窗口界面每次在启动的时候都会跳出一个控制台窗口,这个控制台窗口让本来非常棒的界面失色不少。怎么能够让通过Java开发的GUI程序不弹出Java的控制台窗口呢?其实现在很多流行的开发环境例如JBuilder、Eclipse都是使用纯Java开发的集成环境。这些集成环境启动的时候并不会打开一个命令窗口,因为它使用了JNI(Java Native Interface)的技术。通过这种技术,开发人员不一定要用命令行来启动Java程序,可以通过编写一个本地GUI程序直接启动Java程序,这样就可避免另外打开一个命令窗口,让开发的Java程序更加专业。

JNI允许运行在虚拟机的Java程序能够与其它语言(例如C和C++)编写的程序或者类库进行相互间的调用。同时JNI提供的一整套的API,允许将Java虚拟机直接嵌入到本地的应用程序中。

本文将介绍如何在C/C++中调用Java方法,并结合可能涉及到的问题介绍整个开发的步骤及可能遇到的难题和解决方法。本文所采用的工具是Sun公司创建的 Java Development Kit (JDK) 版本 1.3.1,以及微软公司的Visual C++ 6开发环境。


环境搭建

为了让本文以下部分的代码能够正常工作,我们必须建立一个完整的开发环境。首先需要下载并安装JDK 1.3.1,其下载地址为“http://java.sun.com”。假设安装路径为C:\JDK。

将目录C:\JDK\include和C:\JDK\include\win32加入到开发环境的Include Files目录中,同时将C:\JDK\lib目录添加到开发环境的Library Files目录中。这三个目录是JNI定义的一些常量、结构及方法的头文件和库文件。集成开发环境已经设置完毕,同时为了执行程序需要把Java虚拟机所用到的动态链接库所在的目录C:\JDK \jre\bin\classic设置到系统的Path环境变量中。这里需要提出的是,某些开发人员为了方便直接将JRE所用到的DLL文件直接拷贝到系统目录下。这样做是不行的,将导致初始化Java虚拟机环境失败(返回值-1),原因是Java虚拟机是以相对路径来寻找所用到的库文件和其它一些相关文件的。至此整个JNI的开发环境设置完毕,为了让此次JNI旅程能够顺利进行,还必须先准备一个Java类。在这个类中将用到Java中几乎所有有代表性的属性及方法,如静态方法与属性、数组、异常抛出与捕捉等。我们定义的Java程序(Demo.java)如下,本文中所有的代码演示都将基于该Java程序,代码如下:

[code]
package jni.test;
/**
* 该类是为了演示JNI如何访问各种对象属性等
* @author liudong
*/
public class Demo {
//用于演示如何访问静态的基本类型属性
public static int COUNT = 8;
//演示对象型属性
public String msg;
private int[] counts;
public Demo() {
this("缺省构造函数");
}
/**
* 演示如何访问构造器
*/
public Demo(String msg) {
System.out.println(":" + msg);
this.msg = msg;
this.counts = null;
}
/**
* 该方法演示如何访问一个访问以及中文字符的处理
*/
public String getMessage() {
return msg;
}
/**
* 演示数组对象的访问
*/
public int[] getCounts() {
return counts;
}
/**
* 演示如何构造一个数组对象
*/
public void setCounts(int[] counts) {
this.counts = counts;
}
/**
* 演示异常的捕捉
*/
public void throwExcp() throws IllegalAccessException {
throw new IllegalAccessException("exception occur.");
}
}
[/code]

初始化虚拟机

本地代码在调用Java方法之前必须先加载Java虚拟机,而后所有的Java程序都在虚拟机中执行。为了初始化Java虚拟机,JNI提供了一系列的接口函数Invocation API。通过这些API可以很方便地将虚拟机加载到内存中。创建虚拟机可以用函数 jint JNI_CreateJavaVM(JavaVM **pvm, void **penv, void *args)。对于这个函数有一点需要注意的是,在JDK 1.1中第三个参数总是指向一个结构JDK1_ 1InitArgs, 这个结构无法完全在所有版本的虚拟机中进行无缝移植。在JDK 1.2中已经使用了一个标准的初始化结构JavaVMInitArgs来替代JDK1_1InitArgs。下面我们分别给出两种不同版本的示例代码。

在JDK 1.1初始化虚拟机:

[code]
#include
int main() {
JNIEnv *env;
JavaVM *jvm;
JDK1_1InitArgs vm_args;
jint res;
/* IMPORTANT: 版本号设置一定不能漏 */
vm_args.version = 0x00010001;
/*获取缺省的虚拟机初始化参数*/
JNI_GetDefaultJavaVMInitArgs(&vm_args);
/* 添加自定义的类路径 */
sprintf(classpath, "%s%c%s",
vm_args.classpath, PATH_SEPARATOR, USER_CLASSPATH);
vm_args.classpath = classpath;
/*设置一些其他的初始化参数*/
/* 创建虚拟机 */
res = JNI_CreateJavaVM(&jvm,&env,&vm_args);
if (res < 0) {
fprintf(stderr, "Can't create Java VM\n");
exit(1);
}
/*释放虚拟机资源*/
(*jvm)->DestroyJavaVM(jvm);
}
[/code]

JDK 1.2初始化虚拟机:

[code]
/* invoke2.c */
#include
int main() {
int res;
JavaVM *jvm;
JNIEnv *env;
JavaVMInitArgs vm_args;
JavaVMOption options[3];
vm_args.version=JNI_VERSION_1_2;//这个字段必须设置为该值
/*设置初始化参数*/
options[0].optionString = "-Djava.compiler=NONE";
options[1].optionString = "-Djava.class.path=.";
options[2].optionString = "-verbose:jni"; //用于跟踪运行时的信息
/*版本号设置不能漏*/
vm_args.version = JNI_VERSION_1_2;
vm_args.nOptions = 3;
vm_args.options = options;
vm_args.ignoreUnrecognized = JNI_TRUE;
res = JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args);
if (res < 0) {
fprintf(stderr, "Can't create Java VM\n");
exit(1);
}
(*jvm)->DestroyJavaVM(jvm);
fprintf(stdout, "Java VM destory.\n");
}
[/code]

为了保证JNI代码的可移植性,建议使用JDK 1.2的方法来创建虚拟机。JNI_CreateJavaVM函数的第二个参数JNIEnv *env,就是贯穿整个JNI始末的一个参数,因为几乎所有的函数都要求一个参数就是JNIEnv *env。

访问类方法

初始化了Java虚拟机后,就可以开始调用Java的方法。要调用一个Java对象的方法必须经过几个步骤:

1.获取指定对象的类定义(jclass)

有两种途径来获取对象的类定义:第一种是在已知类名的情况下使用FindClass来查找对应的类。但是要注意类名并不同于平时写的Java代码,例如要得到类jni.test.Demo的定义必须调用如下代码:

jclass cls = (*env)->FindClass(env, "jni/test/Demo"); //把点号换成斜杠

然后通过对象直接得到其所对应的类定义:

jclass cls = (*env)-> GetObjectClass(env, obj);
//其中obj是要引用的对象,类型是jobject

2.读取要调用方法的定义(jmethodID)

我们先来看看JNI中获取方法定义的函数:

jmethodID (JNICALL *GetMethodID)(JNIEnv *env, jclass clazz, const char *name,
const char *sig);
jmethodID (JNICALL *GetStaticMethodID)(JNIEnv *env, jclass class, const char
*name, const char *sig);

这两个函数的区别在于GetStaticMethodID是用来获取静态方法的定义,GetMethodID则是获取非静态的方法定义。这两个函数都需要提供四个参数:env就是初始化虚拟机得到的JNI环境;第二个参数class是对象的类定义,也就是第一步得到的obj;第三个参数是方法名称;最重要的是第四个参数,这个参数是方法的定义。因为我们知道Java中允许方法的多态,仅仅是通过方法名并没有办法定位到一个具体的方法,因此需要第四个参数来指定方法的具体定义。但是怎么利用一个字符串来表示方法的具体定义呢?JDK中已经准备好一个反编译工具javap,通过这个工具就可以得到类中每个属性、方法的定义。下面就来看看jni.test.Demo的定义:

打开命令行窗口并运行 javap -s -p jni.test.Demo 得到运行结果如下:

[code]
Compiled from Demo.java
public class jni.test.Demo extends java.lang.Object {
public static int COUNT;
/* I */
public java.lang.String msg;
/* Ljava/lang/String; */
private int counts[];
/* [I */
public jni.test.Demo();
/* ()V */
public jni.test.Demo(java.lang.String);
/* (Ljava/lang/String;)V */
public java.lang.String getMessage();
/* ()Ljava/lang/String; */
public int getCounts()[];
/* ()[I */
public void setCounts(int[]);
/* ([I)V */
public void throwExcp() throws java.lang.IllegalAccessException;
/* ()V */
static {};
/* ()V */
}
[/code]

我们看到类中每个属性和方法下面都有一段注释。注释中不包含空格的内容就是第四个参数要填的内容(关于javap具体参数请查询JDK的使用帮助)。下面这段代码演示如何访问jni.test.Demo的getMessage方法:

[code]
/*
假设我们已经有一个jni.test.Demo的实例obj
*/
jmethodID mid;
jclass cls = (*env)-> GetObjectClass (env, obj); //获取实例的类定义
mid=(*env)->GetMethodID(env,cls,"getMessage"," ()Ljava/lang/String; ");
/*如果mid为0表示获取方法定义失败*/
jstring msg = (*env)-> CallObjectMethod(env, obj, mid);
/*
如果该方法是静态的方法那只需要将最后一句代码改为以下写法即可:
jstring msg = (*env)-> CallStaticObjectMethod(env, cls, mid);
*/
[/code]

3.调用方法

为了调用对象的某个方法,可以使用函数CallMethod或者CallStaticMethod(访问类的静态方法),根据不同的返回类型而定。这些方法都是使用可变参数的定义,如果访问某个方法需要参数时,只需要把所有参数按照顺序填写到方法中就可以。在讲到构造函数的访问时,将演示如何访问带参数的构造函数。

访问类属性

访问类的属性与访问类的方法大体上是一致的,只不过是把方法变成属性而已。

1.获取指定对象的类(jclass)

这一步与访问类方法的第一步完全相同,具体使用参看访问类方法的第一步。

2.读取类属性的定义(jfieldID)

在JNI中是这样定义获取类属性的方法的:

jfieldID (JNICALL *GetFieldID)
(JNIEnv *env, jclass clazz, const char *name, const char *sig);
jfieldID (JNICALL *GetStaticFieldID)
(JNIEnv *env, jclass clazz, const char *name, const char *sig);

这两个函数中第一个参数为JNI环境;clazz为类的定义;name为属性名称;第四个参数同样是为了表达属性的类型。前面我们使用javap工具获取类的详细定义的时候有这样两行:

public java.lang.String msg;
/* Ljava/lang/String; */

其中第二行注释的内容就是第四个参数要填的信息,这跟访问类方法时是相同的。

3.读取和设置属性值

有了属性的定义要访问属性值就很容易了。有几个方法用来读取和设置类的属性,它们是:GetField、SetField、GetStaticField、SetStaticField。比如读取Demo类的msg属性就可以用GetObjectField,而访问COUNT用GetStaticIntField,相关代码如下:

jfieldID field = (*env)->GetFieldID(env,obj,"msg"," Ljava/lang/String;");
jstring msg = (*env)-> GetObjectField(env, cls, field); //msg就是对应Demo的msg
jfieldID field2 = (*env)->GetStaticFieldID(env,obj,"COUNT","I");
jint count = (*env)->GetStaticIntField(env,cls,field2);

访问构造函数

很多人刚刚接触JNI的时候往往会在这一节遇到问题,查遍了整个jni.h看到这样一个函数NewObject,它应该是可以用来访问类的构造函数。但是该函数需要提供构造函数的方法定义,其类型是jmethodID。从前面的内容我们知道要获取方法的定义首先要知道方法的名称,但是构造函数的名称怎么来填写呢?其实访问构造函数与访问一个普通的类方法大体上是一样的,惟一不同的只是方法名称不同及方法调用时不同而已。访问类的构造函数时方法名必须填写“”。下面的代码演示如何构造一个Demo类的实例:

[code]
jclass cls = (*env)->FindClass(env, "jni/test/Demo");
/**
首先通过类的名称获取类的定义,相当于Java中的Class.forName方法
*/
if (cls == 0)

jmethodID mid = (*env)->GetMethodID(env,cls,"","(Ljava/lang/String;)V ");
if(mid == 0)

jobject demo = jenv->NewObject(cls,mid,0);
/**
访问构造函数必须使用NewObject的函数来调用前面获取的构造函数的定义
上面的代码我们构造了一个Demo的实例并传一个空串null
*/
[/code]

数组处理

创建一个新数组

要创建一个数组,我们首先应该知道数组元素的类型及数组长度。JNI定义了一批数组的类型jArray及数组操作的函数NewArray,其中就是数组中元素的类型。例如,要创建一个大小为10并且每个位置值分别为1-10的整数数组,编写代码如下:

[code]
int i = 1;
jintArray array; //定义数组对象
(*env)-> NewIntArray(env, 10);
for(; i<= 10; i++)
(*env)->SetIntArrayRegion(env, array, i-1, 1, &i);
[/code]

访问数组中的数据

访问数组首先应该知道数组的长度及元素的类型。现在我们把创建的数组中的每个元素值打印出来,代码如下:

[code]
int i;
/* 获取数组对象的元素个数 */
int len = (*env)->GetArrayLength(env, array);
/* 获取数组中的所有元素 */
jint* elems = (*env)-> GetIntArrayElements(env, array, 0);
for(i=0; i< len; i++)
printf("ELEMENT %d IS %d\n", i, elems[i]);
[/code]

中文字符的处理往往是让人比较头疼的事情,特别是使用Java语言开发的软件,在JNI这个问题更加突出。由于Java中所有的字符都是Unicode编码,但是在本地方法中,例如用VC编写的程序,如果没有特殊的定义一般都没有使用Unicode的编码方式。为了让本地方法能够访问Java中定义的中文字符及Java访问本地方法产生的中文字符串,我定义了两个方法用来做相互转换。

· 方法一,将Java中文字符串转为本地字符串
[code]
/**
第一个参数是虚拟机的环境指针
第二个参数为待转换的Java字符串定义
第三个参数是本地存储转换后字符串的内存块
第三个参数是内存块的大小
*/
int JStringToChar(JNIEnv *env, jstring str, LPTSTR desc, int desc_len)
{
int len = 0;
if(desc==NULL||str==NULL)
return -1;
//在VC中wchar_t是用来存储宽字节字符(UNICODE)的数据类型
wchar_t *w_buffer = new wchar_t[1024];
ZeroMemory(w_buffer,1024*sizeof(wchar_t));
//使用GetStringChars而不是GetStringUTFChars
wcscpy(w_buffer,env->GetStringChars(str,0));
env->ReleaseStringChars(str,w_buffer);
ZeroMemory(desc,desc_len);
//调用字符编码转换函数(Win32 API)将UNICODE转为ASCII编码格式字符串
//关于函数WideCharToMultiByte的使用请参考MSDN
len = WideCharToMultiByte(CP_ACP,0,w_buffer,1024,desc,desc_len,NULL,NULL);
//len = wcslen(w_buffer);
if(len>0 && len desc[len]=0;
delete[] w_buffer;
return strlen(desc);
}
[/code]

· 方法二,将C的字符串转为Java能识别的Unicode字符串
[code]
jstring NewJString(JNIEnv* env,LPCTSTR str)
{
if(!env || !str)
return 0;
int slen = strlen(str);
jchar* buffer = new jchar[slen];
int len = MultiByteToWideChar(CP_ACP,0,str,strlen(str),buffer,slen);
if(len>0 && len < slen)
buffer[len]=0;
jstring js = env->NewString(buffer,len);
delete [] buffer;
return js;
}
[/code]

异常

由于调用了Java的方法,因此难免产生操作的异常信息。这些异常没有办法通过C++本身的异常处理机制来捕捉到,但JNI可以通过一些函数来获取Java中抛出的异常信息。之前我们在Demo类中定义了一个方法throwExcp,下面将访问该方法并捕捉其抛出来的异常信息,代码如下:

[code]
/**
假设我们已经构造了一个Demo的实例obj,其类定义为cls
*/
jthrowable excp = 0; /* 异常信息定义 */
jmethodID mid=(*env)->GetMethodID(env,cls,"throwExcp","()V");
/*如果mid为0表示获取方法定义失败*/
jstring msg = (*env)-> CallVoidMethod(env, obj, mid);
/* 在调用该方法后会有一个IllegalAccessException的异常抛出 */
excp = (*env)->ExceptionOccurred(env);
if(excp){
(*env)->ExceptionClear(env);
//通过访问excp来获取具体异常信息
/*
在Java中,大部分的异常信息都是扩展类java.lang.Exception,因此可以访问excp的toString
或者getMessage来获取异常信息的内容。访问这两个方法同前面讲到的如何访问类的方法是相同的。
*/
}
[/code]

线程和同步访问

有些时候需要使用多线程的方式来访问Java的方法。我们知道一个Java虚拟机是非常消耗系统的内存资源,差不多每个虚拟机需要内存大约在20MB左右。为了节省资源要求每个线程使用的是同一个虚拟机,这样在整个的JNI程序中只需要初始化一个虚拟机就可以了。所有人都是这样想的,但是一旦子线程访问主线程创建的虚拟机环境变量,系统就会出现错误对话框,然后整个程序终止。

其实这里面涉及到两个概念,它们分别是虚拟机(JavaVM *jvm)和虚拟机环境(JNIEnv *env)。真正消耗大量系统资源的是jvm而不是env,jvm是允许多个线程访问的,但是env只能被创建它本身的线程所访问,而且每个线程必须创建自己的虚拟机环境env。这时候会有人提出疑问,主线程在初始化虚拟机的时候就创建了虚拟机环境env。为了让子线程能够创建自己的env,JNI提供了两个函数:AttachCurrentThread和DetachCurrentThread。下面代码就是子线程访问Java方法的框架:

[code]
DWORD WINAPI ThreadProc(PVOID dwParam)
{
JavaVM jvm = (JavaVM*)dwParam; /* 将虚拟机通过参数传入 */
JNIEnv* env;
(*jvm)-> AttachCurrentThread(jvm, (void**)&env, NULL);
.........
(*jvm)-> DetachCurrentThread(jvm);
}
[/code]

时间

关于时间的话题是我在实际开发中遇到的一个问题。当要发布使用了JNI的程序时,并不一定要求客户要安装一个Java运行环境,因为可以在安装程序中打包这个运行环境。为了让打包程序利于下载,这个包要比较小,因此要去除JRE(Java运行环境)中一些不必要的文件。但是如果程序中用到Java中的日历类型,例如java.util.Calendar等,那么有个文件一定不能去掉,这个文件就是[JRE]\lib\tzmappings。它是一个时区映射文件,一旦没有该文件就会发现时间操作上经常出现与正确时间相差几个小时的情况。下面是打包JRE中必不可少的文件列表(以Windows环境为例),其中[JRE]为运行环境的目录,同时这些文件之间的相对路径不能变。

文件名 目录
hpi.dll [JRE]\bin
ioser12.dll [JRE]\bin
java.dll [JRE]\bin
net.dll [JRE]\bin
verify.dll [JRE]\bin
zip.dll [JRE]\bin
jvm.dll [JRE]\bin\classic
rt.jar [JRE]\lib
tzmappings [JRE]\lib

由于rt.jar有差不多10MB,但是其中有很大一部分文件并不需要,可以根据实际的应用情况进行删除。例如程序如果没有用到Java Swing,就可以把涉及到Swing的文件都删除后重新打包。

2.
Java Tip 17: Integrating Java with C++
Learn how to use C++ code from within a Java application and how to call from C++ to a Java object

http://www.javaworld.com/javaworld/javatips/jw-javatip17.html

Summary
This article explores some of the issues involved in integrating C++ code into a Java program. It shows how to call from a Java object to a C++ object, and how to call from a C++ object to a Java object. Interaction with garbage collection is explored, and a simple framework for integrating Java and C++ is developed. Finally, current solutions are compared with what can be expected from JDK 1.1, as well as from future offerings.

In this article, I'll discuss some of the issues involved in integrating C++ code with a Java application. After a word about why one would want to do this and what some of the hurdles are, I'll build up a working Java program that uses objects written in C++. Along the way, I'll discuss some of the implications of doing this (such as interaction with garbage collection), and I'll present a glimpse of what we can expect in this area in the future.

Why integrate C++ and Java?
Why would you want to integrate C++ code into a Java program in the first place? After all, the Java language was created, in part, to address some of the shortcomings of C++. Actually, there are several reasons why you might want to integrate C++ with Java:



Performance. Even if you're developing for a platform with a just-in-time (JIT) compiler, odds are that the code generated by the JIT runtime is significantly slower than the equivalent C++ code. As JIT technology improves, this should become less of a factor. (In fact, in the near future, good JIT technology may well mean that Java runs faster than the equivalent C++ code.)

For reuse of legacy code and integration into legacy systems.

To directly access hardware or do other low-level activities.

To leverage tools that are not yet available for Java (mature OODBMSes, ANTLR, and so on).

If you take the plunge and decide to integrate Java and C++, you do give up some of the important advantages of a Java-only application. Here are the downsides:



A mixed C++/Java application cannot run as an applet.

You give up pointer safety. Your C++ code is free to miscast objects, access a deleted object, or corrupt memory in any of the other ways that are so easy in C++.

Your code may not be portable.

Your built environment definitely won't be portable -- you'll have to figure out how to put C++ code in a shared library on all platforms of interest.

The APIs for integrating C and Java are works in progress and will very likely change with the move from JDK 1.0.2 to JDK 1.1.
As you can see, integrating Java and C++ is not for the faint of heart! However, if you wish to proceed, read on.

We'll start with a simple example showing how to call C++ methods from Java. We'll then extend this example to show how to support the observer pattern. The observer pattern, in addition to being one of the cornerstones of object-oriented programming, serves as a nice example of the more involved aspects of integrating C++ and Java code. We'll then build a small program to test our Java-wrapped C++ object, and we'll end with a discussion of future directions for Java.

Calling C++ from Java
What's so hard about integrating Java and C++, you ask? After all, SunSoft's Java Tutorial has a section on "Integrating Native Methods into Java Programs" (see Resources). As we'll see, this is adequate for calling C++ methods from Java, but it doesn't give us enough to call Java methods from C++. To do that, we'll need to do a little more work.

As an example, we'll take a simple C++ class that we'd like to use from within Java. We'll assume that this class already exists and that we're not allowed to change it. This class is called "C++::NumberList" (for clarity, I'll prefix all C++ class names with "C++::"). This class implements a simple list of numbers, with methods to add a number to the list, query the size of the list, and get an element from the list. We'll make a Java class whose job it is to represent the C++ class. This Java class, which we'll call NumberListProxy, will have the same three methods, but the implementation of these methods will be to call the C++ equivalents. This is pictured in the following object modeling technique (OMT) diagram:




A Java instance of NumberListProxy needs to hold onto a reference to the corresponding C++ instance of NumberList. This is easy enough, if slightly non-portable: If we're on a platform with 32-bit pointers, we can simply store this pointer in an int; if we're on a platform that uses 64-bit pointers (or we think we might be in the near future), we can store it in a long. The actual code for NumberListProxy is straightforward, if somewhat messy. It uses the mechanisms from the "Integrating Native Methods into Java Programs" section of SunSoft's Java Tutorial.

A first cut at the Java class looks like this:



public class NumberListProxy {
static {
System.loadLibrary("NumberList");
}
NumberListProxy() {
initCppSide();
}
public native void addNumber(int n);
public native int size();
public native int getNumber(int i);
private native void initCppSide();
private int numberListPtr_;
// NumberList*
}


The static section is run when the class is loaded. System.loadLibrary() loads the named shared library, which in our case contains the compiled version of C++::NumberList. Under Solaris, it will expect to find the shared library "libNumberList.so" somewhere in the $LD_LIBRARY_PATH. Shared library naming conventions may differ in other operating systems.

Most of the methods in this class are declared as "native." This means that we will provide a C function to implement them. To write the C functions, we run javah twice, first as "javah NumberListProxy," then as "javah -stubs NumberListProxy." This automatically generates some "glue" code needed for the Java runtime (which it puts in NumberListProxy.c) and generates declarations for the C functions that we are to implement (in NumberListProxy.h).

I chose to implement these functions in a file called NumberListProxyImpl.cc. It begins with some typical #include directives:



//
//
NumberListProxyImpl.cc
//
//
// This file contains the C++ code that implements the stubs generated
// by "javah -stubs NumberListProxy".
cf. NumberListProxy.c.
#include
#include "NumberListProxy.h"
#include "NumberList.h"


is part of the JDK, and includes a number of important system declarations. NumberListProxy.h was generated for us by javah, and includes declarations of the C functions we're about to write. NumberList.h contains the declaration of the C++ class NumberList.

In the NumberListProxy constructor, we call the native method initCppSide(). This method must find or create the C++ object we want to represent. For the purposes of this article, I'll just heap-allocate a new C++ object, although in general we might instead want to link our proxy to a C++ object that was created elsewhere. The implementation of our native method looks like this:



void NumberListProxy_initCppSide(struct HNumberListProxy *javaObj)
{
NumberList* list = new NumberList();
unhand(javaObj)->numberListPtr_ = (long) list;
}


As described in the Java Tutorial, we're passed a "handle" to the Java NumberListProxy object. Our method creates a new C++ object, then attaches it to the numberListPtr_ data member of the Java object.

Now on to the interesting methods. These methods recover a pointer to the C++ object (from the numberListPtr_ data member), then invoke the desired C++ function:



void NumberListProxy_addNumber(struct HNumberListProxy* javaObj,long v)
{
NumberList* list = (NumberList*) unhand(javaObj)->numberListPtr_;
list->addNumber(v);
}
long NumberListProxy_size(struct HNumberListProxy* javaObj)
{
NumberList* list = (NumberList*) unhand(javaObj)->numberListPtr_;
return list->size();
}
long NumberListProxy_getNumber(struct HNumberListProxy* javaObj, long i)
{ NumberList* list = (NumberList*) unhand(javaObj)->numberListPtr_;
return list->getNumber(i);
}


The function names (NumberListProxy_addNumber, and the rest) are determined for us by javah. For more information on this, the types of arguments sent to the function, the unhand() macro, and other details of Java's support for native C functions, please refer to the Java Tutorial.

While this "glue" is somewhat tedious to write, it's fairly straightforward and works well. But what happens when we want to call Java from C++?

Calling Java from C++
Before delving into how to call Java methods from C++, let me explain why this can be necessary. In the diagram I showed earlier, I didn't present the whole story of the C++ class. A more complete picture of the C++ class is shown below:




As you can see, we're dealing with an observable number list. This number list might be modified from many places (from NumberListProxy, or from any C++ object that has a reference to our C++::NumberList object). NumberListProxy is supposed to faithfully represent all of the behavior of C++::NumberList; this should include notifying Java observers when the number list changes. In other words, NumberListProxy needs to be a subclass of java.util.Observable, as pictured here:




It's easy enough to make NumberListProxy a subclass of java.util.Observable, but how does it get notified? Who will call setChanged() and notifyObservers() when C++::NumberList changes? To do this, we'll need a helper class on the C++ side. Luckily, this one helper class will work with any Java observable. This helper class needs to be a subclass of C++::Observer, so it can register with C++::NumberList. When the number list changes, our helper class' update() method will be called. The implementation of our update() method will be to call setChanged() and notifyObservers() on the Java proxy object. This is pictured in OMT:




Before going into the implementation of C++::JavaObservableProxy, let me mention some of the other changes.

NumberListProxy has a new data member: javaProxyPtr_. This is a pointer to the instance of C++JavaObservableProxy. We'll need this later when we discuss object destruction. The only other change to our existing code is a change to our C function NumberListProxy_initCppSide(). It now looks like this:



void NumberListProxy_initCppSide(struct HNumberListProxy *javaObj)
{
NumberList* list = new NumberList();
struct HObservable* observable = (struct HObservable*) javaObj;
JavaObservableProxy* proxy = new JavaObservableProxy(observable, list);
unhand(javaObj)->numberListPtr_ = (long) list;
unhand(javaObj)->javaProxyPtr_ = (long) proxy;
}


Note that we cast javaObj to a pointer to an HObservable. This is OK, because we know that NumberListProxy is a subclass of Observable. The only other change is that we now create a C++::JavaObservableProxy instance and maintain a reference to it. C++::JavaObservableProxy will be written so that it notifies any Java Observable when it detects an update, which is why we needed to cast HNumberListProxy* to HObservable*.

Given the background so far, it may seem that we just need to implement C++::JavaObservableProxy:update() such that it notifies a Java observable. That solution seems conceptually simple, but there is a snag: How do we hold onto a reference to a Java object from within a C++ object?

Maintaining a Java reference in a C++ object
It might seem like we could simply store a handle to a Java object within a C++ object. If this were so, we might code C++::JavaObservableProxy like this:



class JavaObservableProxy public Observer {
public:
JavaObservableProxy(struct HObservable* javaObj, Observable* obs) {
javaObj_ = javaObj;
observedOne_ = obs;
observedOne_->addObserver(this);
}
~JavaObservableProxy() {
observedOne_->deleteObserver(this);
}
void update() {
execute_java_dynamic_method(0, javaObj_, "setChanged",
"()V");
}
private:
struct HObservable* javaObj_;
Observable* observedOne_;
};


Unfortunately, the solution to our dilemma is not so simple. When Java passes you a handle to a Java object, the handle] will remain valid for the duration of the call. It will not necessarily remain valid if you store it on the heap and try to use it later. Why is this so? Because of Java's garbage collection.

First of all, we're trying to maintain a reference to a Java object, but how does the Java runtime know we're maintaining that reference? It doesn't. If no Java object has a reference to the object, the garbage collector might destroy it. In this case, our C++ object would have a dangling reference to an area of memory that used to contain a valid Java object but now might contain something quite different.

Even if we're confident that our Java object won't get garbage collected, we still can't trust a handle to a Java object after a time. The garbage collector might not remove the Java object, but it could very well move it to a different location in memory! The Java spec contains no guarantee against this occurrence. Sun's JDK 1.0.2 (at least under Solaris) won't move Java objects in this way, but there are no guarantees for other runtimes.

What we really need is a way of informing the garbage collector that we plan to maintain a reference to a Java object, and ask for some kind of "global reference" to the Java object that's guaranteed to remain valid. Sadly, JDK 1.0.2 has no such mechanism. (One will probably be available in JDK 1.1; see the end of this article for more information on future directions.) While we're waiting, we can kludge our way around this problem.

To safely keep references to Java objects, we can simply store the references in a Java vector. If we make this vector part of a singleton object that's globally available, the list will never get garbage collected, and we'll be able to get to the list from C++. Within a C++ object, the "reference" to the Java object can actually be an index into the singleton Vector. In my example, I've called the class that maintains this vector JavaObjectHolder. The structure of our entire system is shown below:




JavaObjectHolder is a straightforward Java class. It simply has methods to add an object (returning its index), remove an object, and get an object. For simplicity, I made these methods static. The declarations of the methods look like this:



class JavaObjectHolder {
public static int addObject(Object o) {
... }
public static void removeObject(int handle) {
...
}
public static Object getObject(int handle) {
...
}
}


(The complete source of this class, including exception specifications, can be found at the end of this article.)

Now that we have a way of maintaining references to Java objects, we're in a position to actually implement C++::JavaObservableProxy. The header is straightforward:



#if !defined(JavaObservableProxy_h)
#define JavaObservableProxy_h
#include "Observer.h"
#include "Observable.h"
class JavaObservableProxy : public Observer {
public:
JavaObservableProxy(struct HObservable* javaObj, Observable* obs);
~JavaObservableProxy();
void update();
private:
int javaObjectId_;
Observable* observedOne_;
};
#endif



Within the implementation file (JavaObservableProxy.cc), we first define a convenience function to get us the class descriptor of the JavaObjectHolder class:



static ClassClass* javaObjectHolder()
// Give a pointer to the class descriptor for JavaObjectHolder
{
static ClassClass* result = 0;
if (result == 0) {
result = FindClass(0, "JavaObjectHolder", TRUE);
assert(result != 0);
}
return result;
}


This is faster than calling the Java library function FindClass() each time.

Next, we need to write the constructor. The constructor simply calls JavaObjectHolder.addObject to convert the handle into an integer that's safe to store on the heap:



JavaObservableProxy::JavaObservableProxy(
struct HObservable* javaObj,
Observable* obs)
{
javaObjectId_ = execute_java_static_method(
0, javaObjectHolder(), "addObject",
"(Ljava/lang/Object;)I",
javaObj);
observedOne_ = obs;
observedOne_->addObserver(this);
}


When the C++::JavaObservableProxy is destroyed, we'll need to have the matching call to javaObjectHolder.removeObject()...



JavaObservableProxy::~JavaObservableProxy()
{
observedOne_->deleteObserver(this);
execute_java_static_method(
0, javaObjectHolder(), "removeObject",
"(I)V", javaObjectId_);
javaObjectId_ = -1;
}


Finally, we have the infrastructure we need. All that's left is to implement C++::JavaObservableProxy::update()...



void JavaObservableProxy::update()
{
HObject* javaObj = (HObject*)
execute_java_static_method(
0, javaObjectHolder(), "getObject",
"(I)Ljava/lang/Object;",
javaObjectId_);
// If an exception occurred, get back to the Java runtime, because
// invoking another method would clear the exception flag.
if (exceptionOccurred(EE()))
return;
execute_java_dynamic_method(0, javaObj, "setChanged",
"()V");
if (exceptionOccurred(EE()))
return;
execute_java_dynamic_method(0, javaObj, "notifyObservers",
"()V");
}


This method gets a handle to the Java proxy (by calling JavaObjectHolder.getObject()), then executes setChanged() and notifyObservers() on the Java object.

Garbage collection revisited
In this discussion, we've created a number of instances, but we haven't said anything about how to clean them up. Remember, this is C++, and we need to manually dispose of the objects we create!

An obvious place to dispose of the C++ objects we create would be within the finalize() method of NumberListProxy. Unfortunately, this won't work because there is a circular reference: NumberListProxy maintains a reference to C++::JavaObservableProxy, and C++::JavaObservableProxy maintains a reference to NumberListProxy (by going through the static Vector inside JavaObjectHolder). There is no way for Java's garbage collector to detect this circular reference, so NumberListProxy instance will never be collected.

To get around this, we must resort to manual memory management. We add a method called "detach()" to NumberListProxy. When the Java side is done with a NumberListProxy instance, it must call NumberListProxy.detach(). This method can free all of the C++ instances that are created. (Some languages support a concept called "weak references" that can solve problems of this nature in an automatic fashion. Weak references are not a part of Java, and a discussion of them would be beyond the scope of this article.)

Putting it all together
To demonstrate the system we've just developed, I created a simple application to exercise it. This application creates a number list, establishes an observer, and adds a few numbers to the list. Whenever a number is added, the observer is notified, and it prints a message to stdout. The Java observer is quite simple:



import java.util.*;
class NumberListObserver implements Observer {
NumberListObserver(NumberListProxy subject) {
subject_ = subject;
subject.addObserver(this);
}
/**
* Called when the subject changed
* @param o not used
* @param arg not used
**/
public void update(Observable o, Object arg) {
synchronized (subject_) {
// Don't want size() to change under us!
int sz = subject_.size();
System.out.print("
The list now has: ");
for (int i = 0; i < sz; i++) {
if (i > 0)
System.out.print(", ");
System.out.print(subject_.getNumber(i));
}
}
System.out.println("");
}
private NumberListProxy subject_;
// Thing being observed }



The main program looks like this:



import java.util.*;
class TestNumberList {
public static void main(String args[]) {
NumberListProxy model = new NumberListProxy();
NumberListObserver obs = new NumberListObserver(model);
System.out.println("Adding 3 to the list...");
model.addNumber(3);
System.out.println("Adding 42 to the list...");
model.addNumber(42);
System.out.println("Adding 666 to the list...");
model.addNumber(666);
System.out.println("Adding 7 to the list...");
model.addNumber(7);
model.deleteObserver(obs);
model.detach();
}
}


Running the program yields this output:



billf@pluto:~/javaC++Article/src$ java TestNumberList
Adding 3 to the list...
The list now has: 3
Adding 42 to the list...
The list now has: 3, 42
Adding 666 to the list...
The list now has: 3, 42, 666
Adding 7 to the list...
The list now has: 3, 42, 666, 7


JDK 1.1 and beyond
In their recent announcement, JavaSoft informed us that JDK 1.1 will have a "new Java native method interface" (see Resources). Hopefully, this new interface will provide a mechanism for getting a global reference to a Java object.

Netscape has comprehensive documentation on their JRI native method interface (see Resources). JRI ships with Netscape 3.0, and it provides everything we need in the way of registering global references. Unfortunately, it's only implemented by Netscape. Hopefully, JavaSoft will implement something similar, if not exactly the same. (Dare I hope that the JDK 1.1 will have something along the lines of Netscape's JRI?)

ILOG has announced a project called TwinPeaks (being developed with JavaSoft -- see Resources), which promises to "deliver Internet-ready C++ business application components to developers and customers." I expect this means that it will automate the writing of some of the glue code that we wrote by hand in this article. It will probably include other useful tools -- perhaps a debugger that can step from Java into C++.

Getting the source code
If you'd like the complete source code for the program developed in this article (complete with Solaris Makefile), click for javaAndC++.tar.gz, or for javacpp.zip. If you'd just like to browse, here are the source files:



Java source files:


JavaObjectHolder.java

NumberListObserver.java

NumberListProxy.java

TestNumberList.java

C++ source file:


JavaObservableProxy.h

JavaObservableProxy.cc


NumberList.h

NumberList.cc


NumberListProxyImpl.cc


Observable.h


Observable.cc


Observer.h


Other


Makefile (for Solaris-based
systems)


Conclusions
Integrating C++ classes into a Java application is fairly straightforward, although somewhat messy. We expect the standard native method APIs to improve in the near future. Once this happens, the general approach outlined in this article will continue to work, but the implementation will be easier (and performance will be better).

Accessing C++ from Java can be worth the inconvenience. It opens up large bodies of "legacy" C++ code to potential reuse within Java applications. It also lets us exploit the power of C++, where C++ has an advantage -- that is, in performance (at least for now), in directly accessing hardware, in doing other low-level activities, and so on. With this power comes danger, however. We lose the pointer safety that Java provides us, opening ourselves up to memory-corruption bugs. In other words, integrating C++ with Java can be a powerful technique but one to be used with care!



<< Home

This page is powered by Blogger. Isn't yours?