1、在 C 或 C+中调用 JAVA 方法 JAVA 跨平台的特性使 JAVA 越来越受开发人员的欢迎,但也往往会听到不少的抱怨: 用 JAVA 开发的图形用户窗口界面每次在启动的时候都会跳出一个控制台窗口,这个控制 台窗口让本来非常棒的程序失色不少。怎么能够让通过 JAVA 开发的 GUI 程序不弹出 JAVA 的控制台窗口呢?其实现在很多流行的开发环境例如 JBuilder、Eclipse 都是使用纯 JAVA 开发的集成环境,这些集成环境启动的时候并不会打开一个命令窗口,因为它使用 了 JNI(Java Native Interface)的技术。通过这种技术开发人员不一定要用命令行来启动 J
2、AVA 程序,而可以通过编写一个本地 GUI 程序来直接启动 JAVA 程序,这样就可避免另 外打开一个命令窗口,让我们开发的 JAVA 程序更加专业。 JNI 允许运行在虚拟机的 JAVA 程序能够与其他语言(例如:C 和 C+ )编写的程序 或者库进行相互间的调用。同时 JNI 提供的一整套的 API 允许你将 JAVA 虚拟机直接嵌入 到本地的应用程序中。下图是 SUN 站点上对 JNI 的基本结构的描述: 本文将介绍如何在 C/C+中调用 JAVA 方法并将其间可能涉及到的问题串在一起介绍 整个开发的步骤以及可能遇到的难题和解决方法。本文所采用的工具是 Sun Microsystems
3、 公司创建的 Java Development Kit (JDK) 版本 1.3.1 以及 Microsoft 公司的 Visual C+ 6 开 发环境。 一环境搭建 为了让本文以下部分的代码能够正常工作,我们必须建立一个完整的开发环境。首先 需要下载并安装 JDK1.3.1(可以 http:/ 从下载 SUN 公司的 JDK) 。我们假 设安装路径为 C:JDK。下一步就是设置集成开发环境,通过 Visual C+ 6 的菜单 Tools-Options 打开选项对话框如下:将目录 C:JDKinclude 和 C:JDKincludewin32 加入到开发环境的 Include File
4、s 目录中, 同时将 C:JDKlib 目录添加到开发环境的 Library Files 目录中,这三个目录是 JNI 定义 的一些常量、结构以及方法的头文件和库文件。我们的集成开发环境已经设置完毕, 同时为了执行程序我们需要把 JAVA 虚拟机所用到的动态链接库所在的目录 C:JDKjrebinclassic 设置到系统的 PATH 环境变量中。在这里需要提出一点的是:某 些开发人员为了方便直接将 JRE 所用到的 DLL 文件直接拷贝到系统目录下,这样做 是不行的,将导致初始化 JAVA 虚拟机环境失败(返回值-1 ) ,原因是 JAVA 虚拟机是 以相对路径来寻找所用到的库文件和其他一些
5、相关文件的。至此整个 JNI 的开发环境 设置完毕,为了让我们的此次 JNI 旅程能够顺利进行我们还必须先准备一个 JAVA 类, 在这个类中我们将用到 JAVA 中几乎所有有代表性的属性以及方法,例如:静态方法 与属性、数组、异常抛出与捕捉等等。我们定义的 JAVA 程序(Demo.java)如下,本文 中所有的代码演示都将基于该 JAVA 程序。 package jni.test; /* 该类是为了演示JNI如何访问各种对象属性等* author liudong*/ public class Demo /用于演示如何访问静态的基本类型属性 public static int COUNT =
6、 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 c
7、ounts; /* 演示如何构造一个数组对象*/ public void setCounts(int counts) this.counts = counts; /* 演示异常的捕捉*/ public void throwExcp() throws IllegalAccessException throw new IllegalAccessException(“exception occur.“); 二初始化虚拟机本地代码在调用 JAVA 方法之前必须先加载 JAVA 虚拟机,而后所有的 JAVA 程序都 在虚拟机中执行。为了初始化 JAVA 虚拟机,JNI 提供了一系列的接口函数: Invo
8、cation API 。通过这些 API 我们可以很方便的将虚拟机加载到内存中。创建虚拟机 可以用以下的函数: jint JNI_CreateJavaVM(JavaVM *pvm, void *penv, void *args); 但是这个函数有一点需要注意的是在 JDK1.1 中第三个参数总是指向一个结构 JDK1_1InitArgs, 这个结构无法完全在所有版本的虚拟机中进行无缝移植。在 JDK1.2 中已经使用了一个标准的初始化结构 JavaVMInitArgs 来替代 JDK1_1InitArgs。下面我 们分别给出两种不同版本的示例代码。 1 在 JDK1.1 初始化虚拟机 #inc
9、lude int main() JNIEnv *env;JavaVM *jvm;JDK1_1InitArgs vm_args;jint res;/* IMPORTANT: 版本号设置一定不能漏 */ vm_args.version = 0x00010001; /*获取缺省的虚拟机初始化参数*/JNI_GetDefaultJavaVMInitArgs(/* 添加自定义的类路径 */sprintf(classpath, “%s%c%s“,vm_args.classpath, PATH_SEPARATOR, USER_CLASSPATH);vm_args.classpath = classpath;
10、 /*设置一些其他的初始化参数*/* 创建虚拟机 */res = JNI_CreateJavaVM(if (res DestroyJavaVM(jvm); 2 在 JDK1.2 初始化虚拟机 /* invoke2.c */ #include int main() int res;JavaVM *jvm;JNIEnv *env; JavaVMInitArgs vm_args; JavaVMOption options3; vm_args.version=JNI_VERSION_1_2;/这个字段必须设置为该值 /*设置初始化参数*/ options0.optionString = “-Dpile
11、r=NONE“; options1.optionString = “-Djava.class.path=.“; options2.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( if (res DestroyJavaVM(jvm);fpri
12、ntf(stdout, “Java VM destory.n“); 为了保证 JNI 代码的可移植性,建议使用 JDK1.2 的方法来创建虚拟机。JNI_CreateJavaVM 函数的第二个参数 JNIEnv *env,就是贯穿整个 JNI 始末的一个参数。因为几乎所有的函数 都要求一个参数就是 JNIEnv *env。 三访问类方法 初始化了 JAVA 虚拟机后我们就可以开始调用 JAVA 的方法了。要调用一个 JAVA 对 象的方法必须经过几个步骤: 1 获取指定对象的类定义(jclass) 有两种途径来获取对象的类定义:第一种是在已知类名的情况下使用 FindClass 来 查找对应的
13、类。但是有一点要注意的是类名并不是我们平时写 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 *GetMet
14、hodID)(JNIEnv *env, jclass clazz, const char *name, const char *sig); jmethodID (JNICALL *GetStaticMethodID)(JNIEnv *env, jclass class, const char *name, const char *sig); 这两个函数的区别在于一个(GetStaticMethodID) 是用来获取静态方法的定义,另外 一个则是获取非静态的方法定义。这两个函数都需要提供四个参数:env 就是初始 化虚拟机得到的 JNI 环境;第二个参数 class 是对象的类定义,也就是我们第
15、一步 得到的 obj ;第三个参数是方法名称;最重要的是第四个参数,这个参数是方法的 定义,因为我们知道 JAVA 中允许方法的多态,仅仅是通过方法名并没有办法定 位到一个具体的方法,因此需要第四个参数来指定方法的具体定义。但是怎么利 用一个字符串来表示方法的具体定义呢?不要着急,JDK 中已经为我们准备好一 个反编译工具 javap,通过这个工具我们就可以得到类中每个属性、方法的定义。 下面我们看看 jni.test.Demo 的定义: 打开命令行窗口并运行 javap s p jni.test.Demo 得到运行结果如下: Compiled from Demo.java public cl
16、ass 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/la
17、ng/String; */public int getCounts();/* ()I */public void setCounts(int);/* (I)V */public void throwExcp() throws java.lang.IllegalAccessException;/* ()V */static ; /* ()V */ 我们看到类中每个属性和方法下面都有一段注释,注释中不包含空格的内容就是我们 第四个参数要填的内容( 关于 javap 具体的参数意思请查询 JDK 的使用帮助)。下面这段代 码演示如何访问 jni.test.Demo 的 getMessage 方法:
18、/* 假设我们已经有一个 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); /* 如果该方法是静态的方法那只需要将最后一句代码改为以下写法即可 jst
19、ring msg = (*env)- CallStaticObjectMethod(env, cls, mid); */ 3 调用方法 为了调用对象的某个方法,可以使用函数 CallMethod 或者 CallStaticMethod(访问类的静态方法) ,根据不同的返回类型而定。 这些方法都是使用可变参数的定义,如果访问某个方法需要参数时我们只需要把所有 参数按照顺序填写到方法中就可以。在讲到构造函数的访问时我们将演示如何访问带 参数的构造函数。 四访问类属性 访问类的属性与访问类的方法大体上是一致的,只不过是把方法变成属性而已。 1 获取指定对象的类(jclass) 这一步与访问类方法的第
20、一步完全相同,具体使用参看访问类方法的第一步。 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 为属
21、性名称;第四个参 数同样是为了表达属性的类型,前面我们使用 javap 工具获取类的详细定义的时候有这么 两行: public java.lang.String msg;/* Ljava/lang/String; */ 其中第二行注释的内容(注意要包括冒号,不包括空格)就是第四个参数要填的信息, 这跟访问类方法时是相同的。 3 读取和设置属性值 有了属性的定义要访问属性值就易如反掌了。有几个方法用来读取和设置类的属性, 它们是: GetField,SetField,GetStaticField ,SetStaticField。 比如读取 Demo 类的 msg 属性我们就可以用 GetObje
22、ctField,而访问 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,fie
23、ld2); 五访问构造函数 很多人刚刚接触 JNI 的时候往往会在这一节遇到问题,查遍了整个 jni.h 看到了这样一 个函数 NewObject 应该是可以用来访问类的构造函数。但是该函数需要提供构造函数 的方法定义,其类型是 jmethodID。从前面的内容我们知道要获取方法的定义首先要 知道方法的名称,但是构造函数的名称怎么来填写呢,类名?不行,我试过了。其实 访问构造函数与访问一个普通的类方法大体上是一样的,唯一不同的只是方法名称不 同以及方法调用时不同而已。访问类的构造函数时方法名必须填写” 。下面的代 码演示如何构造一个 Demo 类的实例: jclass cls = (*env)
24、-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 */
25、六数组处理 1. 创建一个新数组: 创建一个数组我们首先应该知道数组元素的类型以及数组的长度,JNI 定义了一 批数组的类型 jArray 以及数组操作的函数 NewArray ,其中 就是数组中元素的类型。例如要创建一个大小为 10 并且每个位置值分别 为 110 的整数数组代码如下: int i = 1; jintArray array; / 定义数组对象 (*env)- NewIntArray(env, 10); for(; iSetIntArrayRegion(env, array, i-1, 1, 2. 访问数组中的数据: 访问数组首先应该知道数组的长度以及元素的类型。现在我们把刚才
26、都数组中每 个元素的值打印出来: int i; /* 获取数组对象的元素个数 */ int len = (*env)-GetArrayLength(env, array); /* 获取数组中的所有元素 */ jint* elems = (*env)- GetIntArrayElements(env, array, 0); for(i=0; iGetStringChars(str,0); env-ReleaseStringChars(str,w_buffer); ZeroMemory(desc,desc_len); /调用字符编码转换函数(Win32 API)将 UNICODE 转为 ASCII
27、 编码格式字符串 /关于函数 WideCharToMultiByte 的使用请参考 MSDN len = WideCharToMultiByte(CP_ACP,0, w_buffer,1024,desc,desc_len,NULL,NULL); /len = wcslen(w_buffer); if(len0 delete buffer; return js; 八异常 由于调用了 JAVA 的方法,因此难免产生操作的异常信息,这些异常没有办法通过 C+本身的异常处理机制来捕捉到。JNI 通过一些函数来获取 JAVA 中抛出的异常信息。 之前我们在 Demo 类中定义了一个方法throwExcp
28、,下面我们将访问该方法并捕捉其 抛出来的异常信息。 /* 假设我们已经构造了一个 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
29、)-ExceptionOccurred(env); if(excp) (*env)-ExceptionClear(env); /通过访问 excp 来获取具体异常信息 /* 在 JAVA 中,大部分的异常信息都是扩展类 java.lang.Exception,因此可以访问 excp 的 toString 或者 getMessage 来获取异常信息的内容。访问这两个方法同我们前 面讲到的如何访问类的方法是相同的。*/ 九线程和同步访问有些时候我们需要使用多线程的方式来访问 JAVA 的方法,我们知道一个 JAVA 虚拟 机是非常消耗系统的内存资源,差不多每个虚拟机需要内存大约在 20M 左右。为
30、了节 省资源我们要求每个线程使用的是同一个虚拟机,这样在整个的 JNI 程序中我们只需 要初始化一个虚拟机就可以了。所有人都是这样想的,但是一旦子线程访问主线程创 建的虚拟机环境变量的时候就会出现类似下面的错误对话框,然后整个程序终止。 其实这里面涉及到两个概念分别是虚拟机(JavaVM *jvm)和虚拟机环境(JNIEnv *env) 。 真正消耗大量系统资源的是 jvm 而不是 env ,jvm 是允许多个线程访问的,但是 env 只能被 创建它本身的线程所访问。每个线程必须创建自己的虚拟机环境 env。这时候会有人提出 疑问,主线程在初始化虚拟机的时候就创建了虚拟机环境 env,那么子线
31、程如果也要创建 自己的虚拟机环境是不是其实也创建了第二个虚拟机呢?为了让子线程能够创建自己的 env,JNI 提供了两个函数:AttachCurrentThread 和 DetachCurrentThread。下面代码就是子 线程访问 JAVA 方法的框架: DWORD WINAPI ThreadProc(PVOID dwParam) JavaVM jvm = (JavaVM*)dwParam; /* 将虚拟机通过参数传入 */ JNIEnv* env; (*jvm)- AttachCurrentThread(jvm, (void*) (*jvm)- DetachCurrentThread(j
32、vm); 十时间 关于时间的话题是我在实际开发中遇到的一个问题。当我们要发布使用了 JNI 的程序 时,我们并不用要求客户一定要安装一个 JAVA 运行环境。我们可以在安装程序中打 包这个运行环境。为了让我们的打包程序利于下载,这就要求这个包要比较小,因此 要去除 JRE 中一些不必要的文件,但是如果我们的程序中用到 JAVA 中的日历类型例 如 java.util.Calendar 等,那么有个文件一定不能去掉,这个文件就是JRE libtzmappings。这是一个时区映射文件,一旦没有该文件你会发现时间操作上经常出 现与正确时间相差几个小时的情况。下面是打包 JRE 中必不可少的文件列表
33、(以 Windows 环境为例) :其中JRE为运行环境的目录,同时这些文件之间的相对路径不 能变。 文件名 目录hpi.dll JREbin ioser12.dll JREbin java.dll JREbin net.dll JREbin verify.dll JREbin zip.dll JREbin jvm.dll JREbinclassic rt.jar JRElib tzmappings JRElib 由于 rt.jar 有差不多 10 兆,但是其中有很大一部分文件我们并不需要,可以根据实际 的应用情况进行删除。例如你的程序如果没有用到 JAVA SWING,就可以把涉及到 swi
34、ng 的文件都删除后重新打包。 附录 1JNI 类型对照表: Java Type Native Type Size in bits boolean jboolean 8, unsigned byte jbyte 8 char jchar 16, unsigned short jshort 16 int jint 32 long jlong 64 float jfloat 32 double jdouble 64 void void N/A 2. JNI 类型结构:(此图摘自 JAVA 官方站点 http:/)参考资料: SUN 站点 JNI 教程 http:/ 作者: 刘冬,珠海市创我科技发展有限公司软件工程师,主要从事 J2EE 方面的开发。 联系电话:0756-3377435-351 电子邮件: