Jni函数调用
Chap5:jni教程(very very good) 19
Chap8:如何将java传递过来的jbyteArray转换成C/C++中的BYTE数组... 47
Chap5:使用JNI技术实现java程序调用第三方dll(c/c++)文件的功能... 47
1、实例一:在jni中调用标准c中自带的函数printf(): 57
3、实例三、在jni函数中访问java类中的对象实例域... 58
5、实例五:在jni函数中调用java对象的方法... 60
6、实例六:在jni函数中调用java类的静态方法... 61
Chap10:在 Windows 中实现 Java 本地方法... 66
Chap12:基本JNI调用技术(c/c++与java互调) 93
Chap13:JNI的c代码中,另外一个线程获取 JNIEnv. 96
chap 14:当JNI遇到多线程--java对象如何被C++中的多个线程访问?. 97
chap 17:使用 Java Native Interface 的最佳实践... 106
四、 C/C++访问Java成员变量和成员方法... 138
System.loadLibrary("HelloWorld"); 149
JNIEXPORT void JNICALL Java_HelloWorld_print (JNIEnv *, jobject); 150
1. 在一般的Java类中定义native方法... 156
注:chap1~13, JNI 函数编写教程,其中chap5讲得好;
Chap14~, JNIEnv和多线程,其中chap17讲得好。
Chap1:JNI完全手册
最近在公司里做了一个手机的项目,需要JAVA程序在发送短信的时候和第三方的短信服务器连接。短信接口是用C++ 写的。琢磨了三天,大致搞懂了JNI的主体部分。先将心得整理,希望各位朋友少走弯路。
首先引用一篇文章,介绍一个简单的JNI的调用的过程。
JAVA以其跨平台的特性深受人们喜爱,而又正由于它的跨平台的目的,使得它和本地机器的各种内部联系变得很少,约束了它的功能。解决JAVA对本地操作的一种方法就是JNI。
JAVA通过JNI调用本地方法,而本地方法是以库文件的形式存放的(在WINDOWS平台上是DLL文件形式,在UNIX机器上是SO文件形式)。通过调用本地的库文件的内部方法,使JAVA可以实现和本地机器的紧密联系,调用系统级的各接口方法。
简单介绍及应用如下:
一、JAVA中所需要做的工作
在JAVA程序中,首先需要在类中声明所调用的库名称,如下:
static {
System.loadLibrary(“goodluck”);
}
在这里,库的扩展名字可以不用写出来,究竟是DLL还是SO,由系统自己判断。
还需要对将要调用的方法做本地声明,关键字为native。并且只需要声明,而不需要具 体实现。如下:
public native static void set(int i);
public native static int get();
然后编译该JAVA程序文件,生成CLASS,再用JAVAH命令,JNI就会生成C/C++ 的头文件。
例如程序testdll.java,内容为:
public class testdll
{
static
{
System.loadLibrary("goodluck");
}
public native static int get();
public native static void set(int i);
public static void main(String[] args)
{
testdll test = new testdll();
test.set(10);
System.out.println(test.get());
}
}
用javac testdll.java编译它,会生成testdll.class。
再用javah testdll,则会在当前目录下生成testdll.h文件,这个文件需要被C/C++ 程序调用来生成所需的库文件。
二、C/C++ 中所需要做的工作
对于已生成的.h头文件,C/C++ 所需要做的,就是把它的各个方法具体的实现。然后编译连接成库文件即可。再把库文件拷贝到JAVA程序的路径下面,就可以用JAVA调用C/C++ 所实现的功能了。
接上例子。我们先看一下testdll.h文件的内容:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include
/* Header for class testdll */
#ifndef _Included_testdll
#define _Included_testdll
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: testdll
* Method: get
* Signature: ()I
*/
JNIEXPORT jint JNICALL Java _testdll_get (JNIEnv *, jclass);
/*
* Class: testdll
* Method: set
* Signature: (I)V
*/
JNIEXPORT void JNICALL Java _testdll_set (JNIEnv *, jclass, jint);
#ifdef __cplusplus
}
#endif
#endif
在具体实现的时候,我们只关心两个函数原型
JNIEXPORT jint JNICALL Java _testdll_get (JNIEnv *, jclass); 和
JNIEXPORT void JNICALL Java _testdll_set (JNIEnv *, jclass, jint);
这里JNIEXPORT和JNICALL都是JNI的关键字,表示此函数是要被JNI调用的。而jint是以JNI为中介使JAVA的int类型与本地 的int沟通的一种类型,我们可以视而不见,就当做int使用。函数的名称是JAVA_再加上java程序的package路径再加函数名组成的。参数 中,我们也只需要关心在JAVA程序中存在的参数,至于JNIEnv*和jclass我们一般没有必要去碰它。
好,下面我们用testdll.cpp文件具体实现这两个函数:
#include "testdll.h"
int i = 0;
JNIEXPORT jint JNICALL Java _testdll_get (JNIEnv *, jclass)
{
return i;
}
JNIEXPORT void JNICALL Java _testdll_set (JNIEnv *, jclass, jint j)
{
i = j;
}
编译连接成库文件,本例是在WINDOWS下做的,生成的是DLL文件。并且名称要与JAVA中需要调用的一致,这里就是goodluck.dll 。把goodluck.dll拷贝到testdll.class的目录下,java testdll运行它,就可以观察到结果了。
我的项目比较复杂,需要调用动态链接库,这样在JNI传送参数到C程序时,需要对参数进行处理转换。才可以被C程序识别。
大体程序如下:
public class SendSMS {
static
{
System.out.println(System.getProperty("java.library.path"));
System.loadLibrary("sms");
}
public native static int SmsInit();
public native static int SmsSend(byte[] mobileNo, byte[] smContent);
}
在这里要注意的是,path里一定要包含类库的路径,否则在程序运行时会抛出异常:
java.lang.UnsatisfiedLinkError: no sms in java.library.path
at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1491)
at java.lang.Runtime.loadLibrary0(Runtime.java:788)
at java.lang.System.loadLibrary(System.java:834)
at com.mobilesoft.sms.mobilesoftinfo.SendSMS.(SendSMS.java:14)
at com.mobilesoft.sms.mobilesoftinfo.test.main(test.java:18)
Exception in thread "main"
指引的路径应该到.dll文件的上一级,如果指到.dll,则会报:
java.lang.UnsatisfiedLinkError: C:\sms.dll: Can't find dependent libraries
at java.lang.ClassLoader$NativeLibrary.load(Native Method)
at java.lang.ClassLoader.loadLibrary0(ClassLoader.java:1560)
at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1485)
at java.lang.Runtime.loadLibrary0(Runtime.java:788)
at java.lang.System.loadLibrary(System.java:834)
at com.mobilesoft.sms.mobilesoftinfo.SendSMS.(SendSMS.java:14)
at com.mobilesoft.sms.mobilesoftinfo.test.main(test.java:18)
Exception in thread "main"
通过编译,生成com_mobilesoft_sms_mobilesoftinfo_SendSMS.h头文件。(建议使用Jbuilder进行编译,操作比较简单!)这个头文件就是Java 和C之间的纽带。要特别注意的是方法中传递的参数jbyteArray,这在接下来的过程中会重点介绍。
/* DO NOT EDIT THIS FILE - it is machine generated */
#include
/* Header for class com_mobilesoft_sms_mobilesoftinfo_SendSMS */
#ifndef _Included_com_mobilesoft_sms_mobilesoftinfo_SendSMS
#define _Included_com_mobilesoft_sms_mobilesoftinfo_SendSMS
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_mobilesoft_sms_mobilesoftinfo_SendSMS
* Method: SmsInit
* Signature: ()I
*/
JNIEXPORT jint JNICALL Java _com_mobilesoft_sms_mobilesoftinfo_SendSMS_SmsInit
(JNIEnv *, jclass);
/*
* Class: com_mobilesoft_sms_mobilesoftinfo_SendSMS
* Method: SmsSend
* Signature: ([B[B)I
*/
JNIEXPORT jint JNICALL Java _com_mobilesoft_sms_mobilesoftinfo_SendSMS_SmsSend
(JNIEnv *, jclass, jbyteArray, jbyteArray);
#ifdef __cplusplus
}
#endif
#endif
对于我要调用的C程序的动态链接库,C程序也要提供一个头文件,sms.h。这个文件将要调用的方法罗列了出来。
/*
* SMS API
* Author: yippit
* Date: 2004.6.8
*/
#ifndef MCS_SMS_H
#define MCS_SMS_H
#define DLLEXPORT __declspec(dllexport)
/*sms storage*/
#define SMS_SIM 0
#define SMS_MT 1
/*sms states*/
#define SMS_UNREAD 0
#define SMS_READ 1
/*sms type*/
#define SMS_NOPARSE -1
#define SMS_NORMAL 0
#define SMS_FLASH 1
#define SMS_MMSNOTI 2
typedef struct tagSmsEntry {
int index; /*index, start from 1*/
int status; /*read, unread*/
int type; /*-1-can't parser 0-normal, 1-flash, 2-mms*/
int storage; /*SMS_SIM, SMS_MT*/
char date[24];
char number[32];
char text[144];
} SmsEntry;
DLLEXPORT int SmsInit(void);
DLLEXPORT int SmsSend(char *phonenum, char *content);
DLLEXPORT int SmsSetSCA(char *sca);
DLLEXPORT int SmsGetSCA(char *sca);
DLLEXPORT int SmsSetInd(int ind);
DLLEXPORT int SmsGetInd(void);
DLLEXPORT int SmsGetInfo(int storage, int *max, int *used);
DLLEXPORT int SmsSaveFlash (int flag);
DLLEXPORT int SmsRead(SmsEntry *entry, int storage, int index);
DLLEXPORT int SmsDelete(int storage, int index);
DLLEXPORT int SmsModifyStatus(int storage, int index); /*unread -> read*/
#endif
在有了这两个头文件之后,就可以进行C程序的编写了。也就是实现对JNI调用的两个方法。在网上的资料中,由于调用的方法实现的都比较简单,(大多是打印字符串等)所以避开了JNI中最麻烦的部分,也是最关键的部分,参数的传递。由于Java 和C的编码是不同的,所以传递的参数是要进行再处理,否则C程序是会对参数在编译过程中提出警告,例如;warning C4024: 'SmsSend' : different types for formal and actual parameter 2等。
Sms.c的程序如下:
#include "sms.h"
#include "com_mobilesoft_sms_mobilesoftinfo_SendSMS.h"
JNIEXPORT jint JNICALL Java _com_mobilesoft_sms_mobilesoftinfo_SendSMS_SmsInit(JNIEnv * env, jclass jobject)
{
return SmsInit();
}
JNIEXPORT jint JNICALL Java _com_mobilesoft_sms_mobilesoftinfo_SendSMS_SmsSend(JNIEnv * env, jclass jobject, jbyteArray mobileno, jbyteArray smscontent)
{
char * pSmscontent ;
//jsize theArrayLengthJ = (*env)->GetArrayLength(env,mobileno);
jbyte * arrayBody = (*env)->GetByteArrayElements(env,mobileno,0);
char * pMobileNo = (char *)arrayBody;
printf("[%s]\n ", pMobileNo);
//jsize size = (*env)->GetArrayLength(env,smscontent);
arrayBody = (*env)->GetByteArrayElements(env,smscontent,0);
pSmscontent = (char *)arrayBody;
printf("
Chap2:JNI-百度百科
目录
定义
JNI是Java Native Interface 的缩写,中文为JAVA本地调用。从Java 1.1 开始,Java Native Interface (JNI)标准成为java平台的一部分,它允许Java代码和其他语言写的代码进行交互。JNI一开始是为了本地已编译语言,尤其是C和C++而设计 的,但是它并不妨碍你使用其他语言,只要调用约定受支持就可以了。
使用java与本地已编译的代码交互,通常会丧失平台可移植性。但是,有些情况下这样做是可以接受的,甚至是必须的,比如,使用一些旧的库,与硬件、操作系统进行交互,或者为了提高程序的性能。JNI标准至少保证本地代码能工作在任何Java 虚拟机 实现下。
设计目的
·标准的java类库可能不支持你的程序所需的特性。
·或许你已经有了一个用其他语言写成的库或程序,而你希望在java程序中使用它。
·你可能需要用底层语言实现一个小型的时间敏感代码,比如汇编,然后在你的java程序中调用这些功能。
书写步骤
·编写带有native声明的方法的java类
·使用 javac 命令编译所编写的java类
·使用 “ javah -jni java类名” 生成扩展名为h的头文件
·使用C/C++实现本地方法
·将C/C++编写的文件生成动态连接库
·ok
1) 编写java程序:这里以HelloWorld为例。
代码1:
class HelloWorld {
public native void displayHelloWorld();
static {
System.loadLibrary("hello");
}
public static void main(String[] args) {
new HelloWorld().displayHelloWorld();
}
}
声明native方法:如果你想将一个方法做为一个本地方法的话,那么你就必须声明改方法为native的,并且不能实现。其中方法的参数和返回值在后 面讲述。 Load动态库:System.loadLibrary("hello");加载动态库(我们可以这样理解:我们的方法 displayHelloWorld()没有实现,但是我们在下面就直接使用了,所以必须在使用之前对它进行初始化)这里一般是以static块进行加载 的。同时需要注意的是System .loadLibrary();的参数“hello”是动态库的名字。
2) 编译
没有什么好说的了 javac HelloWorld.java
3) 生成扩展名为h的头文件
javah -jni HelloWorld
头文件的内容:
/* DO NOT EDIT THIS FILE - it is machine generated */
1. include
/* Header for class HelloWorld */
1. ifndef _Included_HelloWorld
2. define _Included_HelloWorld
3. ifdef __cplusplus
extern "C" {
1. endif
/*
* Class: HelloWorld
* Method: displayHelloWorld
* Signature: ()V
* /
JNIEXPORT void JNICALL Java_HelloWorld_displayHelloWorld (JNIEnv *, jobject);
1. ifdef __cplusplus
}
1. endif
2. endif
(这里我们可以这样理解:这个h文件相当于我们在java里面的接口,这里声明了一个 Java_HelloWorld_displayHelloWorld (JNIEnv *, jobject);方法,然后在我们的本地方法里面实现这个方法,也就是说我们在编写C/C++ 程序的时候所使用的方法名必须和这里的一致)。
4) 编写本地方法实现和由javah命令生成的头文件里面声明的方法名相同的方法。
代码2:
1 #include "jni.h"
2 #include "HelloWorld.h"
3 //#include other headers
4 JNIEXPORT void JNICALL Java_HelloWorld_displayHelloWorld(JNIEnv *env, jobject obj)
{
printf("Hello world!\n");
return;
}
注意代码2中的第1行,需要将jni.h(该文件可以在%JAVA_HOME%/include文件夹下面找到)文件引入,因为在程序中的 JNIEnv、 jobject等类型都是在该头文件中定义的;另外在第2行需要将HelloWorld.h头文件引入(我是这么理解的:相当于我们在编写java程序的 时候,实现一个接口的话需要声明才可以,这里就是将HelloWorld.h头文件里面声明的方法加以实现。当然不一定是这样)。然后保存为 HelloWorldImpl.c就ok了。
5) 生成动态库
这里以在 Windows中为例,需要生成dll文件。在保存HelloWorldImpl.c文件夹下面,使用VC的编译器cl成。 cl -I%java_home%\include -I%java_home%\include\win32 -LD HelloWorldImp.c -Fehello.dll 注意:生成的dll文件名在选项-Fe后面配置,这里是hello,因为在HelloWorld.java文件中我们loadLibary的时候使用的名 字是hello。当然这里修改之后那里也需要修改。另外需要将-I%java_home%\include -I%java_home%\include\win32参数加上,因为在第四步里面编写本地方法的时候引入了jni.h文件。
6) 运行程序 java HelloWorld就ok.
简要使用例子
下面是一个简单的例子实现打印一句话的功能,但是用的c的printf最终实现。一般提供给java的jni接口包括一个so文件(封装了c函数的实现)和一个java文件(需要调用path的类)。
1. JNI的目的是使java方法中能够调用c实现的一些函数,比如以下的java类,就需要调用一个本地函数testjni(一般声明为private native类型),首先需要创建文件weiqiong.java,内容如下:
class weiqiong {
static { System.loadLibrary("testjni");//载入静态库,test函数在其中实现
}
private native void testjni(); //声明本地调用
public void test()
{
testjni();
}
public static void main(String args[])
{
weiqiong haha = new weiqiong(); haha.test();
}
}
2.然后执行javac weiqiong.java,如果没有报错,会生成一个weiqiong.class。
3.然后设置classpath为你当前的工作目录,如直接输入命令行:set classpath = weiqiong.class所在的完整目录(如 c:\test)再执行javah weiqiong,会生成一个文件weiqiong.h文件,其中有一个函数的声明如下:
JNIEXPORT void JNICALL Java_weiqiong_testjni (JNIEnv *, jobject);
4.创建文件testjni.c将上面那个函数实现,内容如下:
1. include
2. include
JNIEXPORT void JNICALL Java_weiqiong_testjni (JNIEnv *env, jobject obj) { printf("haha---------go into c!!!\n"); }
5.为了生成.so文件,创建makefile文件如下:
libtestjni.so:testjni.o makefile gcc -Wall -rdynamic -shared -o libtestjni.so testjni.o testjni.o:testjni.c weiqiong.h gcc -Wall -c testjni.c -I./ -I/usr/java/j2sdk1.4.0/include -I/usr/java/j2sdk1.4.0/include/linux cl: rm -rf *.o *.so 注意:gcc前面是tab空,j2sdk的目录根据自己装的j2sdk的具体版本来写,生成的so文件的名字必须是loadLibrary的参数名前加 “lib”。
6.export LD_LIBRARY_PATH=.,由此设置library路径为当前目录,这样java文件才能找到so文件。一般的做法是将so文件copy到本机的LD_LIBRARY_PATH目录下。
7.执行java weiqiong,打印出结果:“haha---------go into c!!!”
调用中考虑的问题
在首次使用JNI的时候有些疑问,后来在使用中一一解决,下面就是这些问题的备忘:
1。 java和c是如何互通的?
其实不能互通的原因主要是数据类型的问题,jni解决了这个问题,例如那个c文件中的jstring数据类型就是java传入的String对象,经过jni函数的转化就能成为c的char*。
对应数据类型关系如下表:
Java 类型 本地c类型 说明 boolean jboolean 无符号,8 位 byte jbyte 无符号,8 位 char jchar 无符号,16 位 short jshort 有符号,16 位 int jint 有符号,32 位 long jlong 有符号,64 位 float jfloat 32 位 double jdouble 64 位 void void N/A
JNI 还包含了很多对应于不同 Java 对象的引用类型如下图:
2. 如何将java传入的String参数转换为c的char*,然后使用?
java传入的String参数,在c文件中被jni转换为jstring的数据类型,在c文件中声明char* test,然后test = (char*)(*env)->GetStringUTFChars(env, jstring, NULL);注意:test使用完后,通知虚拟机平台相关代码无需再访问:(*env)->ReleaseStringUTFChars(env, jstring, test);
3. 将c中获取的一个char*的buffer传递给java?
这个char*如果是一般的字符串的话,作为string传回去就可以了。如果是含有’\0’的buffer,最好作为bytearray传出,因为可以制定copy的length,如果copy到string,可能到’\0’就截断了。
有两种方式传递得到的数据:
一种是在jni中直接new一个byte数组,然后调用函数(*env)->SetByteArrayRegion(env, bytearray, 0, len, buffer);将buffer的值copy到bytearray中,函数直接return bytearray就可以了。
一种是return错误号,数据作为参数传出,但是java的基本数据类型是传值,对象是传递的引用,所以将这个需要传出的byte数组用某个类包一下,如下:
class RetObj { public byte[] bytearray; } 这个对象作为函数的参数retobj传出,通过如下函数将retobj中的byte数组赋值便于传出。代码如下:
jclass cls;
jfieldID fid;
jbyteArray bytearray;
bytearray = (*env)->NewByteArray(env,len);
(*env)->SetByteArrayRegion(env, bytearray, 0, len, buffer);
cls = (*env)->GetObjectClass(env, retobj);
fid = (*env)->GetFieldID(env, cls, "retbytes", "[B"]);
(*env)->SetObjectField(env, retobj, fid, bytearray);
4. 不知道占用多少空间的buffer,如何传递出去呢?
在jni的c文件中new出空间,传递出去。java的数据不初始化,指向传递出去的空间即可。
对JAVA传入数据的处理
1. 如果传入的是bytearray的话,作如下处理得到buffer:
char *tmpdata = (char*)(*env)->GetByteArrayElements(env, bytearray, NULL);
(*env)->ReleaseByteArrayElements(env, bytearray, tmpdata, 0);
Chap 3:javah命令帮助信息
D:\Program Files\Java\jdk1.6.0_12\bin>javah
用法:javah [选项] <类>
其中 [选项] 包括:
-help 输出此帮助消息并退出
-classpath <路径> 用于装入类的路径
-bootclasspath <路径> 用于装入引导类的路径
-d <目录> 输出目录
-o <文件> 输出文件(只能使用 -d 或 -o 中的一个)
-jni 生成 JNI样式的头文件(默认)
-version 输出版本信息
-verbose 启用详细输出
Chap 4:用javah产生一个.h文件
2009-07-29 15:21 阅读23 评论0
Java 不是完善的,Java的不足除了体现在运行速度上要比传统的C++慢许多之外,Java无法直接造访到操作体系底层(如系统硬件等),为此 Java使用native法子来扩大Java程序的功效。 可以将native法子比作Java程序同C程序的接口,其实现步骤:
1、在Java中声明native()方式,然后编译;
2、用javah发生一个.h文件;
3、写一个.cpp文件实现native导出方式,其中须要包括第二步发生的.h文件(注意其中又包孕了JDK带的jni.h文件)
4、将第三步的.cpp文件编译成动态链接库文件;
5、在Java中用System.loadLibrary()法子加载第四步发生的动态链接库文件,这个native()办法就可以在Java中被拜访了。
JAVA本地办法实用的情形
1.为了使用底层的主机平台的某个特性,而这个特性不能通过JAVA API拜访
2.为了拜访一个老的体系或者使用一个已有的库,而这个体系或这个库不是用JAVA编写的
3.为了加快程序的性能,而将一段时光敏感的代码作为本地方式实现。
首先写好JAVA文件
/*
* Created on 2005-12-19 Author shaoqi
*/
package com.hode.hodeframework.modelupdate,视频聊天网站 ;
public class CheckFile
{
public native void displayHelloWorld();
static
{
System.loadLibrary("test");
}
public static void main(String[] args) {
new CheckFile().displayHelloWorld();
}
}
然后依据写好的文件编译成CLASS文件
然后在classes或bin之类的class根目录下(其中有已经生成的*.class文件) 执行javah -jni com.hode.hodeframework.modelupdate.CheckFile,就会在class根目录下得到一个 com_hode_hodeframework_modelupdate_CheckFile.h的文件
然后依据头文件的内容编写com_hode_hodeframework_modelupdate_CheckFile.c文件
#include "CheckFile.h"
#include
#include
JNIEXPORT void JNICALL Java_com_hode_hodeframework_modelupdate_CheckFile_displayHelloWorld(
JNIEnv *env, jobject obj)
{
printf("Hello world!
");
return;
}
之后编译生成DLL文件如“test.dll”,名称与System.loadLibrary("test")中的名称一致
vc的编译办法:cl -I%java_home%include -I%java_home%includewin32 -LD com_hode_hodeframework_modelupdate_CheckFile.c -Fetest.dll
最后在运行时加参数-Djava.library.path=[dll寄存的路径]
Chap5:jni教程(very very good)
本文来源:http://blog.csdn.net/sunjavaduke/archive/2007/07/28/1713895.aspx
本教程 摘自IBM DW,如有转载,请声明!
Java 本机接口(Java Native Interface (JNI))是一个本机编程 接口,它是 Java 软件开发工具箱(Java Software Development Kit (SDK))的一部分。
JNI 允许 Java 代码使用以其它语言(譬如 C 和 C++)编写的代码和代码库。Invocation API(JNI 的一部分)可以用来将 Java 虚拟机(JVM)嵌入到本机应用程序中,从而允许程序员从本机代码内部调用 Java 代码。
本教程 涉及 JNI 最常见的两个应用:从 Java 程序调用 C/C++,以及从 C/C++ 程序调用 Java 代码。我们将讨论 Java 本机接口的这两个基本部分以及可能出现的一些更高级的编程 难题。
本教程 将带您去了解使用 Java 本机接口的所有步骤。您将学习如何从 Java 应用程序内部调用本机 C/C++ 代码以及如何从本机 C/C++ 应用程序内部调用 Java 代码。
所有示例都是使用 Java、C 和 C++ 代码编写的,并可以移植到 Windows 和基于 UNIX 的平台上。要完全理解这些示例,您必须有一些 Java 语言编程 经验。此外,您还需要一些 C 或 C++ 编程 经验。严格来说,JNI 解决方案可以分成 Java 编程 任务和 C/C++ 编程任务,由不同的程序员完成每项任务。然而,要完全理解 JNI 是如何在两种编程环境中工作的,您必须能够理解 Java 和 C/C++ 代码。
我们还将讲述一些高级主题,包括本机方法的异常处理和多线程。要充分理解本教程,您应该熟悉 Java 平台的安全性模型,并有一些多线程应用程序开发的经验。
这里将关于高级主题的节从较基本的循序渐进 JNI 简介中划分出来。现在,初级 Java 程序员可以先学习本教程的前两部分,掌握之后再开始学习高级主题。
要运行本教程中的示例,您需要下列工具与组件:
- Java 编译器 :随 SDK 一起提供的 javac.exe。
- Java 虚拟机(JVM) :随 SDK 一起提供的 java.exe。
- 本机方法 C 文件生成器 :随 SDK 一起提供的 javah.exe。
- 定义 JNI 的库文件和本机头文件 。jni.h C 头文件、jvm.lib 和 jvm.dll 或 jvm.so 文件,这些文件都是随 SDK 一起提供的。
- 能够创建共享库的 C 和 C++ 编译器 。最常见的两个 C 编译器是用于 Windows 的 Visual C++ 和用于基于 UNIX 系统的 cc。
虽 然您可以使用自己喜欢的任何开发环境,但我们将在本教程中使用示例是用随 SDK 一起提供的标准工具和组件编写的。请参阅参考资料来下载 SDK、完整的源文件以及对于完成本教程不可缺少的其它工具。本教程具体地解释了 Sun 的 JNI 实现,该实现被认为是 JNI 解决方案的标准。本教程中没有讨论其它 JNI 实现的详细信息。
在 Java 2 SDK 中,JVM 和运行时支持位于名为 jvm.dll(Windows)或 libjvm.so(UNIX)的共享库文件中。在 Java 1.1 JDK 中,JVM 和运行时支持位于名为 javai.dll(Windows)或 libjava.so(UNIX)的共享库文件中。版本 1.1 的共享库包含运行时以及类库的一些本机方法,但在版本 1.2 中已经不包含运行时,并且本机方法被放在 java.dll 和 libjava.so 中。对于以下 Java 代码,这一变化很重要:
- 代码是用非 JNI 本机方法编写的(因为使用了 JDK 1.0 中旧的本机方法接口)
- 通过 JNI Invocation 接口使用了嵌入式 JVM
在两种情况下,在您的本机库能与版本 1.2 一起使用之前,都必须重新链接它们。注:这个变化应该不影响 JNI 程序员实现本机方法 — 只有通过 Invocation API调用 JVM 的 JNI 代码才会受到影响。
如 果使用随 SDK/JDK 一起提供的 jni.h 文件,则头文件将使用 SDK/JDK 安装目录中的缺省 JVM(jvm.dll 或 libjvm.so)。支持 JNI 的 Java 平台的任何实现都会这么做,或允许您指定 JVM 共享库;然而,完成这方面操作的细节可能会因具体 Java 平台/JVM 实现而有所不同。实际上,许多 JVM 实现根本不支持 JNI。
用Java调用C/C++代码
当无法用 Java 语言编写整个应用程序时,JNI 允许您使用本机代码。在下列典型情况下,您可能决定使用本机代码:
- 希望用更低级、更快的编程语言去实现对时间有严格要求的代码。
- 希望从 Java 程序访问旧代码或代码库。
- 需要标准 Java 类库中不支持的依赖于平台的特性。
从 Java 代码调用 C/C++ 的六个步骤
从 Java 程序调用 C 或 C ++ 代码的过程由六个步骤组成。我们将在下面几页中深入讨论每个步骤,但还是先让我们迅速地浏览一下它们。
- 编写 Java 代码 。我们将从编写 Java 类开始,这些类执行三个任务:声明将要调用的本机方法;装入包含本机代码的共享库;然后调用该本机方法。
- 编译 Java 代码 。在使用 Java 类之前,必须成功地将它们编译成字节码。
- 创建 C/C++ 头文件 。C/C++ 头文件将声明想要调用的本机函数说明。然后,这个头文件与 C/C++ 函数实现(请参阅步骤 4)一起来创建共享库(请参阅步骤 5)。
- 编写 C/C++ 代码 。这一步实现 C 或 C++ 源代码文件中的函数。C/C++ 源文件必须包含步骤 3 中创建的头文件。
- 创建共享库文件 。从步骤 4 中创建的 C 源代码文件来创建共享库文件。
- 运行 Java 程序 。运行该代码,并查看它是否有用。我们还将讨论一些用于解决常见错误的技巧。
步骤 1:编写 Java 代码
我们从编写 Java 源代码文件开始,它将声明本机方法(或方法),装入包含本机代码的共享库,然后实际调用本机方法。
这里是名为 Sample1.java 的 Java 源代码文件的示例:
package com.ibm.course.jni;
public class Sample1 {
public native int intMethod(int n);
public native boolean booleanMethod(boolean bool);
public native String stringMethod(String text);
public native int intArrayMethod(int [] intArray);
public static void main(String[] args) {
System.loadLibrary ("Sample1");
Sample1 sample = new Sample1();
int square = sample.intMethod(5);
boolean bool = sample.booleanMethod(true );
String text = sample.stringMethod("JAVA");
int sum = sample.intArrayMethod(new int [] { 1, 1, 2, 3, 5, 8, 13 });
System.out .println("intMethod: " + square);
System.out .println("booleanMethod: " + bool);
System.out .println("stringMethod: " + text);
System.out .println("intArrayMethod: " + sum);
}
}
这段代码做了些什么?
首先,请注意对 native 关键字的使用,它只能随方法一起使用。native 关键字告诉 Java 编译器:方法是用 Java 类之外的本机代码实现的,但其声明却在 Java 中。只能在 Java 类中声明 本机方法,而不能实现它 (但是不能声明为抽象的方法,使用native关键字即可),所以本机方法不能拥有方法主体。
现在,让我们逐行研究一下代码:
- 从第 3 行到第 6 行,我们声明了四个 native 方法。
- 在第 10 行,我们装入了包含这些本机方法的实现的共享库文件。(到步骤 5 时,我们将创建该共享库文件。)
- 最终,从第 12 行到第 15 行,我们调用了本机方法。注:这个操作和调用非本机 Java 方法的操作没有差异。
注 :基于 UNIX 的平台上的共享库文件通常含有前缀“lib”。在本例中,第 10 行可能是 System.loadLibrary("libSample1");。请一定要注意您在步骤 5:创建共享库文件中生成的共享库文件名。
步骤 2:编译 Java 代码
接下来,我们需要将 Java 代码编译成字节码。完成这一步的方法之一是使用随 SDK 一起提供的 Java 编译器 javac。用来将 Java 代码编译成字节码的命令是:
C:\eclipse\workspace\IBMJNI\src\com\ibm\course\jni>javac Sample1.java
步骤 3:创建 C/C++ 头文件
第 三步是创建 C/C++ 头文件,它定义本机函数说明。完成这一步的方法之一是使用 javah.exe,它是随 SDK 一起提供的本机方法 C 存根生成器工具。这个工具被设计成用来创建头文件,该头文件为在 Java 源代码文件中所找到的每个 native 方法定义 C 风格的函数。这里使用的命令是:
C:\eclipse\workspace\IBMJNI\bin>javah –classpath ./ –jni com.ibm.course.jni.Sample1
javah工具帮助
Usage: javah [options] <classes>
where [options] include:
-help Print this help message and exit
-classpath <path> Path from which to load classes
-bootclasspath <path> Path from which to load bootstrap classes
-d <dir> Output directory
-o <file> Output file (only one of -d or -o may be used)
-jni Generate JNI-style header file (default)
-version Print version information
-verbose Enable verbose output
-force Always write output files
<classes> are specified with their fully qualified names (for
instance, java.lang.Object).
在 Sample1.java 上运行 javah.exe 的结果
下面的 Sample1.h 是对我们的 Java 代码运行 javah 工具所生成的 C/C++ 头文件:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_ibm_course_jni_Sample1 */
#ifndef _Included_com_ibm_course_jni_Sample1
#define _Included_com_ibm_course_jni_Sample1
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_ibm_course_jni_Sample1
* Method: intMethod
* Signature: (I)I
*/
JNIEXPORT jint JNICALL Java_com_ibm_course_jni_Sample1_intMethod
(JNIEnv *, jobject, jint);
/*
* Class: com_ibm_course_jni_Sample1
* Method: booleanMethod
* Signature: (Z)Z
*/
JNIEXPORT jboolean JNICALL Java_com_ibm_course_jni_Sample1_booleanMethod
(JNIEnv *, jobject, jboolean);
/*
* Class: com_ibm_course_jni_Sample1
* Method: stringMethod
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_ibm_course_jni_Sample1_stringMethod
(JNIEnv *, jobject, jstring);
/*
* Class: com_ibm_course_jni_Sample1
* Method: intArrayMethod
* Signature: ([I)I
*/
JNIEXPORT jint JNICALL Java_com_ibm_course_jni_Sample1_intArrayMethod
(JNIEnv *, jobject, jintArray);
#ifdef __cplusplus
}
#endif
#endif
关于 C/C++ 头文件
正 如您可能已经注意到的那样,Sample1.h 中的 C/C++ 函数说明和 Sample1.java 中的 Java native 方法声明有很大差异。JNIEXPORT 和 JNICALL 是用于导出函数的、依赖于编译器的指示符。返回类型是映射到 Java 类型的 C/C++ 类型。附录 A:JNI 类型中完整地说明了这些类型。
除了 Java 声明中的一般参数以外,所有这些函数的参数表中都有一个指向 JNIEnv 和 jobject 的指针。指向 JNIEnv 的指针实际上是一个指向函数指针表的指针。正如将要在步骤 4 中看到的,这些函数提供各种用来在 C 和 C++ 中操作 Java 数据的能力。
jobject 参数引用当前对象。因此,如果 C 或 C++ 代码需要引用 Java 函数,则这个 jobject 充当引用或指针,返回调用的 Java 对象。函数名本身是由前缀“Java_”加全限定类名,再加下划线和方法名构成的。
JNI类型
JNI 使用几种映射到 Java 类型的本机定义的 C 类型。这些类型可以分成两类:原始类型和伪类(pseudo-classes)。在 C 中,伪类作为结构实现,而在 C++ 中它们是真正的类。
Java 原始类型直接映射到 C 依赖于平台的类型,如下所示:
C 类型 jarray 表示通用数组。在 C 中,所有的数组类型实际上只是 jobject 的同义类型。但是,在 C++ 中,所有的数组类型都继承了 jarray,jarray 又依次继承了 jobject。下列表显示了 Java 数组类型是如何映射到 JNI C 数组类型的。
这里是一棵对象树,它显示了 JNI 伪类是如何相关的。
步骤 4:编写 C/C++ 代码
当谈到编写 C/C++ 函数实现时,有一点需要牢记:说明必须和 Sample1.h 的函数声明完全一样。我们将研究用于 C 实现和 C++ 实现的完整代码,然后讨论两者之间的差异。
C函数实现
以下是 Sample1.c,它是用 C 编写的实现:
#include "com_ibm_course_jni_Sample1.h"
#include <string.h>
JNIEXPORT jint JNICALL Java_com_ibm_course_jni_Sample1_intMethod
(JNIEnv *env, jobject obj, jint num) {
return num * num;
}
JNIEXPORT jboolean JNICALL Java_com_ibm_course_jni_Sample1_booleanMethod
(JNIEnv *env, jobject obj, jboolean boolean) {
return !boolean;
}
JNIEXPORT jstring JNICALL Java_com_ibm_course_jni_Sample1_stringMethod
(JNIEnv *env, jobject obj, jstring string) {
const char *str = (*env)->GetStringUTFChars(env, string, 0);
char cap[128];
strcpy(cap, str);
(*env)->ReleaseStringUTFChars(env, string, str);
return (*env)->NewStringUTF(env, strupr(cap));
}
JNIEXPORT jint JNICALL Java_com_ibm_course_jni_Sample1_intArrayMethod
(JNIEnv *env, jobject obj, jintArray array) {
int i, sum = 0;
jsize len = (*env)->GetArrayLength(env, array);
jint *body = (*env)->GetIntArrayElements(env, array, 0);
for (i=0; i<len; i++)
{ sum += body[i];
}
(*env)->ReleaseIntArrayElements(env, array, body, 0);
return sum;
}
void main(){}
C++ 函数实现
以下是 Sample1.cpp(C++ 实现)
#include "com_ibm_course_jni_Sample1.h"
#include <string.h>
JNIEXPORT jint JNICALL Java_Sample1_intMethod
(JNIEnv *env, jobject obj, jint num) {
return num * num;
}
JNIEXPORT jboolean JNICALL Java_Sample1_booleanMethod
(JNIEnv *env, jobject obj, jboolean boolean) {
return !boolean;
}
JNIEXPORT jstring JNICALL Java_Sample1_stringMethod
(JNIEnv *env, jobject obj, jstring string) {
const char *str = env->GetStringUTFChars(string, 0);
char cap[128];
strcpy(cap, str);
env->ReleaseStringUTFChars(string, str);
return env->NewStringUTF(strupr(cap));
}
JNIEXPORT jint JNICALL Java_Sample1_intArrayMethod
(JNIEnv *env, jobject obj, jintArray array) {
int i, sum = 0;
jsize len = env->GetArrayLength(array);
jint *body = env->GetIntArrayElements(array, 0);
for (i=0; i<len; i++)
{ sum += body[i];
}
env->ReleaseIntArrayElements(array, body, 0);
return sum;
}
void main(){}
C 和 C++ 函数实现的比较
唯 一的差异在于用来访问 JNI 函数的方法。在 C 中,JNI 函数调用由“(*env)->”作前缀,目的是为了取出函数指针所引用的值。在 C++ 中,JNIEnv 类拥有处理函数指针查找的内联成员函数。下面将说明这个细微的差异,其中,这两行代码访问同一函数,但每种语言都有各自的语法。
C 语法: jsize len = (*env)->GetArrayLength(env,array);
C++ 语法: jsize len =env->GetArrayLength(array);
步骤 5:创建共享库文件
接下来,我们创建包含本机代码的共享库文件。大多数 C 和 C++ 编译器除了可以创建机器代码可执行文件以外,也可以创建共享库文件。用来创建共享库文件的命令取决于您使用的编译器。下面是在 Windows 和 Solaris 系统上执行的命令。
Windows: cl -Ic:\jdk\include -Ic:\jdk\include\win32 -LD Sample1.c -FeSample1.dll
Solaris: cc -G -I/usr/local/jdk/include -I/user/local/jdk/include/solaris Sample1.c -o Sample1.so
步骤 6:运行 Java 程序
最后一步是运行 Java 程序,并确保代码正确工作。因为必须在 Java 虚拟机中执行所有 Java 代码,所以需要使用 Java 运行时环境。完成这一步的方法之一是使用 java,它是随 SDK 一起提供的 Java 解释器。所使用的命令是:
java com.ibm.course.jni.Sample1
当运行 Sample1.class 程序时,应该获得下列结果:
PROMPT>java Sample1
intMethod: 25
booleanMethod: false
stringMethod: JAVA
intArrayMethod: 33
PROMPT>
故障排除
当使用 JNI 从 Java 程序访问本机代码时,您会遇到许多问题。您会遇到的三个最常见的错误是:
- 无法找到动态链接 。它所产生的错误消息是:java.lang.UnsatisfiedLinkError。这通常指无法找到共享库,或者无法找到共享库内特定的本机方法。
- 无法找到共享库文件 。当用 System.loadLibrary(String libname) 方法(参数是文件名)装入库文件时,请确保文件名拼写正确以及没有 指定扩展名。还有,确保库文件的位置在类路径中,从而确保 JVM 可以访问该库文件。
- 无法找到具有指定说明的方法 。确保您的 C/C++ 函数实现拥有与头文件中的函数说明相同的说明。
从 Java 调用 C 或 C++ 本机代码(虽然不简单)是 Java 平台中一种良好集成的功能。虽然 JNI 支持 C 和 C++,但 C++ 接口更清晰一些并且通常比 C 接口更可取。
正 如您已经看到的,调用 C 或 C++ 本机代码需要赋予函数特殊的名称,并创建共享库文件。当利用现有代码库时,更改代码通常是不可取的。要避免这一点,在 C++ 中,通常创建代理代码或代理类,它们有专门的 JNI 所需的命名函数。然后,这些函数可以调用底层库函数,这些库函数的说明和实现保持不变。
Chap6: JNI传递返回值
作为主调方的Java源程序TestJNI.java如下。
代码清单15-4 在Linux平台上调用C函数的例程——TestJNI.java
1. public class TestJNI
2. {
3. static
4. {
5. System.loadLibrary("testjni");//载入静态库,test函数在其中实现
6. }
7.
8. private native void testjni(); //声明本地调用
9.
10. public void test()
11. {
12. testjni();
13. }
14.
15. public static void main(String args[])
16. {
17. TestJNI haha = new TestJNI();
18. haha.test();
19. }
20. }
TestJNI.java声明从libtestjni.so(注意Linux平台的动态链接库文件的扩展名是.so)中调用函数testjni()。
在Linux平台上,遵循JNI 规范的动态链接库文件名必须以“lib”开头。例如在上面的Java程序中指定的库文件名为“testjni”,则实际的库文件应该命名为“libtestjni.so”。
编译TestJNI.java,并为C程序生成头文件:
javac TestJNI.java
javah TestJNI
提供testjni()函数的testjni.c源文件如下。
代码清单15-5 在Linux平台上调用C函数的例程——testjni.c
#include <stdio.h>
#include <TestJNI.h>
JNIEXPORT void JNICALL Java_TestJNI_testjni(JNIEnv *env, jobject obj){
printf("haha---------go into c!!!\n");
}
编写Makefile文件如下,JDK安装的位置请读者自行调整:
libtestjni.so:testjni.o
gcc -rdynamic -shared -o libtestjni.so testjni.o
testjni.o:testjni.c TestJNI.h
gcc -c testjni.c -I./ -I/usr/java/jdk1.6.0_00/include -I/usr/java/jdk1.6.0_00/include/linux
在 Makefile文件中,我们描述了最终的 libtestjin.so依赖于目标文件testjni.o,而testjni.o则依赖于testjni.c源文件和TestJNI.h头文件。请注 意,我们在将testjni.o连接成动态链接库文件时使用了“-rdynamic”选项。
执行make命令编译testjni.c。Linux平台和在Windows平台上类似,有3种方法可以让Java程序找到并装载动态链接库文件。
— 将动态链接库文件放置在当前路径下。
— 将动态链接库文件放置在LD_LIBRARY_PATH环境变量所指向的路径下。注意这一点和Windows平台稍有区别,Windows平台参考PATH环境变量。
— 在启动JVM时指定选项“-Djava.library.path”,将动态链接库文件放置在该选项所指向的路径下。
从下一节开始,我们开始接触到在JNI 框架内Java调用C程序的一些高级话题,包括如何传递参数 、如何传递数组 、如何传递对象等。
各种类型数据的传递是跨平台、跨语言互操作的永恒话题,更复杂的操作其实都可以分解为各种 基本数据类型的操作。只有掌握了基于各种数据类型的互操作,才能称得上掌握了JNI 开发。从下一节开始,环境和步骤不再是阐述的重点,将不再花费专门的篇 幅,例程中的关键点将成为我们关注的焦点。
15.2.2.3 传递字符串
到目前为止,我们还没有实现Java程序向C程序传递参数 ,或者C程序向Java程序传递参数 。本例程将由Java程序向C程序传入一个字符串,C程序对该字符串转成大写形式后回传给Java程序。
Java源程序如下。
代码清单15-6 在Linux平台上调用C函数的例程——Sample1
public class Sample1
{
public native String stringMethod(String text);
public static void main(String[] args)
{
System.loadLibrary("Sample1");
Sample1 sample = new Sample1();
String text = sample.stringMethod("Thinking In Java");
System.out.println("stringMethod: " + text);
}
}
Sample1.java以“Thinking In Java”为参数 调用libSample1.so中的函数stringMethod(),在得到返回的字符串后打印输出。
Sample1.c的源程序如下。
代码清单15-7 在Linux平台上调用C函数的例程——Sample1.c
#include <Sample1.h>
#include <string.h>
JNIEXPORT jstring JNICALL Java_Sample1_stringMethod(JNIEnv *env, jobject obj, jstring string)
{
const char *str = (*env)->GetStringUTFChars(env, string, 0);
char cap[128];
strcpy(cap, str);
(*env)->ReleaseStringUTFChars(env, string, str);
int i=0;
for(i=0;i<strlen(cap);i++)
*(cap+i)=(char)toupper(*(cap+i));
return (*env)->NewStringUTF(env, cap);
}
首先请注意函数头部分,函数接收一个jstring类 型的输入参数 ,并输出一个jstring类型的参数 。jstring是jni .h中定义的数据类型,是JNI 框架内特有的字符串类型,因为jni .h在 Sample1.h中被引入,因此在Sample1.c中无须再次引入。
程序的第4行是从JNI 调用上下文中获取UTF编码的输入字符,将其放在指针str所指向 的一段内存中。第9行是释放这段内存。第13行是将经过大写转换的字符串予以返回,这一句使用了NewStringUTF()函数,将C语言的字符串指针 转换为JNI 的jstring类型。JNIEnv也是在jni .h中定义的,代表JNI 调用的上下文,GetStringUTFChars()、 ReleaseStringUTFChars()和NewStringUTF()均是JNIEnv的函数。
15.2.2.4 传递整型数组
本节例程将首次尝试在JNI 框架内启用数组:C程序向Java程序返回一个定长的整型数组成的数组,Java程序将该数组打印输出。
Java程序的源代码如下。
代码清单15-8 在Linux平台上调用C函数的例程——Sample2
public class Sample2
{
public native int[] intMethod();
public static void main(String[] args)
{
System.loadLibrary("Sample2");
Sample2 sample=new Sample2();
int[] nums=sample.intMethod();
for(int i=0;i<nums.length;i++)
System.out.println(nums[i]);
}
}
Sample2.java调用libSample2.so中的函数intMethod()。Sample2.c的源代码如下。
代码清单15-9 在Linux平台上调用C函数的例程——Sample2.c
#include <Sample2.h>
JNIEXPORT jintArray JNICALL Java_Sample2_intMethod(JNIEnv *env, jobject obj)
{
int i = 1;
jintArray array;//定义数组对象
array = (*env)-> NewIntArray(env, 10);
for(; i<= 10; i++)
(*env)->SetIntArrayRegion(env, array, i-1, 1, &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]);
return array;
}
Sample2.c涉及了两个jni .h定义的整型数相关的数据类型:jint和jintArray,jint是在JNI 框 架内特有的整数类型。程序的第7行开辟出一个长度为10 的jint数组。然后依次向该数组中放入元素1-10。第11行至第16行不是程序的必须部分,纯粹是为了向读者们演示GetArrayLength() 和GetIntArrayElements()这两个函数的使用方法,前者是获取数组长度,后者则是获取数组的首地址以便于遍历数组。
15.2.2.5 传递字符串数组
本节例程是对上节例程的进一步深化:虽然仍然是传递数组 ,但是数组的基类换成了字符串这样一种对象数据类型。Java程序将向C程序传入一个包含中文字符的字符串,C程序并没有处理这个字符串,而是开辟出一个新的字符串数组返回给Java程序,其中还包含两个汉字字符串。
Java程序的源代码如下。
代码清单15-10 在Linux平台上调用C函数的例程——Sample3
public class Sample3
{
public native String[] stringMethod(String text);
public static void main(String[] args)
throws java.io.UnsupportedEncodingException
{
System.loadLibrary("Sample3");
Sample3 sample = new Sample3();
String[] texts = sample.stringMethod("java编程思想");
for(int i=0;i<texts.length;i++)
{
texts[i]=new String(texts[i].getBytes("ISO8859-1"),"GBK");
System.out.print( texts[i] );
}
System.out.println();
}
}
Sample3.java调用libSample3.so中的函数stringMethod()。Sample3.c的源代码如下:
代码清单15-11 在Linux平台上调用C函数的例程——Sample3.c
#include <Sample3.h>
#include <string.h>
#include <stdlib.h>
#define ARRAY_LENGTH 5
JNIEXPORT jobjectArray JNICALL Java_Sample3_stringMethod
(JNIEnv *env, jobject obj, jstring string)
{
jclass objClass = (*env)->FindClass(env, "java/lang/String");
jobjectArray texts= (*env)->NewObjectArray(env,
(jsize)ARRAY_LENGTH, objClass, 0);
jstring jstr;
char* sa[] = { "Hello,", "world!", "JNI ", "很", "好玩" };
int i=0;
for(;i<ARRAY_LENGTH;i++)
{
jstr = (*env)->NewStringUTF( env, sa[i] );
(*env)->SetObjectArrayElement(env, texts, i, jstr);//必须放入jstring
}
return texts;
}
第9、10行是我们需要特别关注的地方:JNI 框架并 没有定义专门的字符串数组,而是使用jobjectArray——对象数组,对象数组的基类是jclass,jclass是JNI 框架内特有的类型,相当 于Java语言中的Class类型。在本例程中,通过FindClass()函数在JNI 上下文中获取到java.lang.String的类型 (Class),并将其赋予jclass变量。
在例程中我们定义了一个长度为5的对象数组texts,并在程序的第18行向其中循环放入预先定义好的sa数组中的字符串,当然前置条件是使用NewStringUTF()函数将C语言的字符串转换为jstring类型。
本例程的另一个关注点是C程序向Java程序传递的中文字符,在Java程序中能否正常显 示的问题。在笔者的试验环境中,Sample3.c是在Linux平台上编辑的,其中的中文字符则是用支持GBK的输入法输入的,而Java程序采用 ISO8859_1字符集存放JNI 调用的返回字符,因此在“代码清单15-10在Linux平台上调用C函数的例程——Sample3”的第14行中将其转码后输出。
15.2.2.6 传递对象数组
本节例程演示的是C程序向Java程序传递对象数组,而且对象数组中存放的不再是字符串,而是一个在Java中自定义的、含有一个topic属性的MailInfo对象类型。
MailInfo对象定义如下。
代码清单15-12 在Linux平台上调用C函数的例程——MailInfo
public class MailInfo {
public String topic;
public String getTopic()
{
return this.topic;
}
public void setTopic(String topic)
{
this.topic=topic;
}
}
Java程序的源代码如下。
代码清单15-13 在Linux平台上调用C函数的例程——Sample4
public class Sample4
{
public native MailInfo[] objectMethod(String text);
public static void main(String[] args)
{
System.loadLibrary("Sample4");
Sample4 sample = new Sample4();
MailInfo[] mails = sample.objectMethod("Thinking In Java");
for(int i=0;i<mails.length;i++)
System.out.println(mails[i].topic);
}
}
Sample4.java调用libSample4.so中的objectMethod()函数。Sample4.c的源代码如下。
代码清单15-14 在Linux平台上调用C函数的例程——Sample4.c
#include <Sample4.h>
#include <string.h>
#include <stdlib.h>
#define ARRAY_LENGTH 5
JNIEXPORT jobjectArray JNICALL Java_Sample4_objectMethod(
JNIEnv *env, jobject obj, jstring string)
{
jclass objClass = (*env)->FindClass(env, "java/lang/Object");
jobjectArray mails= (*env)->NewObjectArray(env,
(jsize)ARRAY_LENGTH, objClass, 0);
jclass objectClass = (*env)->FindClass(env, "MailInfo");
jfieldID topicFieldId = (*env)->GetFieldID(env, objectClass,
"topic", "Ljava/lang/String;");
int i=0;
for(;i<ARRAY_LENGTH;i++)
{
(*env)->SetObjectField(env, obj, topicFieldId, string);
(*env)->SetObjectArrayElement(env, mails, i, obj);
}
return mails;
}
程序的第9、10行读者们应该不会陌生,在上一节的例 程中已经出现过,不同之处在于这次通过FindClass()函数在JNI 上下文中获取的是java.lang.Object的类型(Class),并将 其作为基类开辟出一个长度为5的对象数组,准备用来存放MailInfo对象。
程序的第12、13行的目的则是创建一个jfieldID类型的变量,在JNI 中,操作对 象属性都是通过jfieldID进行的。第12行首先查找得到MailInfo的类型(Class),然后基于这个jclass进一步获取其名为 topic的属性,并将其赋予jfieldID变量。
程 序的第18、19行的目的是循环向对象数组中放入jobject对象。 SetObjectField()函数属于首次使用,该函数的作用是向jobject的属性赋值,而值的内容正是Java程序传入的jstring变量 值。请注意在向对象属性赋值和向对象数组中放入对象的过程中,我们使用了在函数头部分定义的jobject类型的环境参数 obj作为中介。至此,JNI 框 架固有的两个环境入参env和obj,我们都有涉及。
Chap7:Jni中C++和Java的参数传递
如何使用JNI的一些基本方法和过程在网上多如牛毛,如果你对Jni不甚了解,不知道Jni是做什么的,如何建立一个基本的jni程序,或许可以参考下面下面这些文章:
<利用VC++6.0实现JNI的最简单的例子>
<JNI入门教程之HelloWorld篇>
<SUN JNI Tutorial>
这 些资料的例子中,大多数只是输入一些简单的参数,获取没有参数。而在实际的使用过程中,往往需要对参数进行处理转换。才可以被C/C++程序识别。比如我 们在C++中有一个结构(Struct)DiskInfo ,需要传递一个类似于DiskInfo *pDiskInfo的参数,类似于在C++这样参数如何传递到Java中呢?下面我们就来讨论C++到Java中方法的一些常见参数的转换:
1.定义Native Java类:
如果你习惯了使用JNI,你就不会觉得它难了。既然本地方法是由其他语言实现的,它们在Java中没有函数体。但是,所有本地代码必须用本地关键词native声明,成为Java类的成员。假设我们在C++中有这么一个结构,它用来描述硬盘信息:
//硬盘信息
struct {
char name[256];
int serial;
}DiskInfo;
那么我们需要在Java中定义一个类来与之匹配,声明可以写成这样:
class DiskInfo {
//名字
public String name;
//序列号
public int serial;
}
在这个类中,申明一些Native的本地方法,来测试方法参数的传递,分别定义了一些函数,用来传递结构或者结构数组,具体定义如下面代码:
/**//****************** 定义本地方法 ********************/
//输入常用的数值类型(Boolean,Byte,Char,Short,Int,Float,Double)
public native void displayParms(String showText, int i, boolean bl);
//调用一个静态方法
public native int add(int a, int b);
//输入一个数组
public native void setArray(boolean[] blList);
//返回一个字符串数组
public native String[] getStringArray();
//返回一个结构
public native DiskInfo getStruct();
//返回一个结构数组
public native DiskInfo[] getStructArray();
2.编译生成C/C++头文件
定义好了Java类之后,接下来就要写本地代码。本地方法符号提供一个满足约定的头文件,使用Java工具Javah可以很容易地创建它而不用手动去创建。你对Java的class文件使用javah命令,就会为你生成一个对应的C/C++头文件。
1)、在控制台下进入工作路径,本工程路径为:E:\work\java\workspace\JavaJni。
2)、运行javah 命令:javah -classpath E:\work\java\workspace\JavaJni com.sundy.jnidemo ChangeMethodFromJni
本文生成的C/C++头文件名为: com_sundy_jnidemo_ChangeMethodFromJni.h
3.在C/C++中实现本地方法
生 成C/C++头文件之后,你就需要写头文件对应的本地方法。注意:所有的本地方法的第一个参数都是指向JNIEnv结构的。这个结构是用来调用JNI函数 的。第二个参数jclass的意义,要看方法是不是静态的(static)或者实例(Instance)的。前者,jclass代表一个类对象的引用,而 后者是被调用的方法所属对象的引用。
返回值和参数类型根据等价约定映射到本地C/C++类型,如表JNI类型映射所示。有些类型,在本地代码中可直接使用,而其他类型只有通过JNI调用操作。
表A ※ JNI 类型映射
Java类型 本地类型 描述
boolean jboolean C/C++8位整型
byte jbyte C/C++带符号的8位整型
char jchar C/C++无符号的16位整型
short jshort C/C++带符号的16位整型
int jint C/C++带符号的32位整型
long jlong C/C++带符号的64位整型e
float jfloat C/C++32位浮点型
double jdouble C/C++64位浮点型
Object jobject 任何Java对象,或者没有对应java类型的对象
Class jclass Class对象
String jstring 字符串对象
Object[] jobjectArray 任何对象的数组
boolean[] jbooleanArray 布尔型数组
byte[] jbyteArray 比特型数组
char[] jcharArray 字符型数组
short[] jshortArray 短整型数组
int[] jintArray 整型数组
long[] jlongArray 长整型数组
float[] jfloatArray 浮点型数组
double[] jdoubleArray 双浮点型数组
3.1 使用数组:
JNI通过JNIEnv提供的操作Java数组的功能。它提供了两个函数:一个是操作java的简单型数组的,另一个是操作对象类型数组的。
因为速度的原因,简单类型的数组作为指向本地类型的指针暴露给本地代码。因此,它们能作为常规的数组存取。这个指针是指向实际的Java数组或者Java数组的拷贝的指针。另外,数组的布置保证匹配本地类型。
为了存取Java简单类型的数组,你就要要使用GetXXXArrayElements函数(见表B),XXX代表了数组的类型。这个函数把Java数组看成参数,返回一个指向对应的本地类型的数组的指针。
表B
函数 Java数组类型 本地类型
GetBooleanArrayElements jbooleanArray jboolean
GetByteArrayElements jbyteArray jbyte
GetCharArrayElements jcharArray jchar
GetShortArrayElements jshortArray jshort
GetIntArrayElements jintArray jint
GetLongArrayElements jlongArray jlong
GetFloatArrayElements jfloatArray jfloat
GetDoubleArrayElements jdoubleArray jdouble
JNI数组存取函数
当 你对数组的存取完成后,要确保调用相应的ReleaseXXXArrayElements函数,参数是对应Java数组和 GetXXXArrayElements返回的指针。如果必要的话,这个释放函数会复制你做的任何变化(这样它们就反射到java数组),然后释放所有相 关的资源。
为了使用java对象的数组,你必须使用GetObjectArrayElement函数和SetObjectArrayElement函数,分别去get,set数组的元素。GetArrayLength函数会返回数组的长度。
3.2 使用对象
JNI 提供的另外一个功能是在本地代码中使用Java对象。通过使用合适的JNI函数,你可以创建Java对象,get、set 静态(static)和实例(instance)的域,调用静态(static)和实例(instance)函数。JNI通过ID识别域和方法,一个域或 方法的ID是任何处理域和方法的函数的必须参数。
表C列出了用以得到静态(static)和实例(instance)的域与方法的JNI函数。每个函数接受(作为参数)域或方法的类,它们的名称,符号和它们对应返回的jfieldID或jmethodID。
表C
函数 描述
GetFieldID 得到一个实例的域的ID
GetStaticFieldID 得到一个静态的域的ID
GetMethodID 得到一个实例的方法的ID
GetStaticMethodID 得到一个静态方法的ID
※域和方法的函数
如果你有了一个类的实例,它就可以通过方法GetObjectClass得到,或者如果你没有这个类的实例,可以通过FindClass得到。符号是从域的类型或者方法的参数,返回值得到字符串,如表D所示。
表D
Java类型 符号
boolean Z
byte B
char C
short S
int I
long L
float F
double D
void V
objects对象 Lfully-qualified-class-name;L类名
Arrays数组 [array-type [数组类型
methods方法 (argument-types)return-type(参数类型)返回类型
※确定域和方法的符号
下面我们来看看,如果通过使用数组和对象,从C++中的获取到Java中的DiskInfo 类对象,并返回一个DiskInfo数组:
//返回一个结构数组,返回一个硬盘信息的结构数组
JNIEXPORT jobjectArray JNICALL
Java_com_sundy_jnidemo_ChangeMethodFromJni_getStructArray
(JNIEnv *env, jobject _obj)
{
//申明一个object数组
jobjectArray args = 0;
//数组大小
jsize len = 5;
//获取object所属类,一般为java/lang/Object就可以了
jclass objClass = (env)->FindClass("java/lang/Object");
//新建object数组
args = (env)->NewObjectArray(len, objClass, 0);
/**//* 下面为获取到Java中对应的实例类中的变量*/
//获取Java中的实例类
jclass objectClass = (env)->FindClass("com/sundy/jnidemo/DiskInfo");
//获取类中每一个变量的定义
//名字
jfieldID str = (env)->GetFieldID(objectClass,"name","Ljava/lang/String;");
//序列号
jfieldID ival = (env)->GetFieldID(objectClass,"serial","I");
//给每一个实例的变量付值,并且将实例作为一个object,添加到objcet数组中
for(int i=0; i < len; i++ )
{
//给每一个实例的变量付值
jstring jstr = WindowsTojstring(env,"我的磁盘名字是 D:");
//(env)->SetObjectField(_obj,str,(env)->NewStringUTF("my name is D:"));
(env)->SetObjectField(_obj,str,jstr);
(env)->SetShortField(_obj,ival,10);
//添加到objcet数组中
(env)->SetObjectArrayElement(args, i, _obj);
}
//返回object数组
return args;
}
全部的C/C++方法实现代码如下:
/**//*
*
* 一缕阳光(sundy)版权所有,保留所有权利。
*/
/**//**
*
* TODO Jni 中一个从Java到C/C++参数传递测试类
*
* @author 刘正伟(sundy)
* @see http://www.cnweblog.com/sundy
* @see mailto:sundy26@126.com
* @version 1.0
* @since 2005-4-30
*
* 修改记录:
*
* 日期 修改人 描述
* ----------------------------------------------------------------------------------------------
*
*
*
*/
// JniManage.cpp : 定义 DLL 应用程序的入口点。
//
package com.sundy.jnidemo;
#include "stdafx.h"
#include <stdio.h>
#include <math.h>
#include "jni.h"
#include "jni_md.h"
#include "./head/Base.h"
#include "head/wmi.h"
#include "head/com_sundy_jnidemo_ChangeMethodFromJni.h" //通过javah –jni javactransfer 生成
#include <stdio.h>
#include "stdlib.h"
#include "string.h"
#pragma comment (lib,"BaseInfo.lib")
#pragma comment (lib,"jvm.lib")
//硬盘信息
struct {
char name[256];
int serial;
}DiskInfo;
/**//*BOOL APIENTRY DllMain( HANDLE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
LPTSTR strName = new CHAR[256] ;
(*GetHostName)(strName);
printf("%s\n",strName);
delete [] strName;
return TRUE;
}*/
//将jstring类型转换成windows类型
char* jstringToWindows( JNIEnv *env, jstring jstr );
//将windows类型转换成jstring类型
jstring WindowsTojstring( JNIEnv* env, char* str );
//主函数
BOOL WINAPI DllMain(HANDLE hHandle, DWORD dwReason, LPVOID lpReserved)
{
return TRUE;
}
//输入常用的数值类型 Boolean,Byte,Char,Short,Int,Float,Double
JNIEXPORT void JNICALL Java_com_sundy_jnidemo_ChangeMethodFromJni_displayParms
(JNIEnv *env, jobject obj, jstring s, jint i, jboolean b)
{
const char* szStr = (env)->GetStringUTFChars(s, 0 );
printf( "String = [%s]\n", szStr );
printf( "int = %d\n", i );
printf( "boolean = %s\n", (b==JNI_TRUE ? "true" : "false") );
(env)->ReleaseStringUTFChars(s, szStr );
}
//调用一个静态方法,只有一个简单类型输出
JNIEXPORT jint JNICALL Java_com_sundy_jnidemo_ChangeMethodFromJni_add
(JNIEnv *env, jobject, jint a, jint b)
{
int rtn = (int)(a + b);
return (jint)rtn;
}
/**/输入一个数组,这里输入的是一个Boolean类型的数组
JNIEXPORT void JNICALL Java_com_sundy_jnidemo_ChangeMethodFromJni_setArray
(JNIEnv *env, jobject, jbooleanArray ba)
{
jboolean* pba = (env)->GetBooleanArrayElements(ba, 0 );
jsize len = (env)->GetArrayLength(ba);
int i=0;
// change even array elements
for( i=0; i < len; i+=2 )
{
pba[i] = JNI_FALSE;
printf( "boolean = %s\n", (pba[i]==JNI_TRUE ? "true" : "false") );
}
(env)->ReleaseBooleanArrayElements(ba, pba, 0 );
}
/**/返回一个字符串数组
JNIEXPORT jobjectArray JNICALL Java_com_sundy_jnidemo_ChangeMethodFromJni_getStringArray
(JNIEnv *env, jobject)
{
jstring str;
jobjectArray args = 0;
jsize len = 5;
char* sa[] = { "Hello,", "world!", "JNI", "is", "fun" };
int i=0;
args = (env)->NewObjectArray(len,(env)->FindClass("java/lang/String"),0);
for( i=0; i < len; i++ )
{
str = (env)->NewStringUTF(sa[i] );
(env)->SetObjectArrayElement(args, i, str);
}
return args;
}
//返回一个结构,这里返回一个硬盘信息的简单结构类型
JNIEXPORT jobject JNICALL Java_com_sundy_jnidemo_ChangeMethodFromJni_getStruct
(JNIEnv *env, jobject obj)
{
/**//* 下面为获取到Java中对应的实例类中的变量*/
//获取Java中的实例类
jclass objectClass = (env)->FindClass("com/sundy/jnidemo/DiskInfo");
//获取类中每一个变量的定义
//名字
jfieldID str = (env)->GetFieldID(objectClass,"name","Ljava/lang/String;");
//序列号
jfieldID ival = (env)->GetFieldID(objectClass,"serial","I");
//给每一个实例的变量付值
(env)->SetObjectField(obj,str,(env)->NewStringUTF("my name is D:"));
(env)->SetShortField(obj,ival,10);
return obj;
}
//返回一个结构数组,返回一个硬盘信息的结构数组
JNIEXPORT jobjectArray JNICALL Java_com_sundy_jnidemo_ChangeMethodFromJni_getStructArray
(JNIEnv *env, jobject _obj)
{
//申明一个object数组
jobjectArray args = 0;
//数组大小
jsize len = 5;
//获取object所属类,一般为ava/lang/Object就可以了
jclass objClass = (env)->FindClass("java/lang/Object");
//新建object数组
args = (env)->NewObjectArray(len, objClass, 0);
/**//* 下面为获取到Java中对应的实例类中的变量*/
//获取Java中的实例类
jclass objectClass = (env)->FindClass("com/sundy/jnidemo/DiskInfo");
//获取类中每一个变量的定义
//名字
jfieldID str = (env)->GetFieldID(objectClass,"name","Ljava/lang/String;");
//序列号
jfieldID ival = (env)->GetFieldID(objectClass,"serial","I");
//给每一个实例的变量付值,并且将实例作为一个object,添加到objcet数组中
for(int i=0; i < len; i++ )
{
//给每一个实例的变量付值
jstring jstr = WindowsTojstring(env,"我的磁盘名字是 D:");
//(env)->SetObjectField(_obj,str,(env)->NewStringUTF("my name is D:"));
(env)->SetObjectField(_obj,str,jstr);
(env)->SetShortField(_obj,ival,10);
//添加到objcet数组中
(env)->SetObjectArrayElement(args, i, _obj);
}
//返回object数组
return args;
}
//将jstring类型转换成windows类型
char* jstringToWindows( JNIEnv *env, jstring jstr )
{
int length = (env)->GetStringLength(jstr );
const jchar* jcstr = (env)->GetStringChars(jstr, 0 );
char* rtn = (char*)malloc( length*2+1 );
int size = 0;
size = WideCharToMultiByte( CP_ACP, 0, (LPCWSTR)jcstr, length, rtn,(length*2+1), NULL, NULL );
if( size <= 0 )
return NULL;
(env)->ReleaseStringChars(jstr, jcstr );
rtn[size] = 0;
return rtn;
}
//将windows类型转换成jstring类型
jstring WindowsTojstring( JNIEnv* env, char* str )
{
jstring rtn = 0;
int slen = strlen(str);
unsigned short * buffer = 0;
if( slen == 0 )
rtn = (env)->NewStringUTF(str );
else
{
int length = MultiByteToWideChar( CP_ACP, 0, (LPCSTR)str, slen, NULL, 0 );
buffer = (unsigned short *)malloc( length*2 + 1 );
if( MultiByteToWideChar( CP_ACP, 0, (LPCSTR)str, slen, (LPWSTR)buffer, length ) >0 )
rtn = (env)->NewString( (jchar*)buffer, length );
}
if( buffer )
free( buffer );
return rtn;
}
Java 测试native代码
这没有什么多说的,看代码吧
//主测试程序
public static void main(String[] args) {
ChangeMethodFromJni changeJni = new ChangeMethodFromJni();
//输入常用的数值类型(string int boolean)
System.out
.println("------------------输入常用的数值类型(string int boolean)-----------");
changeJni.displayParms("Hello World!", 100, true);
//调用一个静态方法
System.out.println("------------------调用一个静态方法-----------");
int ret = changeJni.add(12, 20);
System.out.println("The result is: " + String.valueOf(ret));
//输入一个数组
System.out.println("------------------输入一个数组-----------");
boolean[] blList = new boolean[] { true, false, true };
changeJni.setArray(blList);
//返回一个字符串数组
System.out.println("------------------返回一个字符串数组-----------");
String[] strList = changeJni.getStringArray();
for (int i = 0; i < strList.length; i++) {
System.out.print(strList[i]);
}
System.out.println();
System.out.println("------------------返回一个结构-----------");
//返回一个结构
DiskInfo disk = changeJni.getStruct();
System.out.println("name:" + disk.name);
System.out.println("Serial:" + disk.serial);
//返回一个结构数组
System.out.println("------------------返回一个结构数组 -----------");
DiskInfo[] diskList = changeJni.getStructArray();
for (int i = 0; i < diskList.length; i++) {
System.out.println("name:" + diskList[i].name);
System.out.println("Serial:" + diskList[i].serial);
}
}
注:本程序在VS2003,eclipse (jse5.0) winxp sp2编译通过
posted on 2005-05-02 20:22 sundy 阅读(4406) 评论(21) 编辑 收藏 所属分类: Java
评论
# re: Jni中C++和Java的参数传递 2005-05-22 14:35 张磊
请问如果想返回byte[]类型该怎么做 回复 更多评论
# re: Jni中C++和Java的参数传递 2005-05-23 08:37 sundy
因为:
byte[] jbyteArray 比特型数组
所以你将byte[] 作为一个jbyteArray数组传递就可以了
回复 更多评论
# re: Jni中C++和Java的参数传递 2005-09-21 14:46 小影
请问如果我想把在C++里面计算好的一个二维数组传回给java程序接受,该怎么写代码呢?我找了很多这方面的书和资料,都没有关于传递二维数组的介绍,请您给予指导,多谢啦^_^ 回复 更多评论
# re: Jni中C++和Java的参数传递 2005-09-21 17:47 sundy
我没有直接传递过二维数组
但我想你可以把试一试二维数组转换成为一个Hashmap的数组传出来。
请参考"如何在Jni中传递出Hashmap的数组?"的一些代码
回复 更多评论
# re: Jni中C++和Java的参数传递 2005-12-26 16:32 wangjian
返回一个结构数组时,为什么每个对象的数据都是一样的?即5个Diskinfo的成员值都相同,能不能不相同? 回复 更多评论
# re: Jni中C++和Java的参数传递 2005-12-26 16:55 wangjian
我把5个DiskInfo对象的成员serial分别设置为1、2、3、4、5,可是传递到java后5个对象的serial成员值都是5,为什么这样阿?盼回复,多谢! 回复 更多评论
# re: Jni中C++和Java的参数传递 2005-12-27 21:51 sundy
//给每一个实例的变量付值,并且将实例作为一个object,添加到objcet数组中
for(int i=0; i < len; i++ )
{
......
//添加到objcet数组中
(env)->SetObjectArrayElement(args, i, _obj);
}
你看看设置的_Obj是不是都是同一个??
回复 更多评论
# re: Jni中C++和Java的参数传递 2005-12-28 13:32 wangjian
如下所示,我就是把你程序中(env)->SetShortField(_obj,ival,10)的参数10换成i,结果每个对象都是对象的serial成员值都是4,请问怎样实现多个不同对象的传递?
for(int i=0; i < len; i++ )
{
jstring jstr = WindowsTojstring(env,"我的磁盘名字是D:");
(env)->SetObjectField(_obj,str,jstr);
(env)->SetShortField(_obj,ival,i);
(env)->SetObjectArrayElement(args, i, _obj);
}
回复 更多评论
# re: Jni中C++和Java的参数传递 2005-12-28 15:15 sundy
应该没有问题的呀,
SetObjectArrayElement的时候,_obj是不同的吗?
要不你将for循环改为:
jstring jstr = WindowsTojstring(env,"我的磁盘名字是C:");
(env)->SetObjectField(_obj,str,jstr);
(env)->SetShortField(_obj,ival,0);
(env)->SetObjectArrayElement(args, 0, _obj);
jstring jstr = WindowsTojstring(env,"我的磁盘名字是D:");
(env)->SetObjectField(_obj,str,jstr);
(env)->SetShortField(_obj,ival,1);
(env)->SetObjectArrayElement(args, 1, _obj);
看看对吗? 回复 更多评论
# re: Jni中C++和Java的参数传递 2005-12-29 20:42 wangjian
这样作不对,不过我找到正确的方法了,要用构造函数生成新的对象。 回复 更多评论
# re: Jni中C++和Java的参数传递 2006-01-17 11:07 luli
SQLRETURN SQLAllocHandle( SQLSMALLINT HandleType,
SQLHANDLE InputHandle,
SQLHANDLE * OutputHandlePtr
)
这是odbc api里的一个函数 SQLHANDLE 是一个结构
c#里的引用方式如下
[DllImport("ODBC32.dll")]
private static extern short SQLAllocHandle(short hType, IntPtr inputHandle, out IntPtr outputHandle);
但我不清楚 SQLHANDLE 结构具体怎么构造的 因此我无法用java类来模拟
我是菜鸟 望解答 谢过了 回复 更多评论
# re: Jni中C++和Java的参数传递 2006-01-17 14:25 luli
忘了补充 SQLHANDLE InputHandle与SQLHANDLE * OutputHandlePtr
一个是结构 一个是结构指针 那我是否该如下模拟
class SQLHANDLE
{
}
public class test
{
SQLHANDLE a=new SQLHANDLE ();
public static void main(String args[]) {
int i=SQLAllocHandle( SQLSMALLINT HandleType, new SQLHANDLE(),a)
}
}
回复 更多评论
# re: Jni中C++和Java的参数传递 2006-03-21 17:31 Hefe
WideCharToMultiByte();
MultiByteToWideChar();
请问这两个函数实现什么功能,请作者给出代码,多谢!
回复 更多评论
# re: Jni中C++和Java的参数传递 2006-03-22 08:47 sundy
@Hefe look here: http://www.google.com/search?hl=zh-CN&lr=lang_zh-CN&q=WideCharToMultiByte
回复 更多评论
# re: Jni中C++和Java的参数传递 2006-03-28 17:40 dijk
要在c函数中调用java类的类成员的方法,比如调用JEditorPane类型成员的setText方法,该怎么办? 回复 更多评论
# re: Jni中C++和Java的参数传递 2006-04-16 21:33 陈世雄
java中函数的处理中,对于对象类型(非基本类型int,long...)的输入参数,函数中是可以修改输入参数的内容,函数执行完毕,修改仍然是有效的。
jni中 是否也是这样呢?
回复 更多评论
# re: Jni中C++和Java的参数传递 2006-04-18 17:50 王文波
你好:
向你请教一个问题:我想用jini来调用dll。我在jbuilder中新建的简单的project调用jini运行正常。但是,我现在要对一个工程软件进行二次开发,该软件的
开发也使用jbuilder生成一个project,然后放在指定的路径下就可以了,该软件在运行的时候会自动读取该project。我在这个软件二次开发的project中使用
jini,则总是报错:unsatisfiedlinkError get()。其中get()方法名。请问该怎么解决这个问题?
我的邮箱:zwj23232@tom.com 回复 更多评论
# re: Jni中C++和Java的参数传递 2006-05-29 21:25 single
# re: Jni中C++和Java的参数传递 2005-12-29 20:42 wangjian
这样作不对,不过我找到正确的方法了,要用构造函数生成新的对象。 回复
---------------------------------------------------
能说说方法吗? 回复 更多评论
# re: Jni中C++和Java的参数传递 2006-08-29 11:34 yangyongfa
我 正在做JNI,是在C++中调用JAVA类的方法,请问,我在JAVA类的方法中参数使 用的是byte[],而我在C++中是把一个文件读成unsigned char*,请问怎么可以正确调用JAVA中的方法? 类中方法原型:public boolean AddHoyuBox2DB(String BoxName, byte[] BoxFile,byte[] WDHPic,int BoxFileBinLen, int WDHPicBinLen, String ParameterText, byte[] XXPic,int PicBinLen, byte[] SeriousPics,int SeriousPicsBinLen,String FileLenStr) ?
回复 更多评论
# re: Jni中C++和Java的参数传递 2007-10-25 15:27 vampire
c的结构提里写有一个**p,指针的指针,在java中该如何封装??? 回复 更多评论
# re: Jni中C++和Java的参数传递 2007-12-11 13:13 Focus
@single
for(int i=0; i < len; i++ )
{
jobject objTemp = (env)->AllocObject(objectClass); //释放问题??这个是否需要释放不是很懂
//objectClass是函数上面给的 那个
jstring jstr = WindowsTojstring(env,"我的磁盘名字是D:");
(env)->SetObjectField(objTemp,str,jstr);
(env)->SetShortField(objTemp,ival,i);
(env)->SetObjectArrayElement(args, i, objTemp);
}
这个 可以实现 数组 元素相同的问题
Chap8:如何将java传递过来的jbyteArray转换成C/C++中的BYTE数组
近遇到一个问题,请各位帮忙解决下:
如何将java传递过来的jbyteArray转换成C/C++中的BYTE数组?BYTE为unsigned char类型
这两个我理解应该是相同的吧,强制类型转换好像不启作用,应该如何转换呢?
该问题已经关闭: 问 题已解决,之前代码有问题 jbyte * arrayBody = env->GetByteArrayElements(data,0); jsize theArrayLengthJ = env->GetArrayLength(data); BYTE * starter = (BYTE *)arrayBody;
Chap5:使用JNI技术实现java程序调用第三方dll(c/c++)文件的功能
JAVA 的跨平台的特性深受java 程 序员们的喜爱,但正是由于它为了实现跨平台的目的,使得它和本地机器的各种内部联系变得很少,大大约束了它的功能,比如与一些硬件设备通信,往往要花费很 大的精力去设计流程编写代码去管理设备端口,而且有一些设备厂商提供的硬件接口已经经过一定的封装和处理,不能直接使用java 程序通过端口和设备通信,这种情况下就得考虑使用java 程序去调用比较擅长同系统打交道的第三方程序,从1.1版本开始的JDK提供了解决这个问题的技术标准:JNI 技术.
JNI 是Java Native Interface(Java 本地接口)的缩写,本地是相对于java 程序来说的,指直接运行在操作系统之上,与操作系统直接交互的程序.从1.1版本的JDK开始,JNI 就作为标准平台的一部分发行.在JNI 出现的初期是为了Java 程序与本地已编译语言,尤其是C和C++的互操作而设计的,后来经过扩展也可以与c和c++之外的语言编写的程序交互,例如Delphi程序.
使用JNI 技术固然增强了java 程序的性能和功能,但是它也破坏了java 的跨平台的优点,影响程序的可移植性和安全性,例如由于其他语言(如C/C++)可能能够随意地分配对象/占用内存,Java 的指针安全性得不到保证.但在有些情况下,使用JNI 是可以接受的,甚至是必须的,例如上面提到的使用java 程序调用硬件厂商提供的类库同设备通信等,目前市场上的许多读卡器设备就是这种情况.在这必须使用JNI 的情况下,尽量把所有本地方法都封装在单个类中,这个类调用单个的本地库文件,并保证对于每种目标操作系统,都可以用特定于适当平台的版本替换这个文件,这样使用JNI 得到的要比失去的多很多.
现在开始讨论上面提到的问题,一般设备商会提供两种类型的类库文件,windows系统的会包含.dll/.h/.lib文件,而linux系统的会包含.so/.a文件,这里只讨论windows系统下的c/c++编译的dll文件调用方法.
我把设备商提供的dll文件称之为第三方dll文件,之所以说第三方,是因为JNI 直 接调用的是按它的标准使用c/c++语言编译的dll文件,这个文件是客户程序员按照设备商提供的.h文件中的列出的方法编写的dll文件,我称之为第二 方dll文件,真正调用设备商提供的dll文件的其实就是这个第二方dll文件.到这里,解决问题的思路已经产生了,大慨分可以分为三步:
1>编写一个java 类,这个类包含的方法是按照设备商提供的.h文件经过变形/转换处理过的,并且必须使用native定义.这个地方需要注意的问题是java 程序中定义的方法不必追求和厂商提供的头文件列出的方法清单中的方法具有相同的名字/返回值/参数,因为一些参数类型如指针等在java 中没法模拟,只要能保证这个方法能实现原dll文件中的方法提供的功能就行了;
2>按JNI 的规则使用c/c++语言编写一个dll程序;
3>按dll调用dll的规则在自己编写的dll程序里面调用厂商提供的dll程序中定义的方法.
我之前为了给一个java 项目添加IC卡读写功能,曾经查了很多资料发现查到的资料都是只说到第二步,所以剩下的就只好自己动手研究了.下面结合具体的代码来按这三个步骤分析.
1>假设厂商提供的.h文件中定义了一个我们需要的方法:
__int16 __stdcall readData( HANDLE icdev, __int16 offset, __int16 len, unsigned char *data_buffer );
a.__int16定义了一个不依赖于具体的硬件和软件环境,在任何环境下都占16 bit的整型数据(java 中的int类型是32 bit),这个数据类型是vc++中特定的数据类型,所以我自己做的dll也是用的vc++来编译.
b.__stdcall表示这个函数可以被其它程序调用,vc++编译的DLL欲被其他语言编写的程序调用,应将函数的调用方式声明为__stdcall方式,WINAPI都采用这种方式.c/c++语言默认的调用方式是__cdecl,所以在自己做可被java 程序调用的dll时一定要加上__stdcall的声明,否则在java 程序执行时会报类型不匹配的错误.
c.HANDLE icdev是windows操作系统中的一个概念,属于win32的一种数据类型,代表一个核心对象在某一个进程中的唯一索引,不是指针,在知道这个索引代表的对象类型时可以强制转换成此类型的数据.
这些知识都属于win32编程的范围,更为详细的win32资料可以查阅相关的文档.
这个方法的原始含义是通过设备初始时产生的设备标志号icdev,读取从某字符串在内存空间 中的相对超始位置offset开始的共len个字符,并存放到data_buffer指向的无符号字符类型的内存空间 中,并返回一个16 bit的整型值来标志这次的读设备是否成功,这里真正需要的是unsigned char *这个指针指向的地址 存放的数据,而java 中没有指针类型,所以可以考虑定义一个返回字符串类型的java 方法,原方法中返回的整型值也可以按经过一定的规则处理按字符串类型传出,由于HANDLE是一个类型于java 中的Ojbect类型的数据,可以把它当作int类型处理,这样java 程序中的方法定义就已经形成了:
String readData( int icdev, int offset, int len );
声明这个方法的时候要加上native关键字,表明这是一个与本地方法通信的java 方法,同时为了安全起见,此文方法要对其它类隐藏,使用private声明,再另外写一个public方法去调用它,同时要在这个类中把本地文件加载进来,最终的代码如下:
package test;
public class LinkDll
{
//从指定地址 读数据
private native String readData( int icdev, int offset, int len );
public String readData( int icdev, int offset, int len )
{
return this.readDataTemp( icdev, offset, len );
}
static
{
System.loadLibrary( "TestDll" );//如果执行环境是linux这里加载的是SO文件,如果是windows环境这里加载的是dll文件
}
}
2> 使用JDK的javah命令为这个类生成一个包含类中的方法定义的.h文件,可进入到class文件包的根目录下(只要是在 classpath参数中的路径即可),使用javah命令的时候要加上包名javah test.LinkDll,命令成功后生成一个名为test_LinkDll.h的头文件.
文件内容如下:
/* DO NOT EDIT THIS FILE - it is machine generated*/
#include <jni .h>
/* Header for class test_LinkDll */
#ifndef _Included_test_LinkDll #define
Included_test_LinkDll
#ifdef __cplusplus extern "C" { #endif
/*
* Class: test_LinkDll
* Method: readDataTemp
* Signature: (III)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java _test_LinkDll_readDataTemp(JNIEnv *, jobject, jint, jint, jint);
#ifdef __cplusplus } #endif
#endif
可以看出,JNI 为了实现和dll文件的通信,已经按它的标准对方法名/参数类型/参数数目作了一定的处理,其中的JNIEnv*/jobjtct这两个参数是每个JNI 方法固有的参数,javah命令负责按JNI 标准为每个java 方法加上这两个参数.JNIEnv是指向类型为JNIEnv_的一个特殊JNI 数据结构的指针,当由C++编译器编译时JNIEnv_结构其实被定义为一个类,这个类中定义了很多内嵌函数,通过使用"->"符号,可以很方便使用这些函数,如:
(env)->NewString( jchar* c, jint len )
可以从指针c指向的地址 开始读取len个字符封装成一个JString类型的数据.
其中的jchar对应于c/c++中的char,jint对应于c/c++中的len,JString对应于java 中的String,通过查看jni .h可以看到这些数据类型其实都是根据java 和c/c++中的数据类型对应关系使用typedef关键字重新定义的基本数据类型或结构体.
具体的对应关系如下:
Java 类型 本地类型 描述
boolean jboolean C/C++8位整型
byte jbyte C/C++带符号的8位整型
char jchar C/C++无符号的16位整型
short jshort C/C++带符号的16位整型
int jint C/C++带符号的32位整型
long jlong C/C++带符号的64位整型e
float jfloat C/C++32位浮点型
double jdouble C/C++64位浮点型
Object jobject 任何Java 对象,或者没有对应java 类型的对象
Class jclass Class对象
String jstring 字符串对象
Object[] jobjectArray 任何对象的数组
boolean[] jbooleanArray 布尔型数组
byte[] jbyteArray 比特型数组
char[] jcharArray 字符型数组
short[] jshortArray 短整型数组
int[] jintArray 整型数组
long[] jlongArray 长整型数组
float[] jfloatArray 浮点型数组
double[] jdoubleArray 双浮点型数组
更为详细的资料可以查阅JNI 文档.
需要注意的问题:test_LinkDll.h文件包含了jni .h文件;
3>使用vc++ 6.0编写TestDll.dll文件,这个文件名是和java 类中loadLibrary的名称一致.
a>使用vc++6.0 新建一个Win32 Dynamic-Link Library的工程文件,工程名指定为TestDll
b>把源代码文件和头文件使用"Add Fiels to Project"菜单加载到工程中,若使用c来编码,源码文件后缀名为.c,若使用c++来编码,源码文件扩展名为.cpp,这个一定要搞清楚,因为对于不同的语言,使用JNIEnv指针的方式是不同的.
c>在这个文件里调用设备商提供的dll文件,设备商一般提供三种文件:dll/lib/h,这里假设分别为A.dll/A.lib/A.h.
这个地方的调用分为动态调用和静态调用静态调用即是只要把被调用的dll文件放到path路径下,然后加载lib链接文件和.h头文件即可直接调用A.dll中的方法:
把设备商提供的A.h文件使用"Add Fiels to Project"菜单加载到这个工程中,同时在源代码文件中要把这个A.h文件使用include包含进来;
然后依次点击"Project->settings"菜单,打开link选项卡,把A.lib添加到"Object/library modules"选项中.
具体的代码如下:
//读出数据,需要注意的是如果是c程序在调用JNI 函数时必须在JNIEnv的变量名前加*,如(*env)->xxx,如果是c++程序,则直接使用(env)->xxx
#include<WINDOWS.H>
#include<MALLOC.H>
#include<STDIO.H>
#include<jni .h>
#include "test_LinkDll.h"
#include "A.h"
JNIEXPORT jstring JNICALL Java _test_LinkDll_readDataTemp( JNIEnv *env, jobject jo, jint ji_icdev, jint ji_len )
{
//*************************基本数据声明与定义******************************
HANDLE H_icdev = (HANDLE)ji_icdev;//设备标志符
__int16 i16_len = (__int16)ji_len;//读出的数据长度,值为3,即3个HEX形式的字符
__int16 i16_result;//函数返回值
__int16 i16_coverResult;//字符转换函数的返回值
int i_temp;//用于循环的中间变量
jchar jca_result[3] = { 'e', 'r', 'r' };//当读数据错误时返回此字符串
//无符号字符指针,指向的内存空间 用于存放读出的HEX形式的数据字符串
unsigned char* uncp_hex_passward = (unsigned char*)malloc( i16_len );
//无符号字符指针,指向的内存空间 存放从HEX形式转换为ASC形式的数据字符串
unsigned char* uncp_asc_passward = (unsigned char*)malloc( i16_len * 2 );
//java char指针,指向的内存空间 存放从存放ASC形式数据字符串空间读出的数据字符串
jchar *jcp_data = (jchar*)malloc(i16_len*2+1);
//java String,存放从java char数组生成的String字符串,并返回给调用者
jstring js_data = 0;
//*********读出3个HEX形式的数据字符到uncp_hex_data指定的内存空间 **********
i16_result = readData( H_icdev, 6, uncp_hex_data );//这里直接调用的是设备商提供的原型方法.
if ( i16_result != 0 )
{
printf( "读卡错误......\n" );
//这个地方调用JNI 定义的方法NewString(jchar*,jint),把jchar字符串转换为JString类型数据,返回到java 程序中即是String
return (env)->NewString( jca_result, 3 );
}
printf( "读数据成功......\n" );
//**************HEX形式的数据字符串转换为ASC形式的数据字符串**************
i16_coverResult = hex_asc( uncp_hex_data, uncp_asc_data, 3 );
if ( i16_coverResult != 0 )
{
printf( "字符转换错误!\n" );
return (env)->NewString( jca_result, 3 );
}
//**********ASC char形式的数据字符串转换为jchar形式的数据字符串***********
for ( i_temp = 0; i_temp < i16_len; i_temp++ )
jcp_data[i_temp] = uncp_hex_data[i_temp];
//******************jchar形式的数据字符串转换为java String****************
js_data = (env)->NewString(jcp_data,i16_len);
return js_data;
}
动态调用,不需要lib文件,直接加载A.dll文件,并把其中的文件再次声明,代码如下:
#include<STDIO.H>
#include<WINDOWS.H>
#include "test_LinkDll.h"
//首先声明一个临时方法,这个方法名可以随意定义,但参数同设备商提供的原型方法的参数保持一致.
typedef int ( *readDataTemp )( int, int, int, unsigned char * );//从指定地址 读数据
//从指定地址 读数据
JNIEXPORT jstring JNICALL Java _readDataTemp( JNIEnv *env, jobject jo, jint ji_icdev, jint ji_offset, jint ji_len )
{
int i_temp;
int i_result;
int i_icdev = (int)ji_icdev;
int i_offset = (int)ji_offset;
int i_len = (int)ji_len;
jchar jca_result[5] = { 'e', 'r', 'r' };
unsigned char *uncp_data = (unsigned char*)malloc(i_len);
jchar *jcp_data = (jchar *)malloc(i_len);
jstring js_data = 0;
//HINSTANCE是win32中同HANDLE类似的一种数据类型,意为Handle to an instance,常用来标记App实例,在这个地方首先把A.dll加载到内存空间 ,以一个App的形式存放,然后取
得它的instance交给dllhandle,以备其它资源使用.
HINSTANCE dllhandle;
dllhandle = LoadLibrary( "A.dll" );
//这个地方首先定义一个已声明过的临时方法,此临时方法相当于一个结构体,它和设备商提供的原型方法具有相同的参数结构,可互相转换
readDataTemp readData;
//使用win32的GetProcAddress方法取得A.dll中定义的名为readData的方法,并把这个方法转换为已被定义好的同结构的临时方法,
//然后在下面的程序中,就可以使用这个临时方法了,使用这个临时方法在这时等同于使用A.dll中的原型方法.
readData = (readDataTemp) GetProcAddress( dllhandle, "readData" );
i_result = (*readData)( i_icdev, i_offset, i_len, uncp_data );
if ( i_result != 0 )
{
printf( "读数据失败......\n" );
return (env)->NewString( jca_result, 3 );
}
for ( i_temp = 0; i_temp < i_len; i_temp++ )
{
jcp_data[i_temp] = uncp_data[i_temp];
}
js_data = (env)->NewString( jcp_data, i_len );
return js_data;
}
4>以上即是一个java 程序调用第三方dll文件的完整过程,当然,在整个过程的工作全部完成以后,就可以使用java 类LinkDll中的public String radData( int, int, int )方法了,效果同直接使用c/c++调用这个设备商提供的A.dll文件中的readData方法几乎一样.
总结:JNI 技术确实是提高了java 程序的执行效率,并且扩展了java 程序的功能,但它也确确实实破坏了java 程序的最重要的优点:平台无关性,所以除非必须(不得不)使用JNI 技术,一般还是提倡写100%纯java 的程序.根据自己的经验及查阅的一些资料,把可以使用JNI 技术的情况罗列如下:
1>需要直接操作物理设备,而没有相关的驱动程序,这时候我们可能需要用C甚至汇编语言来编写该设备的驱动,然后通过JNI 调用;
2>涉及大量数学运算的部分,用java 会带来些效率上的损失;
3>用java 会产生系统难以支付的开销,如需要大量网络链接的场合;
4>存在大量可重用的c/c++代码,通过JNI 可以减少开发工作量,避免重复开发.
另外,在利用JNI 技术的时候要注意以下几点:
1>由于Java 安全机制的限制,不要试图通过Jar文件的方式发布包含本地化方法的Applet到客户端;
2>注意内存管理问题,虽然在本地方法返回Java 后将自动释放局部引用,但过多的局部引用将使虚拟机在执行本地方法时耗尽内存;
3>JNI 技术不仅可以让java 程序调用c/c++代码,也可以让c/c++代码调用java 代码.
注: 有一个名叫Jawin开源项目实现了直接读取第三方dll文件,不用自己辛苦去手写一个起传值转换作用的dll文件,有兴趣的可以研究一下.但 是我用的时候不太顺手,有很多规则限制,像自己写程序时可以随意定义返回值,随意转换类型,用这个包的话这些都是不可能的了,所以我的项目还没开始就把它 抛弃了.
Chap9:如何编写jni方法(转载)
作者:crazycow 发布时间:2008-11-16 14:44:21.0
http://blog.chinaunix.net/u1/38994/showart_1099528.html
一、概述:
在 这篇文章中将会简单介绍如何编制一些简单的JNI 方法。我们都知道JNI方法可以帮助我们调用用C/c++编写的函数,这样如果一项工作已经用C /c++语言实现的话,我们就可以不用花很大的力气再用JAVA语言对这一工作进行再实现,只要编制相应的JNI函数,就可以轻松实现JAVA语言对C /c++函数的调用,从而大大减轻程序开发人员的工作量。
在这个项目中,我们编制了很多小实例,通过阅读,运行这些小实例,你可以轻松的学会如何编制JNI方法。这篇文档可以帮助你更好的理解及实现这些实例。
现 在让我们进入主题。首先,我们看一下这个项目的体系构架。该项目分为两部分,一部分用c语言是c语言的例子,另一部分是c++语言的例子。每部分都包含 java,src源文件目录,以及一个Makefile文件。java目录中是需要调用JNI函数的JAVA源程序,含有后缀名.java。src 目录 中含有JNI函数的实现代码,包括.c或.cpp文件和.h文件。Makefile文件是对 java 、src 目录下的文件进行编译组织进而生成可执 行文件的文件。当Makefile文件执行以后还会生成以下子目录:lib , class ,bin目录 。lib 目录中包含项目中生成的静态函数库 文件libJNIExamples.so,java程序所调用的JNI方法都是通过这个库来调用的。class 目录中包含由java目录下 的.java 文件生成的.class文件。bin目录中是一个可执行的shell脚本文件。在执行该脚本的时候,项目所有程序实例的运行结果都将一并显 示在屏幕上。
具体执行步骤为:
make
cd bin
./run.sh
下面来介绍一下在这个项目中所实现的实例:
1. 如何调用标准C/c++中的函数--例如:printf(...)
2. 如何调用C/c++中自定义的函数
3. 如何在jni函数中访问java类中的对象实例域
4. 如何在jni函数中访问java类中的静态实例域
5. 如何在jni函数中调用java对象的方法
6. 如何在jni函数中调用java类的静态方法
7. 如何在jni函数中传递基本数据类型参数
8. 如何在jni函数中传递对象类型参数
9. 如何在jni函数中处理字符串
10. 如何在jni函数中处理数组
11. 处理jni函数中的返回值情况
12. 在jni中实现创建java类对象
二、基本步骤:
在介绍这些例子之前,让我们先来看看编写jni方法所需要的基本步骤,这些实例都是用c来实例来讲解,至于c++的实例和c的实例区别不大,只要作稍微的修改即可,在文档的末尾我们将介绍这些内容:
1、要想定义jni方法,首先得要在java语言中对这一方法进行声明(自然这一声明过程要在类中进行)
声明格式如下:
publicnativevoid print(); System.loadLibrary(“JNIExamples”); }
jni 函数用关键字native方法声明。
2、对该类的源文件进行编译使用javac命令,生成相应的.class文件。
3、用javah -jni为函数生成一个在java调用和实际的c函数之间的转换存根,该存根通过从虚拟机栈中取出参数信息,并将其传递给已编译的C函数来实现转换。
4、建立一个特殊的共享库,并从该共享库到处这个存根,在上面的例子中使用了System.loadLibrary,来加载libJNIExamples共享库。
三、配置运行环境:
在 编写一个简单的jni函数之前我们必须配置相应的运行环境。jdk的配置在这里就不作介绍,这里主要说的是库的路径。当调用 System.loadLibrary(..)时,编译器会到我们系统设置的库路径中寻找该库。修改路径的方法和修改任何环境变量的方法基本相同,只要在 /etc/bash.bashrc目录下增加一行LD_LIBRARY_PATH=.:./lib:$(LD_LIBRARY_PATH)即可。也可以通 过命令行export LD_LIBRARY_PATH=.:./lib:$(LD_LIBRARY_PATH)
四、运行实例分析:
1、实例一:在jni中调用标准c中自带的函数printf():
下面以实例1为例来详细说明编写jni方法的详细过程。
(1)、定义包含jni函数的类Print.java:
{
/*********************************************************************** * the print() function will call the printf() funcion which is a ANSI c funciton * *************************************************************************/
Public native void print();
System.loadLibrary("JNIExamples");
}
}
在 上面的实例 中,使用public native void print();语句来定义了一个Print类的jni方法。并用 Sysgem.loadLibrary(“JNIExamples”)语句来加载libJNIExamples.so库。注意:加载的语句一定要用 static关键字声明在静态块中,以保证引用该类时该库始终被加载。
(2)、对该类进行编译:javac Print.java。生成Print.class类,然后用javah 产生一个Print.h的头文件:javah Print。长生的Print.h文件格式如下:
/* DO NOT EDIT THIS FILE - it is machine generated *//* Header for class Print */ JNIEXPORT void JNICALL Java_Print_print (JNIEnv *, jobject); }
其中的加粗字体为要实现的JNI函数生命部分。
(3)、编写JNI函数的实现部分Print.c
JNIEXPORT void JNICALL Java_Print_print (JNIEnv *env, jobject obj)
{
printf("example1:in this example a printf() function in ANSI C is called\n");
printf("Hello,the output is generated by printf() function in ANSI C\n");
}
在这个文件中实现了一个简单的Jni方法。该方法调用ANSI C 中的printf()函数,输出了两个句子。
(4)、将本地函数编译到libJNIExamples.so的库中:
使用语句:gcc -fPIC -I/usr/jdk1.5/include -I/usr/jdk1.5/include/linux -shared -o libJNIExamples.so Print.c。
(5)、至此Jni函数已全部实现,可以在java代码中调用拉。
在此我们使用一个简单的类来对实现的jni方法进行测试,下面是PrintTest.java的源代码部分:
publicstaticvoid main(String[] args) { Print p = new Print(); p.print(); } }
(6)、对PrintTest.java进行编译执行得到如下结果:
example1:in this example a printf() function in ANSI C is called
Hello,the output is generated by printf() function in ANSI C .
下面介绍的每个实例实现的步骤也都是按着上述步骤执行的。所以介绍时只介绍实现的关键部分。
2、实例二、调用c 语言用户定义的函数
(源程序为:java/Cfunction.java java/C_functionTest.java src/Cfunction.c src/Cfunction.h )
当需要在java程序中调用用c所实现的函数是,需要在需要调用该c函数的类中定义一个jni方法,在该jni方法中去调用该c函数,相当于用java方法把c函数封装起来,以供java程序调用。
在实例二中我们简单定义了一个printHello()函数,该函数的功能只是输出一句话,如果要在java程序中调用该函数,只需在jni函数中调用即可,和调用ANSI C中自带的prinf()函数没有任何区别。
3、实例三、在jni函数中访问java类中的对象实例域
(源程序为:java/CommonField.java java/CommonFieldTest.java src/CommonField.c src/CommonField.h )
jni函数的实现部分是在c 语言中实现的,如果它想访问java中定义的类对象的实例域需要作三步工作,
(1)调用GetObjectClass()函数得到该对像的类,该函数返回一个jclass类型值。
(2)调用GetFieldID()函数得到要访问的实例域在该类中的id。
(3)调用GetXXXField()来得到要访问的实例域的值。其中XXX和要访问的实例域的类型相对应。
在jni中java 编程语言和c 语言数据类型的对应关系为java原始数据类型前加 'j' 表示对应c语言的数据类型例如boolean 为jboolean ,int 为 jint,double 为jdouble等。对象类型的对应类型为jobject。
在 本实例中,您可以看到我们在java/CommonField.java 中定义了类CommonField类,其中包含int a , int b 两 个实例域,我们要在jni函数getCommonField()中对这两个域进行访问和修改。你可以在 src/CommonField.c中找到该函数 的实现部分。
以下语句是对该域的访问(以下代码摘自:src/CommonField.c):
jclass class_Field = (*env)->GetObjectClass(env,obj);
jfieldID fdA = (*env)->GetFieldID(env,class_Field,"a","I");
jfieldID fdB = (*env)->GetFieldID(env,class_Field,"b","I");
jint valueA = (*env)->GetIntField(env,obj,fdA);
jint valueB = (*env)->GetIntField(env,obj,fdB);
在jni 中对所有jni函数的调用都要用到env指针,该指针也是每一个本地方法的第一个参数,他是函数指针表的指针,所以,必须在每一个jni调用前面加上 (*env)->GetObjectClass(env,obj)函数调用返回obj对像的类型,其中obj 参数表示要你想要得到类型的类对象。
jfieldID GetFieldID(JNIEnv *env,jclass cl, const char name[], const char sig[]) 该 函数返回一个域的标识符name 表示域名,sig表示编码的域签名。所谓编码的签名即编码类型的签名在上例中类中的a实例域为int 型,用"I”来表 示,同理"B” 表示byte ,"C” 表示 char , “D”表示 double ,”F” 表示float,“J”表示long, “S” 表 示short , “V” 表示void ,”Z”表示 boolean类型。
GetIntField(env,obj,fdA),用来访问obj对象的fdA域,如果要访问的域为double类型,则要使用GetDoubleField(env,obj,fdA)来访问,即类型对应GetXXXField中的XXX。
以下函数用来修改域的值:
(*env)->SetIntField(env,obj,fdA,109); (*env)->SetIntField(env,obj,fdB,145);
这和获得域的值类似,只是该函数多了一个要设置给该域的值参数。
访问对象实例域的相关函数如下:
jfieldID GetFieldID(JNIEnv *env, jclass cl, const char name[], const char sig[])
该函数返回一个域的标识符。各参数含义如下:
env JNI 接口指针;cl 类对象 ; name 域名; sig 编码的域签名
XXX GetXXXField(JNIEnv *env, jobject obj, jfieldID id)
该函数返回域的值。域类型XXX是Object, Boolean, byte, char , short, int ,long ,float, double 中类型之一。
参数 env JNI借口指针;obj为域所在对象;id为域的标识符。
void SetXXXField(JNIEnv *env,jobject obj, jfieldID id, XXX value)
该函数用于设置域的值。XXX的含义同上,
参数中env, obj , id 的含义也同上,value 值为将要设置的值。
4、实例四:在jni函数中访问类的静态实例域
(java/Field.java java/FieldTest.java src/Field.c src/Field.h)
因为静态实例域并不属于某个对象,而是属于一个类,所以在要访问静态实例域时,和访问对象的实例域不同,它所调用的函数是(以实例四来说明,一下代码摘自src/Field.c):
jclass class_Field = (*env)->FindClass(env,"Field");
jfieldID fdA = (*env)->GetStaticFieldID(env,class_Field,"a","I");
jint valueA = (*env)->GetStaticIntField(env,class_Field,fdA);
(*env)->SetStaticIntField(env,class_Field,fdA,111);
由 于没有 对象,必须使用FindClass代替GetObjectClass来获得类引用。在FindClass()的第二个参数是类的编码签名,类的编码签名和 基本类型的编码签名有所不同,如果类在当前包中,就直接是类的名称,如果类不在当前包中则要加入该类的详细路径:例如String类在java.lang 包中,则String的签名要写成( Ljava/lang/String;),其中的(L和;)是不可少的,其中(;)是表达是的终止符。其他三个函数 和访问对象数据域基本没什么区别。
5、实例五:在jni函数中调用java对象的方法
(java/CommonMethod.java java/CommonMethodTest.java src/CommonMehod.c src/CommonMethod.h )
在jni函数中我们不仅要对java对象的数据域进行访问,而且有时也需要调用java中类对象已经实现的方法,实例五就是关于这方面的实现的。在src/CommonMethod.c中我们可以找到下面的代码:
JNIEXPORT void JNICALL Java_CommonMethod_callMethod (
JNIEnv *env, jobject obj, jint a, jstring s)
{
printf("example 5:in this example,a object's method will be called\n");
jclass class_CommonMethod = (*env)->GetObjectClass(env,obj);
jmethodID md = (*env)->GetMethodID(env,class_CommonMethod,"print",
"(ILjava/lang/String;)V");
(*env)->CallVoidMethod(env,obj,md,a,s);
}
该代码部分展示了如何实现对java类对象函数的调用过程。从以上代码部分我们可以看到,要实现该调用需要有三个步骤,调用三个函数
jclass class_CommonMethod = (*env)->GetObjectClass(env,obj);
jmethodID md = (*env)->GetMethodID(env,class_CommonMethod,"print","(ILjava/lang/String;)V");
(*env)->CallVoidMethod(env,obj,md,a,s);
GetObjectClass(...)函数获得要调用对象的类;GetMethodID(...)获得要调用的方法相对于该类的ID号;CallXXXMethod(...)调用该方法。
在 编写该调用过程的时候,需要注意的仍然是GetMethodID(...)函数中编码签名的问题,在该实例中,我们要做的是找到CommonMethod 类的print(int a, String s)方法,该方法打印整数a,和字符串s 的直。在函数的编码签名部分(该部分以加粗、并加有下划 线)GetMethodID(env,class_CommonMethod,"print","(ILjava/lang /String;)V"); 从左往右可以查看,括号中的内容为要调用方法的参数部分内容,I表示第一个参数为int类型,“Ljava/lang /String;”表示第二个参数为String类型,V表示返回值类型为空void,如果返回值类型不为空,则使用相应的类型签名。返回值类型是和下面 将要使用的调用该方法的函数CallXXXMethod(...)相关联的,该函数的xxx要用相应的类型来替换,在此实例中为void,如果返回值类型 为int类型则调用该方法的函数就为CallIntMethod(...)。
6、实例六:在jni函数中调用java类的静态方法
(java/Method.java java/MethodTest.java src/Method.h src/Method.c)
实例五中介绍了如何调用类对象的方法,在此实例中我们将介绍如何调用java类的静态方法在此实例中我们在/java/Method.java中定义了静态方法:
public static void print() {
System.out.println("this is a static method of class Method");
}
该函数的功能就是打印字符串“ this is a static method of class Method”;
我们在src/Method.c中实现了对该方法调用的jni函数:
JNIEXPORT void JNICALL Java_Method_callMethod (JNIEnv *env, jobject obj)
{
printf("example 6:in this example, the class's static method will be called\n");
jclass class_Method = (*env)->FindClass(env,"Method");
jmethodID md = (*env)->GetStaticMethodID(env,class_Method,"print","()V");
(*env)->CallStaticVoidMethod(env,class_Method,md); }
和实例五不同的是,我们要调用的三个函数变为:
FindClass(...)、GetStaticMethodID(...)、CallStaticVoidMethod(...)。
其中的机制和实例五是一样的。再次就不做过多的介绍。
7、实例七:jni函数中传递基本数据类型参数
(java/Basic.java java/BasicTest.java src/Basic.c src/Basic.h) 在java/Basic.java中,我们定义了一个public native void raiseValue(int a)函数,该函数将打印使value的值增加a,并打印原来的value和新的value值。
在src/Basic.c中给出了该jni函数的实现部分。
JNIEXPORT void JNICALL Java_Basic_raiseValue (
JNIEnv *env, jobject obj, jint a)
{
printf("example 7: in this example, a integer type parament will be passed to the jni method\n");
jclass class_Basic = (*env)->GetObjectClass(env,obj);
jfieldID fd = (*env)->GetFieldID(env,class_Basic,"value","I");
jint v = (*env)->GetIntField(env,obj,fd);
v = v+a;
(*env)->SetIntField(env,obj,fd,v);
}
在 此函数实现中,因为要访问 Basic类中的value域,所以调用了 GetObjectClass(...), GetFieldID(...), GetIntField(...)函数获取value值,下面一步 的 “ = v+a; ”说明,传递基本类型参数的处理方式和在c语言中的基本数据类型的处理无异。
8、实例八:在jni函数中传递对象类型参数
(java/Book.java java/BookTest.java src/BookTest.c src/BookTest.h)
在该实例中演示了在jni函数中传递对象函数的过程。
我们在该实例中定义了一个类Book
total_page = t;
}
publicint getTotalPage() { }
publicint getCurrentPage() { }
current_page++;
}
}
然后我们在java/BookTest.java中定义jni函数
public native void bookCurrentStatus(Book b);
该函数需要一个Book类型的参数,并返回该参数的当前状态,包括该书一共有多少页的total_page,以及当前页current_page。函数的实现部分为(src/BookTest.c)
JNIEXPORT void JNICALL Java_BookTest_bookCurrentStatus (JNIEnv *env,
jobject this_obj, jobject obj)
{
printf("example 8: in this example, a object parament will be passed to the jni method。\n");
jclass class_book = (*env)->GetObjectClass(env,obj);
jmethodID id_getTotal = (*env)->GetMethodID(env,
class_book,"getTotalPage","()I");
jmethodID id_getCurrent = (*env)->GetMethodID(env,
class_book,"getCurrentPage","()I");
jint total_page = (*env)->CallIntMethod(env,
obj,id_getTotal);
jint current_page = (*env)->CallIntMethod(env,
obj,id_getCurrent);
printf("the total page is:%d and the current page is :%d\n",
total_page,current_page);
}
该 函数包含三个参数(JNIEnv *env, jobject this_obj, jobject obj) ,第二 个jobject this_obj参数表示当前的jni 函数所属于的类对象,第三个jobject obj参数表示传递的参数Book类型的类对象。
对于实现部分,基本和实例五--调用java类对象的方法中的操作相同,就不作详解。
9、实例九:在jni函数中处理字符串
(java/Str.java java/StrTest.java src/Str.c src/Str.h)
在该实例中我们讲解如何传递、处理字符串参数。
在java/Str.java中我们定义了一个 printString(String s) 的方法,用来处理字符串参数。
在src/Str.c中我们可以看到该函数的实现部分:
JNIEXPORT void JNICALL Java_Str_printString (JNIEnv *env,
jobject obj, jstring s)
{
printf("example 9: in this example, a String object parament will be passed to the jni method.\n");
const char* string = (char*)(*env)->GetStringUTFChars(env,s,NULL);
printf("%s is put out in native method\n",string);
(*env)->ReleaseStringUTFChars(env,s,(jbyte*)string);
}
实现过程中调用了两个函数:GetStringUTFChars(...)、 ReleaseStringUTFChars(...)。
GetStringUTFChars(...) 用来获取String对象的字符串,并将其抓那还为char*类型,这应该字符串就可以在c语言中进行处理拉。 ReleaseStringUTFChars(...)用于当该字符串使用完成后,将其进行垃圾回收。记住,当使用完字符串时一定不要忘记调用该函数。
10、实例十:在jni函数中处理数组
(java/Arr.java java/ArrTest.java src/Arr.c src/Arr.h)
java中所有的数组类型都有相对应的c语言类型,其中jarray类型表示一个泛型数组
boolean[] --jbooleanArray
byte[]--jbyteArray
char[]--jcharArary
int[]---jcharArray
short[]---jshortArray
long[]---jlongArray
float[]--jfloatArray
double[]—-jdoubleArray
Object[]--- jobjectArray。
当访问数组时,可以通过GetObjectAraryElement和SetObjectArrayElement方法访问对象数组的元素。
而 对于一般类型数组,你可以调用GetXXXAraryElements来获取一个只想数组起始元素的指针,而当你不在使用该数组时,要记得调用 ReleaseXXXArrayElements,这样你所作的改变才能保证在原始数组里得到反映。当然如果你需要得到数组的长度,可以调用 GetArrayLength函数。
在本实例中,我们在Arr.java中定义一个本地方法:print(int intArry[]),该函数的功能为对该数组进行输出,在src/Arr.c中我们可以看到该方法的实现过程如下:
JNIEXPORT void JNICALL Java_Arr_print (JNIEnv *env,
jobject obj, jintArray intArray)
{
printf("example 10:in this example, a array parament will be passed to the jni method.\n");
jint* arr = (*env)->GetIntArrayElements(env,intArray,NULL);
n = (*env)->GetArrayLength(env,intArray);
printf("the native method output the int array\n");
for( i = 0;i<(*env)->GetArrayLength(env,intArray);i++)
{
printf("%d ",arr[i]);
}
(*env)->ReleaseIntArrayElements(env,intArray,arr,0);
}
我们在此调用了GetIntArrayElements(...)来获取一个指向intArray[]数组第一个元素的指针。
用getArrayLength(..)函数来得到数组的长度,以方便数组遍历时使用。最后应用ReleaseArrayElements(...)函数来释放该数组指针。
11、实例十一:在jni中的返回值问题
(java/ReturnValue.java java/ReturnValueTest.java java/BookClass.java src/ReturnValue.c src/ReturnValue.h)
在java/ReturnValue类中定义了三个jni方法: returnInt(),returnString() ,returnObject()
三个方法,分别返回int , String , Object 类型的值。
其在src/ReturnValue.c中的实现分别为:
JNIEXPORT jint JNICALL Java_ReturnValue_returnInt (
JNIEnv *env, jobject obj)
{
jclass class_ReturnValue = (*env)->GetObjectClass(env,obj);
jfieldID fd = (*env)->GetFieldID(env,class_ReturnValue,"value","I");
jint v = (*env)->GetIntField(env,obj,fd);
return v;
}
* Signature: ()Ljava/lang/String;
JNIEXPORT jstring JNICALL Java_ReturnValue_returnString (
JNIEnv *env, jobject obj)
{
printf("example 11: in this example, the int and object of return value will be proceeding\n");
jclass class_ReturnValue = (*env)->GetObjectClass(env,obj);
jfieldID fd = (*env)->GetFieldID(env,class_ReturnValue,"name","Ljava/lang/String;");
jstring jstr = (jstring)(*env)->GetObjectField(env,obj,fd);
}
* * Method: returnObject
JNIEXPORT jobject JNICALL Java_ReturnValue_returnObject (
JNIEnv *env, jobject obj)
{
jclass class_ReturnValue = (*env)->GetObjectClass(env,obj);
jfieldID fd = (*env)->GetFieldID(env,class_ReturnValue,"myBook","LBookClass;");
jobject jbook = (jstring)(*env)->GetObjectField(env,obj,fd);
}
在这里分别涉及到了对java类对象的一般参数,String参数,以及Object参数的访问。
12、实例十二:在jni中创建java类对象:
(java/Test.java src/CreateObj.c src/CreateObj.h)
如果想要在jni函数创建java类对象则要引用java 类的构造器方法,通过调用NewObject函数来实现。
NewObject函数的调用方式为:
jobject obj_new = (*env)->NewObject(env,class, methodid, paraments);
在该实例中,我们在java/Test.java 中定义了Book1类,要在CreateObj类的modifyProperty() jni方法中创建该类对象。我们可以在src/CreateObj.c中看到该jni方法创建对象的过程:
jobject book;
jclass class_book;
jmethodID md_book;
class_book = (*env)->FindClass(env,"LBook1;");
md_book = (*env)->GetMethodID(env,class_book,"<init>","(IILjava/lang/String;)V");
book = (*env)->NewObject(env,class_book,md_book,100,1,"huanghe");
在 创建对象的过程中可以看到,要创建一个java类对象,首先需要得到得到使用FindClass函数得到该类,然后使用GetMethodID方法得到该 类的构造器方法id,主义在此时构造器的函数名始终为:"”,其后函数的签名要符合函数签名规则。在此我们的构造器有三个参 数:int , int, String.
并且其返回值类型要永久为空,所以函数签名为:"(IILjava/lang/String;)V"
然后我们调用NewObject()函数来创建该类的对象,在此之后就可以使用该对象拉。
以上内容介绍的是jni函数c语言的实现实例。如果想要使用c++的实例,我们只需要把其中的每一个函数调用过程作稍微的修改:
例如:(*env)->NewObject(env,class_book,md_book,100,1,”huanghe”);
修改为:(env)->NewObject(class_book,md_book,100,1,”huanghe”);
即修改(*env)为(env)再把参数中的env去掉。然后把所有c的函数改为c++的函数就OK拉。
具体情况可以去查看我们的c++实例代码.
Chap10:在 Windows 中实现 Java 本地方法
级别: 初级
David Wendt WebSphere Development Research Triangle Park, NC
1999 年 5 月 01 日
本文为在 32 位 Windows 平台上实现 Java 本地方法提供了实用的示例、步骤和准则。这些示例包括传递和返回常用的数据类型。
本文中的示例使用 Sun Microsystems 公司创建的 Java DevelopmentKit (JDK) 版本 1.1.6 和 Java本地接口 (JNI ) 规范 。 用 C 语言编写的本地代码是用 MicrosoftVisual C++ 编译器编译生成的。
简介
本文提供调用本地 C 代码的 Java 代码示例,包括传递和返回某些常用的数据类型。本地方法包含在特定于平台的可执行文件中。就本文中的示例而言,本地方法包含在 Windows 32 位动态链接库 (DLL) 中。
不过我要提醒您,对 Java 外部的调用通常不能移植到其他平台上,在 applet 中还可能引发安全异常。实现本地代码将使您的 Java 应用程序无法通过 100% 纯 Java 测试。但是,如果必须执行本地调用,则要考虑几个准则:
- 将您的所有本地方法都封装在单个类中,这个类调用单个 DLL。对于每种目标操作系统,都可以用特定于适当平台的版本替换这个 DLL。这样就可以将本地代码的影响减至最小,并有助于将以后所需的移植问题包含在内。
- 本地方法要简单。尽量将您的 DLL 对任何第三方(包括 Microsoft)运行时 DLL 的依赖减到最小。使您的本地方法尽量独立,以将加载您的 DLL 和应用程序所需的开销减到最小。如果需要运行时 DLL,必须随应用程序一起提供它们。
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
1.Java 调用 C
对于调用 C 函数的 Java 方法,必须在 Java 类中声明一个本地方法。在本部分的所有示例中,我们将创建一个名为 MyNative 的类,并逐步在其中加入新的功能。这强调了一种思想,即将本地方法集中在单个类中,以便将以后所需的移植工作减到最少。
示例 1 -- 传递参数
在第一个示例中,我们将三个常用参数 类型传递给本地函数: String 、 int 和 boolean 。本例说明在本地 C 代码中如何引用这些参数 。
public class MyNative { public void showParms( String s, int i, boolean b ) { showParms0( s, i , b ); } private native void showParms0( String s, int i, boolean b ); static { System.loadLibrary( "MyNative" ); } } |
请注意,本地方法被声明为专用的,并创建了一个包装方法用于公用目的。这进一步将本地方法同代码的其余部分隔离开来,从而允许针对所需的平台对它进行优化。 static 子句加载包含本地方法实现的 DLL。
下 一步是生成 C 代码来实现 showParms0 方法。此方法的 C 函数原型是通过对 .class 文件使用 javah 实用程序来创建的,而 .class 文件是通过编译 MyNative.java 文件生成的。这个实用程序可在 JDK 中找到。下面是 javah 的用法:
javac MyNative.java(将 .java 编译为 .class) javah -jni MyNative(生成 .h 文件) |
这将生成一个 MyNative.h 文件,其中包含一个本地方法原型,如下所示:
/* * Class: MyNative * Method: showParms0 * Signature: (Ljava/lang/String;IZ)V */ JNIEXPORT void JNICALL Java_MyNative_showParms0 (JNIEnv *, jobject, jstring, jint, jboolean); |
第一个参数 是调用 JNI 方法时使用的 JNI Environment 指针。第二个参数 是指向在此 Java 代码中实例化的 Java 对象 MyNative 的一个句柄。其他参数 是方法本身的参数 。请注意,MyNative.h 包括头文件 jni .h。jni .h 包含 JNI API 和变量类型(包括jobject、jstring、jint、jboolean,等等)的原型和其他声明。
本地方法是在文件 MyNative.c 中用 C 语言实现的:
#include <stdio.h> #include "MyNative.h" JNIEXPORT void JNICALL Java_MyNative_showParms0 (JNIEnv *env, jobject obj, jstring s, jint i, jboolean b) { const char* szStr = (*env)->GetStringUTFChars( env, s, 0 ); printf( "String = [%s]\n", szStr ); printf( "int = %d\n", i ); printf( "boolean = %s\n", (b==JNI _TRUE ? "true" : "false") ); (*env)->ReleaseStringUTFChars( env, s, szStr ); } |
JNI API,GetStringUTFChars,用来根据 Java 字符串或 jstring 参数 创建 C 字符串。这是必需的,因为在本地代码中不能直接读取 Java 字符串,而必须将其转换为 C 字符串或 Unicode。有关转换 Java 字符串的详细信息,请参阅标题为 NLS Strings and JNI 的一篇论文。但是,jboolean 和 jint 值可以直接使用。
MyNative.dll 是通过编译 C 源文件创建的。下面的编译语句使用 Microsoft Visual C++ 编译器:
cl -Ic:\jdk1.1.6\include -Ic:\jdk1.1.6\include\win32 -LD MyNative.c -FeMyNative.dll |
其中 c:\jdk1.1.6 是 JDK 的安装路径。
MyNative.dll 已创建好,现在就可将其用于 MyNative 类了。
可以这样测试这个本地方法:在 MyNative 类中创建一个 main 方法来调用 showParms 方法,如下所示:
public static void main( String[] args ) { MyNative obj = new MyNative(); obj.showParms( "Hello", 23, true ); obj.showParms( "World", 34, false ); } |
当运行这个 Java 应用程序时,请确保 MyNative.dll 位于 Windows 的 PATH 环境变量所指定的路径中或当前目录下。当执行此 Java 程序时,如果未找到这个 DLL,您可能会看到以下的消息:
java MyNative Can't find class MyNative |
这是因为 static 子句无法加载这个 DLL,所以在初始化 MyNative 类时引发异常。Java 解释器处理这个异常,并报告一个一般错误,指出找不到这个类。
如果用 -verbose 命令行选项运行解释器,您将看到它因找不到这个 DLL 而加载 UnsatisfiedLinkError 异常。
如果此 Java 程序完成运行,就会输出以下内容:
java MyNative String = [Hello] int = 23 boolean = true String = [World] int = 34 |
boolean = false 示例 2 -- 返回一个值
本例将说明如何在本地方法中实现返回代码。
将这个方法添加到 MyNative 类中,这个类现在变为以下形式:
public class MyNative { public void showParms( String s, int i, boolean b ) { showParms0( s, i , b ); } public int hypotenuse( int a, int b ) { return hyptenuse0( a, b ); } private native void showParms0( String s, int i, boolean b ); private native int hypotenuse0( int a, int b ); static { System.loadLibrary( "MyNative" ); } /* 测试本地方法 */ public static void main( String[] args ) { MyNative obj = new MyNative(); System.out.println( obj.hypotenuse(3,4) ); System.out.println( obj.hypotenuse(9,12) ); } } |
公用的 hypotenuse 方法调用本地方法 hypotenuse0 来根据传递的参数 计算值,并将结果作为一个整数返回。这个新本地方法的原型是使用 javah 生成的。请注意,每次运行这个实用程序时,它将自动覆盖当前目录中的 MyNative.h。按以下方式执行 javah:
javah -jni MyNative |
生成的 MyNative.h 现在包含 hypotenuse0 原型,如下所示:
/* * Class: MyNative * Method: hypotenuse0 * Signature: (II)I */ JNIEXPORT jint JNICALL Java_MyNative_hypotenuse0 (JNIEnv *, jobject, jint, jint); |
该方法是在 MyNative.c 源文件中实现的,如下所示:
#include <stdio.h> #include <math.h> #include "MyNative.h" JNIEXPORT void JNICALL Java_MyNative_showParms0 (JNIEnv *env, jobject obj, jstring s, jint i, jboolean b) { const char* szStr = (*env)->GetStringUTFChars( env, s, 0 ); printf( "String = [%s]\n", szStr ); printf( "int = %d\n", i ); printf( "boolean = %s\n", (b==JNI _TRUE ? "true" : "false") ); (*env)->ReleaseStringUTFChars( env, s, szStr ); } JNIEXPORT jint JNICALL Java_MyNative_hypotenuse0 (JNIEnv *env, jobject obj, jint a, jint b) { int rtn = (int)sqrt( (double)( (a*a) + (b*b) ) ); return (jint)rtn; } |
再次请注意,jint 和 int 值是可互换的。
使用相同的编译语句重新编译这个 DLL:
cl -Ic:\jdk1.1.6\include -Ic:\jdk1.1.6\include\win32 -LD MyNative.c -FeMyNative.dll |
现在执行 java MyNative 将输出 5 和 15 作为斜边的值。
示例 3 -- 静态方法
您可能在上面的示例中已经注意到,实例化的 MyNative 对象是没必要的。实用方法通常不需要实际的对象,通常都将它们创建为静态方法。本例说明如何用一个静态方法实现上面的示例。更改 MyNative.java 中的方法签名,以使它们成为静态方法:
public static int hypotenuse( int a, int b ) { return hypotenuse0(a,b); } ... private static native int hypotenuse0( int a, int b ); |
现在运行 javah 为 hypotenuse0 创建一个新原型,生成的原型如下所示:
/* * Class: MyNative * Method: hypotenuse0 * Signature: (II)I */ JNIEXPORT jint JNICALL Java_MyNative_hypotenuse0 (JNIEnv *, jclass, jint, jint); |
C 源代码中的方法签名变了,但代码还保持原样:
JNIEXPORT jint JNICALL Java_MyNative_hypotenuse0 (JNIEnv *env, jclass cls, jint a, jint b) { int rtn = (int)sqrt( (double)( (a*a) + (b*b) ) ); return (jint)rtn; } |
本质上,jobject 参数 已变为 jclass 参数 。此参数 是指向 MyNative.class 的一个句柄。main 方法可更改为以下形式:
public static void main( String[] args ) { System.out.println( MyNative.hypotenuse( 3, 4 ) ); System.out.println( MyNative.hypotenuse( 9, 12 ) ); } |
因为方法是静态的,所以调用它不需要实例化 MyNative 对象。本文后面的示例将使用静态方法。
示例 4 -- 传递数组
本例说明如何传递数组 型参数 。本例使用一个基本类型,boolean,并将更改数组 元素。下一个示例将访问 String(非基本类型)数组 。将下面的方法添加到 MyNative.java 源代码中:
public static void setArray( boolean[] ba ) { for( int i=0; i < ba.length; i++ ) ba[i] = true; setArray0( ba ); } ... private static native void setArray0( boolean[] ba ); |
在本例中,布尔型数组 被初始化为 true,本地方法将把特定的元素设置为 false。同时,在 Java 源代码中,我们可以更改 main 以使其包含测试代码:
boolean[] ba = new boolean[5]; MyNative.setArray( ba ); for( int i=0; i < ba.length; i++ ) System.out.println( ba[i] ); |
在编译源代码并执行 javah 以后,MyNative.h 头文件包含以下的原型:
/* * Class: MyNative * Method: setArray0 * Signature: ([Z)V */ JNIEXPORT void JNICALL Java_MyNative_setArray0 (JNIEnv *, jclass, jbooleanArray); |
请注意,布尔型数组 是作为单个名为 jbooleanArray 的类型创建的。
基本类型有它们自已的数组 类型,如 jintArray 和 jcharArray。
非基本类型的数组 使用 jobjectArray 类型。下一个示例中包括一个 jobjectArray。这个布尔数组 的数组 元素是通过 JNI 方法 GetBooleanArrayElements 来访问的。
针对每种基本类型都有等价的方法。这个本地方法是如下实现的:
JNIEXPORT void JNICALL Java_MyNative_setArray0 (JNIEnv *env, jclass cls, jbooleanArray ba) { jboolean* pba = (*env)->GetBooleanArrayElements( env, ba, 0 ); jsize len = (*env)->GetArrayLength(env, ba); int i=0; // 更改偶数数组 元素 for( i=0; i < len; i+=2 ) pba[i] = JNI _FALSE; (*env)->ReleaseBooleanArrayElements( env, ba, pba, 0 ); } |
指向布尔型数组 的指针可以使用 GetBooleanArrayElements 获得。
数组 大小可以用 GetArrayLength 方法获得。使用 ReleaseBooleanArrayElements 方法释放数组 。现在就可以读取和修改数组 元素的值了。jsize 声明等价于 jint(要查看它的定义,请参阅 JDK 的 include 目录下的 jni .h 头文件)。
示例 5 -- 传递 Java String 数组
本例将通过最常用的非基本类型,Java String,说明如何访问非基本对象的数组 。字符串数组 被传递给本地方法,而本地方法只是将它们显示到控制台上。
MyNative 类定义中添加了以下几个方法:
public static void showStrings( String[] sa ) { showStrings0( sa ); } private static void showStrings0( String[] sa ); |
并在 main 方法中添加了两行进行测试:
String[] sa = new String[] { "Hello,", "world!", "JNI ", "is", "fun." }; MyNative.showStrings( sa ); |
本地方法分别访问每个元素,其实现如下所示。
JNIEXPORT void JNICALL Java_MyNative_showStrings0 (JNIEnv *env, jclass cls, jobjectArray sa) { int len = (*env)->GetArrayLength( env, sa ); int i=0; for( i=0; i < len; i++ ) { jobject obj = (*env)->GetObjectArrayElement(env, sa, i); jstring str = (jstring)obj; const char* szStr = (*env)->GetStringUTFChars( env, str, 0 ); printf( "%s ", szStr ); (*env)->ReleaseStringUTFChars( env, str, szStr ); } printf( "\n" ); } |
数组 元素可以通过 GetObjectArrayElement 访问。
在 本例中,我们知道返回值是 jstring 类型,所以可以安全地将它从 jobject 类型转换为 jstring 类型。字符串是通过前面讨论过的方法打印的。有关在 Windows 中处理 Java 字符串的信息,请参阅标题为 NLS Strings and JNI 的一篇论文。
示例 6 -- 返回 Java String 数组
最后一个示例说明如何在本地代码中创建一个字符串数组 并将它返回给 Java 调用者。MyNative.java 中添加了以下几个方法:
public static String[] getStrings() { return getStrings0(); } private static native String[] getStrings0(); |
更改 main 以使 showStrings 将 getStrings 的输出显示出来:
MyNative.showStrings( MyNative.getStrings() ); |
实现的本地方法返回五个字符串。
JNIEXPORT jobjectArray JNICALL Java_MyNative_getStrings0 (JNIEnv *env, jclass cls) { jstring str; jobjectArray args = 0; jsize len = 5; char* sa[] = { "Hello,", "world!", "JNI ", "is", "fun" }; int i=0; args = (*env)->NewObjectArray(env, len, (*env)->FindClass(env, "java/lang/String"), 0); for( i=0; i < len; i++ ) { str = (*env)->NewStringUTF( env, sa[i] ); (*env)->SetObjectArrayElement(env, args, i, str); } return args; } |
字符串数组 是通过调用 NewObjectArray 创建的,同时传递了 String 类和数组 长度两个参数 。Java String 是使用 NewStringUTF 创建的。String 元素是使用 SetObjectArrayElement 存入数组 中的。
2.调试
现 在您已经为您的应用程序创建了一个本地 DLL,但在调试时还要牢记以下几点。如果使用 Java 调试器 java_g.exe,则还需要创建 DLL 的一个“调试”版本。这只是表示必须创建同名但带有一个 _g 后缀的 DLL 版本。就 MyNative.dll 而言,使用 java_g.exe 要求在 Windows 的 PATH 环境指定的路径中有一个 MyNative_g.dll 文件。在大多数情况下,这个 DLL 可以通过将原文件重命名或复制为其名称带缀 _g 的文件。
现在,Java 调试器不允许您进入本地代码,但您可以在 Java 环境外使用 C 调试器(如 Microsoft Visual C++)调试本地方法。首先将源文件导入一个项目中。
将编译设置调整为在编译时将 include 目录包括在内:
c:\jdk1.1.6\include;c:\jdk1.1.6\include\win32 |
将配置设置为以调试模式编译 DLL。在 Project Settings 中的 Debug 下,将可执行文件设置为 java.exe(或者 java_g.exe,但要确保您生成了一个 _g.dll 文件)。程序参数 包括包含 main 的类名。如果在 DLL 中设置了断点,则当调用本地方法时,执行将在适当的地方停止。
下面是设置一个 Visual C++ 6.0 项目来调试本地方法的步骤。
- 在 Visual C++ 中创建一个 Win32 DLL 项目,并将 .c 和 .h 文件添加到这个项目中。
- 在 Tools 下拉式菜单的 Options 设置下设置 JDK 的 include 目录。下面的对话框显示了这些目录。
- 选择 Build 下拉式菜单下的 Build MyNative.dll 来建立这个项目。确保将项目的活动配置设置为调试(这通常是缺省值)。
- 在 Project Settings 下,设置 Debug 选项卡来调用适当的 Java 解释器,如下所示:
当执行这个程序时,忽略“在 java.exe 中找不到任何调试信息”的消息。当调用本地方法时,在 C 代码中设置的任何断点将在适当的地方停止 Java 程序的执行。
3.其他信息
JNI 方法和 C++
上面这些示例说明了如何在 C 源文件中使用 JNI 方法。如果使用 C++,则请将相应方法的格式从:
(*env)->JNIMethod( env, .... ); |
更改为:
env->JNIMethod( ... ); |
在 C++ 中,JNI 函数被看作是 JNIEnv 类的成员方法。
字符串和国家语言支持
本文中使用的技术用 UTF 方法来转换字符串。使用这些方法只是为了方便起见,如果应用程序需要国家语言支持 (NLS),则不能使用这些方法。有关在 Windows 和 NLS 环境中处理 Java 字符串正确方法,请参标题为 NLS Strings and JNI 的一篇论文。
4.小结
本文提供的示例用最常用的数据类据(如 jint 和 jstring)说明了如何实现本地方法,并讨论了 Windows 特定的几个问题,如显示字符串。本文提供的示例并未包括全部 JNI ,JNI 还包括其他参数 类型,如 jfloat、jdouble、jshort、jbyte 和 jfieldID,以及用来处理这些类型的方法。有关这个主题的详细信息,请参阅 Sun Microsystems 提供的 Java 本地接口规范。
5.关于作者: David Wendt 是 IBM WebSphere Studio 的一名程序员,该工作室位于北卡罗莱纳州的 Research Triangle Park。可以通过 wendt@us.ibm.com 与他联系。
Chap11:JNI编程系列之基础篇
编程技术 2008-12-06 19:50 阅读2 评论0
字号: 大 中 小
JNI编程系列之基础篇
最近干一个活需要从Java调用C++编译的动态链接库,研究了一下JNI,现在将网上搜罗的文档和自己的体会贡献出来。
JNI 的做法是:通过在方法前加上关键字native来识别本地方法,然后用本地语言(如C,C++)来实现该方法,并编译成动态链接库,在Java的类中调用 该动态链接库,然后就可以像使用Java自己的方法一样使用native方法了。这样做的好处是既具有了Java语言的便利性,又具有了C语言的效率;另 一个好处是可以利用已有的C代码,避免重复开发。
下面从最简单的JNI程序入手,介绍如何进行JNI编程。
下面是一个简单的Java程序HelloWorld.java,
class HelloWorld {
private native void print();
public static void main(String[] args) {
new HelloWorld().print();
}
static {
System.loadLibrary("HelloWorld");
}
}
在这个例子中,注意到两个关键的地方。
首先是第二行
private native void print();
如果没有native关键字,这一行代码就是普通Java方法的声明。关键字native表明这是一个用本地语言实现的方法。
第二个地方是
System.loadLibrary("HelloWorld");
这行代码的作用是调用名为HelloWorld的动态链接库,在Windows下,是HelloWorld.dll,在Linux下是HelloWorld.so。
显然现在这个Java程序是不能运行的。要运行它先要做下面的工作。执行
> javac HelloWorld.java
> javah -jni HelloWorld
执行完这两条语句之后,会生成一个名为HelloWorld.h的文件,它的内容应该是这样的,
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloWorld */
#ifndef _Included_HelloWorld
#define _Included_HelloWorld
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: HelloWorld
* Method: print
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_HelloWorld_print (JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
注意到在这个程序的开头有这样一行代码,
#include <jni.h>
这里的jni.h,只要你安装了JDK就能在安装目录下找到它。
不要修改这个文件的内容,现在要做的是写一个名为HelloWorld.cpp程序,实现上面这个.h文件里的函数,
//------------
#include "HelloWorld.h"
#include <iostream>
JNIEXPORT void JNICALL Java_HelloWorld_print (JNIEnv *, jobject) {
std::cout << "Hello World!" << std::endl;
}
//--------------
这是一个最简单的C++程序。将它编译为动态链接库,我们得到HelloWorld.dll,将这个.dll文件拷到HelloWorld.java文件所在的目录下。执行
> java HelloWorld
你会看到屏幕上输出
> Hello World!
现在来总结一下,要实现JNI编程,需要以下几个步骤:
1. 写一个Java程序,将你希望用C语言实现的方法用native关键字标识出来,同时加上调用动态链接库的语句。
System.loadLibrary("HelloWorld");
2. 执行下面两条语句,生成.h文件
> javac HelloWorld.java
在class或bin目录下(其下或其子目录下有 javac命令生成的*.class文件)执行
> javah -jni HelloWorld
3. 根据.h文件,写一个.cpp程序,编译成动态链接库,并将其复制到.java文件所在的路径下。
4. 执行java HelloWorld
这 样,就学会了最简单的JNI编程,网上能google到的大部分文章也就到此为止了。但是你一定还有很多疑问,就像我刚开始一样,最容易想到的就是,如果 本地方法要传递参数或者返回值怎么办?本地方法的定义在.java文件中,参数或者返回值的类型都是Java的类型。而它的实现是通过C程序完成的,参数 和返回值的类型只能是C的类型。诸如此类的问题,上面这个简单的例子是回答不了的。在下一篇,我将解释这些问题。
Chap12:JNI编程系列之中级篇(上)
编程技术 2008-12-06 23:41 阅读9 评论0
字号: 大 中 小
本篇将介绍在JNI编程中如何传递参数和返回值。
首先要强调的是,native方法不但可以传递Java的基本类型做参数,还可以传递更复杂的类型,比如String,数组,甚至自定义的类。这一切都可以在jni.h中找到答案。
1. Java基本类型的传递
用 过Java的人都知道,Java中的基本类型包括boolean,byte,char,short,int,long,float,double这样几 种,如果你用这几种类型做native方法的参数,当你通过javah -jni生成.h文件的时候,只要看一下生成的.h文件,就会一清二楚,这些类型分别对应的类型是 jboolean,jbyte,jchar,jshort,jint,jlong,jfloat,jdouble 。这几种类型几乎都可以当成对应的C++类型来用,所以没什么好说的。
2. String参数的传递
Java的String和C++的string是不能对等起来的,所以处理起来比较麻烦。先看一个例子,
//*****************
class Prompt {
// native method that prints a prompt and reads a line
private native String getLine(String prompt);
public static void main(String args[]) {
Prompt p = new Prompt();
String input = p.getLine("Type a line: ");
System.out.println("User typed: " + input);
}
static {
System.loadLibrary("Prompt");
}
}
//*****************
在这个例子中,我们要实现一个native方法
String getLine(String prompt);
读入一个String参数,返回一个String值。
通过执行javah -jni得到的头文件是这样的
//*****************
#include <jni.h>
#ifndef _Included_Prompt
#define _Included_Prompt
#ifdef __cplusplus
extern "C" {
#endif
JNIEXPORT jstring JNICALL Java_Prompt_getLine(JNIEnv *env, jobject this, jstring prompt);
#ifdef __cplusplus
}
#endif
#endif
//*****************
jstring是JNI中对应于String的类型,但是和基本类型不同的是,jstring不能直接当作C++的string用。如果你用
cout << prompt << endl;
编译器肯定会扔给你一个错误信息的。
其实要处理jstring有很多种方式,这里只讲一种我认为最简单的方式,看下面这个例子,
//*****************
#include "Prompt.h"
#include <iostream>
JNIEXPORT jstring JNICALL Java_Prompt_getLine(JNIEnv *env, jobject obj, jstring prompt)
{
const char* str;
str = env->GetStringUTFChars(prompt, false);
if(str == NULL) {
return NULL; /* OutOfMemoryError already thrown */
}
std::cout << str << std::endl;
env->ReleaseStringUTFChars(prompt, str);
// return a string
char* tmpstr = "return string succeeded";
jstring rtstr = env->NewStringUTF(tmpstr);
return rtstr;
}
//*****************
在上面的例子中,作为参数的prompt不能直接被C++程序使用,先做了如下转换
str = env->GetStringUTFChars(prompt, false);
将jstring类型变成一个char*类型。
返回的时候,要生成一个jstring类型的对象,也必须通过如下命令,
jstring rtstr = env->NewStringUTF(tmpstr);
这里用到的GetStringUTFChars和NewStringUTF都是JNI提供的处理String类型的函数,还有其他的函数这里就不一一列举了。
/****************************************************/
JNI编程系列之中级篇(下)
编程技术 2008-12-06 23:44 阅读7 评论0
字号: 大 中 小
3. 数组类型的传递
和String一样,JNI为Java基本类型的数组提供了j*Array类型,比如int[]对应的就是jintArray。来看一个传递int数组的例子,Java程序就不写了,
//*******************
JNIEXPORT jint JNICALL Java_IntArray_sumArray(JNIEnv *env, jobject obj, jintArray arr)
{
jint *carr;
carr = env->GetIntArrayElements(arr, false);
if(carr == NULL) {
return 0; /* exception occurred */
}
jint sum = 0;
for(int i=0; i<10; i++) {
sum += carr[i];
}
env->ReleaseIntArrayElements(arr, carr, 0);
return sum;
}
//*******************
这 个例子中的GetIntArrayElements和ReleaseIntArrayElements函数就是JNI提供用于处理int数组的函数。如果 试图用arr[i]的方式去访问jintArray类型,毫无疑问会出错。JNI还提供了另一对函数GetIntArrayRegion和 ReleaseIntArrayRegion访问int数组,就不介绍了,对于其他基本类型的数组,方法类似。
4. 二维数组和String数组
在JNI中,二维数组和String数组都被视为object数组,因为数组和String被视为object。仍然用一个例子来说明,这次是一个二维int数组,作为返回值。
//*******************
JNIEXPORT jobjectArray JNICALL Java_ObjectArrayTest_initInt2DArray(JNIEnv *env, jclass cls, int size)
{
jobjectArray result;
jclass intArrCls = env->FindClass("[I");
result = env->NewObjectArray(size, intArrCls, NULL);
for (int i = 0; i < size; i++) {
jint tmp[256]; /* make sure it is large enough! */
jintArray iarr = env->NewIntArray(size);
for(int j = 0; j < size; j++) {
tmp[j] = i + j;
}
env->SetIntArrayRegion(iarr, 0, size, tmp);
env->SetObjectArrayElement(result, i, iarr);
env->DeleteLocalRef(iarr);
}
return result;
}
//*******************
上面代码中的第三行,
jobjectArray result;
因为要返回值,所以需要新建一个jobjectArray对象。
jclass intArrCls = env->FindClass("[I");
是 创建一个jclass的引用,因为result的元素是一维int数组的引用,所以intArrCls必须是一维int数组的引用,这一点是如何保证的 呢?注意FindClass的参数"[I",JNI就是通过它来确定引用的类型的,I表示是int类型,[标识是数组。对于其他的类型,都有相应的表示方 法,
Z boolean
B byte
C char
S short
I int
J long
F float
D double
String是通过“Ljava/lang/String;”表示的,那相应的,String数组就应该是“[Ljava/lang/String;”。
还是回到代码,
result = env->NewObjectArray(size, intArrCls, NULL);
的作用是为result分配空间。
jintArray iarr = env->NewIntArray(size);
是为一维int数组iarr分配空间。
env->SetIntArrayRegion(iarr, 0, size, tmp);
是为iarr赋值。
env->SetObjectArrayElement(result, i, iarr);
是为result的第i个元素赋值。
通过上面这些步骤,我们就创建了一个二维int数组,并赋值完毕,这样就可以做为参数返回了。
如果了解了上面介绍的这些内容,基本上大部分的任务都可以对付了。虽然在操作数组类型,尤其是二维数组和String数组的时候,比起在单独的语言中编程要麻烦,但既然我们享受了跨语言编程的好处,必然要付出一定的代价。
有 一点要补充的是,本文所用到的函数调用方式都是针对C++的,如果要在C中使用,所有的env->都要被替换成(*env)->,而且后面的 函数中需要增加一个参数env,具体请看一下jni.h的代码。另外还有些省略的内容,可以参考JNI的文档:Java Native Interface 6.0 Specification,在JDK的文档里就可以找到。如果要进行更深入的JNI编程,需要仔细阅读这个文档。接下来的高级篇,也会讨论更深入的话 题。
Chap13:JNI编程系列之高级篇
编程技术 2008-12-10 17:08 阅读6 评论0
字号: 大 中 小
在本篇中,将会涉及关于JNI编程更深入的话题,包括:在native方法中访问Java类的域和方法,将Java中自定义的类作为参数和返回值传递等等。了解这些内容,将会对JNI编程有更深入的理解,写出的程序也更清晰,易用性更好。
1. 在一般的Java类中定义native方法
在前两篇的例子中,都是将native方法放在main方法的Java类中,实际上,完全可以在任何类中定义native方法。这样,对于外部来说,这个类和其他的Java类没有任何区别。
2. 访问Java类的域和方法
native方法虽然是native的,但毕竟是方法,那么就应该同其他方法一样,能够访问类的私有域和方法。实际上,JNI的确可以做到这一点,我们通过几个例子来说明,
public class ClassA {
String str_ = "abcde";
int number_;
public native void nativeMethod();
private void javaMethod() {
System.out.println("call java method succeeded");
}
static {
System.loadLibrary("ClassA");
}
}
在这个例子中,我们在一个没有main方法的Java类中定义了native方法。我们将演示如何在nativeMethod()中访问域str_,number_和方法javaMethod(),nativeMethod()的C++实现如下,
JNIEXPORT void JNICALL Java_testclass_ClassCallDLL_nativeMethod(JNIEnv *env, jobject obj)
{
// access field
jclass cls = env->GetObjectClass(obj);
jfieldID fid = env->GetFieldID(cls, "str_", "Ljava/lang/String;");
jstring jstr = (jstring)env->GetObjectField(obj, fid);
const char *str = env->GetStringUTFChars(jstr, false);
if(std::string(str) == "abcde")
std::cout << "access field succeeded" << std::endl;
jint i = 2468;
fid = env->GetFieldID(cls, "number_", "I");
env->SetIntField(obj, fid, i);
// access method
jmethodID mid = env->GetMethodID(cls, "javaMethod", "()V");
env->CallVoidMethod(obj, mid);
}
上面的代码中,通过如下两行代码获得str_的值,
jfieldID fid = env->GetFieldID(cls, "str_", "Ljava/lang/String;");
jstring jstr = (jstring)env->GetObjectField(obj, fid);
第一行代码获得str_的id,在GetFieldID函数的调用中需要指定str_的类型,第二行代码通过str_的id获得它的值,当然我们读到的是一个jstring类型,不能直接显示,需要转化为char*类型。
接下来我们看如何给Java类的域赋值,看下面两行代码,
fid = env->GetFieldID(cls, "number_", "I");
env->SetIntField(obj, fid, i);
第一行代码同前面一样,获得number_的id,第二行我们通过SetIntField函数将i的值赋给number_,其他类似的函数可以参考JDK的文档。
访问javaMethod()的过程同访问域类似,
jmethodID mid = env->GetMethodID(cls, "javaMethod", "()V");
env->CallVoidMethod(obj, mid);
需要强调的是,在GetMethodID中,我们需要指定javaMethod方法的类型,域的类型很容易理解,方法的类型如何定义呢,在上面的例子中,我们用的是()V,V表示返回值为空,()表示参数为空。如果是更复杂的函数类型如何表示?看一个例子,
long f (int n, String s, int[] arr);
这个函数的类型符号是(ILjava/lang/String;[I)J,I表示int类型,Ljava/lang/String;表示String类型,[I表示int数组,J表示long。这些都可以在文档中查到。
3. 在native方法中使用用户定义的类
JNI 不仅能使用Java的基础类型,还能使用用户定义的类,这样灵活性就大多了。大体上使用自定义的类和使用Java的基础类(比如String)没有太大的 区别,关键的一点是,如果要使用自定义类,首先要能访问类的构造函数,看下面这一段代码,我们在native方法中使用了自定义的Java类 ClassB,
jclass cls = env->FindClass("Ltestclass/ClassB;");
jmethodID id = env->GetMethodID(cls, "<init>", "(D)V");
jdouble dd = 0.033;
jvalue args[1];
args[0].d = dd;
jobject obj = env->NewObjectA(cls, id, args);
首 先要创建一个自定义类的引用,通过FindClass函数来完成,参数同前面介绍的创建String对象的引用类似,只不过类名称变成自定义类的名称。然 后通过GetMethodID函数获得这个类的构造函数,注意这里方法的名称是"<init>",它表示这是一个构造函数。
jobject obj = env->NewObjectA(cls, id, args);
生成了一个ClassB的对象,args是ClassB的构造函数的参数,它是一个jvalue*类型。
通 过以上介绍的三部分内容,native方法已经看起来完全像Java自己的方法了,至少主要功能上齐备了,只是实现上稍麻烦。而了解了这些,JNI编程的 水平也更上一层楼。下面要讨论的话题也是一个重要内容,至少如果没有它,我们的程序只能停留在演示阶段,不具有实用价值。
4. 异常处理
在C++和Java的编程中,异常处理都是一个重要的内容。但是在JNI中,麻烦就来了,native方法是通过C++实现的,如果在native方法中发生了异常,如何传导到Java呢?
JNI提供了实现这种功能的机制。我们可以通过下面这段代码抛出一个Java可以接收的异常,
jclass errCls;
env->ExceptionDescribe();
env->ExceptionClear();
errCls = env->FindClass("java/lang/IllegalArgumentException");
env->ThrowNew(errCls, "thrown from C++ code");
如果要抛出其他类型的异常,替换掉FindClass的参数即可。这样,在Java中就可以接收到native方法中抛出的异常。
至 此,JNI编程系列的内容就完全结束了,这些内容都是本人的原创,通过查阅文档和网上的各种文章总结出来的,相信除了JDK的文档外,没有比这更全面的讲 述JNI编程的文章了。当然,限于篇幅,有些地方不可能讲的很细。限于水平,也可能有一些错误。文中所用的代码,都亲自编译执行过。希望这些内容能为需要 的朋友提供帮助,毕竟,分享是一种美德。
Chap14:如何在C/C++中调用Java
作者:刘冬 发文时间:2003.02.17
Java 跨平台的特性使Java越来越受开发人员的欢迎,但也往往会听到不少的抱怨:用Java开发的图形用户窗口界面每次在启动的时候都会跳出 一个控制台窗口,这个控制台窗口让本来非常棒的界面失色不少。怎么能够让通过Java开发的GUI程序不弹出Java的控制台窗口呢?其实现在很多流行的 开发环境例如JBuilder、Eclipse都是使用纯Java开发的集成环境。这些集成环境启动的时候并不会打开一个命令窗口,因为它使用了 JNI(Java Native Interface)的技术。通过这种技术,开发人员不一定要用命令行来启动Java程序,可以 通过编写一个本地GUI 程序直接启动Java程序,这样就可避免另外打开一个命令窗口,让开发的Java程序更加专业。
JNI允许运行在虚拟机的Java程序能够与其它语言(例如C和C++)编写的程序或者类库进行相互间的调用。同时JNI提供的一整套的API,允许将Java虚拟机直接嵌入到本地的应用程序中。图1是Sun站点上对JNI的基本结构的描述。
图1 JNI基本结构描述图
本 文将介绍如何在C/C++中调用Java方法,并结合可能涉及到的问题介绍整个开发的步骤及可能遇到的难题和解决方法。本文所采用的工具是 Sun公司创建的 Java Development Kit (JDK) 版本 1.3.1,以及微软公司的Visual C++ 6开发环境。
1.环境搭建
为了让本文以下部分的代码能够正常工作,我们必须建立一个完整的开发环境。首先需要下载并安装JDK 1.3.1,其下载地址为“http://java.sun.com ”。假设安装路径为C:\JDK。下一步就是设置集成开发环境,通过Visual C++ 6的菜单Tools→Options打开选项对话框如图2。
图2 设置集成开发环境图
将目录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 程序,代码如下:
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("<init>:" + 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.");
}
}
2.初始化虚拟机
本地代码在调用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初始化虚拟机:
#include <jni.h>
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);
}
JDK 1.2初始化虚拟机:
/* invoke2.c */
#include <jni.h>
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");
}
为了保证JNI代码的可移植性,建议使用JDK 1.2的方法来创建虚拟机。JNI_CreateJavaVM函数的第二个参数JNIEnv *env,就是贯穿整个JNI始末的一个参数,因为几乎所有的函数都要求一个参数就是JNIEnv *env。
3.访问类方法
初始化了Java虚拟机后,就可以开始调用Java的方法。要调用一个Java对象的方法必须经过几个步骤:
3.1.获取指定对象的类定义(jclass)
有两种途径来获取对象的类定义:
第一种是在已知类名的情况下使用FindClass来查找对应的类。但是要注意类名并不同于平时写的Java代码,例如要得到类jni.test.Demo的定义必须调用如下代码:
jclass cls = (*env)->FindClass(env, "jni/test/Demo"); //把点号换成斜杠
然后通过对象直接得到其所对应的类定义:
jclass cls = (*env)-> GetObjectClass(env, obj); //其中obj是要引用的对象,类型是jobject
3.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 得到运行结果如下:
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 */
}
我们看到类中每个属性和方法下面都有一段注释。注释中不包含空格的内容就是第四个参数要填的内容(关于javap具体参数请查询JDK的使用帮助)。下面这段代码演示如何访问jni.test.Demo的getMessage方法:
/*
假设我们已经有一个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);
*/
3.3.调用方法
为 了调用对象的某个方法,可以使用函数Call<TYPE>Method或者 CallStatic<TYPE>Method(访问类的静态方法),<TYPE>根据不同的返回类型而定。这些方法都是使用可 变参数的定义,如果访问某个方法需要参数时,只需要把所有参数按照顺序填写到方法中就可以。在讲到构造函数的访问时,将演示如何访问带参数的构造函数。
4访问类属性
访问类的属性与访问类的方法大体上是一致的,只不过是把方法变成属性而已。
4.1.获取指定对象的类(jclass)
这一步与访问类方法的第一步完全相同,具体使用参看访问类方法的第一步。
4.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; */
其中第二行注释的内容就是第四个参数要填的信息,这跟访问类方法时是相同的。
4.3.读取和设置属性值
有 了属性的定义要访问属性值就很容易了。有几个方法用来读取和设置类的属性,它们是:Get<TYPE>Field、 Set<TYPE>Field、GetStatic<TYPE>Field、 SetStatic<TYPE>Field。比如读取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);
5.访问构造函数
很 多人刚刚接触JNI的时候往往会在这一节遇到问题,查遍了整个jni.h看到这样一个函数NewObject,它应该是可以用来访问类的构造函 数。但是该函数需要提供构造函数的方法定义,其类型是jmethodID。从前面的内容我们知道要获取方法的定义首先要知道方法的名称,但是构造函数的名 称怎么来填写呢?其实访问构造函数与访问一个普通的类方法大体上是一样的,惟一不同的只是方法名称不同及方法调用时不同而已。访问类的构造函数时方法名必 须填写“<init>”。下面的代码演示如何构造一个Demo类的实例:
jclass cls = (*env)->FindClass(env, "jni/test/Demo");
/**
首先通过类的名称获取类的定义,相当于Java中的Class.forName方法
*/
if (cls == 0)
<error handler>
jmethodID mid = (*env)->GetMethodID(env,cls,"<init>","(Ljava/lang/String;)V ");
if(mid == 0)
<error handler>
jobject demo = (*env)->NewObject( env ,cls,mid,0 );
/**
访问构造函数必须使用NewObject的函数来调用前面获取的构造函数的定义
上面的代码我们构造了一个Demo的实例并传一个空串null
*/
6.数组处理
6.1创建一个新数组
要 创建一个数组,我们首先应该知道数组元素的类型及数组长度。JNI定义了一批数组的类型j<TYPE>Array及数组操作的函数 New<TYPE>Array,其中<TYPE>就是数组中元素的类型。例如,要创建一个大小为10并且每个位置值分别为 1-10的整数数组,编写代码如下:
int i = 1;
jintArray array; //定义数组对象
(*env)-> NewIntArray(env, 10);
for(; i<= 10; i++)
(*env)->SetIntArrayRegion(env, array, i-1, 1, &i);
6.2访问数组中的数据
访问数组首先应该知道数组的长度及元素的类型。现在我们把创建的数组中的每个元素值打印出来,代码如下:
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]);
7.中文处理
中 文字符的处理往往是让人比较头疼的事情,特别是使用Java语言开发的软件,在JNI这个问题更加突出。由于Java中所有的字符都 是 Unicode编码,但是在本地方法中,例如用VC编写的程序,如果没有特殊的定义一般都没有使用Unicode的编码方式。为了让本地方法能够访 问 Java中定义的中文字符及Java访问本地方法产生的中文字符串,我定义了两个方法用来做相互转换。
· 方法一,将Java中文字符串转为本地字符串
/**
第一个参数是虚拟机的环境指针
第二个参数为待转换的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)
desc[len]=0;
delete[] w_buffer;
return strlen(desc);
}
· 方法二,将C的字符串转为Java能识别的Unicode字符串
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;
}
8.异常
由 于调用了Java的方法,因此难免产生操作的异常信息。这些异常没有办法通过C++本身的异常处理机制来捕捉到,但JNI可以通过一些函数来获 取Java中抛出的异常信息。之前我们在Demo类中定义了一个方法throwExcp,下面将访问该方法并捕捉其抛出来的异常信息,代码如下:
/**
假设我们已经构造了一个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来获取异常信息的内容。访问这两个方法同前面讲到的如何访问类的方法是相同的。
*/
}
9.线程和同步访问
有 些时候需要使用多线程的方式来访问Java的方法。我们知道一个Java虚拟机是非常消耗系统的内存资源,差不多每个虚拟机需要内存大约在 20MB左右。为了节省资源要求每个线程使用的是同一个虚拟机,这样在整个的JNI程序中只需要初始化一个虚拟机就可以了。所有人都是这样想的,但是一旦 子线程访问主线程创建的虚拟机环境变量,系统就会出现错误对话框,然后整个程序终止。
其实这里面涉及到两个概念,它们分别是虚拟机 (JavaVM *jvm)和虚拟机环境(JNIEnv *env)。真正消耗大量系统资源的是 jvm而不是env,jvm是允许多个线程访问的,但是env只能被创建它本身的线程所访问,而且每个线程必须创建自己的虚拟机环境env。这时候会有人 提出疑问,主线程在初始化虚拟机的时候就创建了虚拟机环境env。为了让子线程能够创建自己的env,JNI提供了两个函数:AttachCurrentThread 和DetachCurrentThread。下面代码就是子线程访问Java方法的框架:
DWORD WINAPI ThreadProc(PVOID dwParam)
{
JavaVM jvm = (JavaVM*)dwParam; /* 将虚拟机通过参数传入 */
JNIEnv* env;
(*jvm)-> AttachCurrentThread (jvm, &env, NULL);
.........
(*jvm)-> DetachCurrentThread(jvm);
}
10.时间
关 于时间的话题是我在实际开发中遇到的一个问题。当要发布使用了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的文件都删除后重新打包。
Chap15:基本JNI调用技术(c/c++与java互调)
一.C/C++调用Java
在C/C++中调用Java的方法一般分为五个步骤:初始化虚拟机、获取类、创建类对象、调用方法和退出虚拟机。
1. 初始化虚拟机
代码如下:
JNIEnv *env;
JavaVM *jvm;
JavaVMInitArgs vm_args;
JavaVMOption options[3];
int res;
//设置参数
options[0].optionString = "-Djava.compiler=NONE";
//classpath有多个时,UNIX下以“:”分割。
options[1].optionString = "-Djava.class.path=.";
options[2].optionString = "-verbose:jni";
vm_args.version = JNI_VERSION_1_4;
vm_args.nOptions = 3;
vm_args.options = options;
vm_args.ignoreUnrecognized = JNI_TRUE;
res = JNI_CreateJavaVM (&jvm, (void**)&env, &vm_args);
if (res >= 0)
{
//创建虚拟机成功
}
一个应用程序只需要一个虚拟机,但是每个线程需要自己的虚拟机运行环境。我们从一个虚拟机获取多个当前线程的运行环境,代码如下:
int result=0;
result=jvm->AttachCurrentThread ( reinterpret_cast<void**>( &env ), 0 );
if(result>=0)
{
//获取运行环境成功
}
当线程退出时,需要释放本线程使用的运行环境。
jvm->DetachCurrentThread();
2 获取类
在进行方法调用之前,需要先获取相应的类,类名称必须包括包名,其中的“.”用“/”代替。
jclass JavaClass;
JavaClass = env->FindClass("com/test/TestInterface");
if(JavaClass != 0)
{
//获取成功
}
3 创建类对象
如果需要调用的方法静态方法,则可以跳过本步骤。反之,则需要构造该对象。构造对象是通过调用类的构造函数来实现的,构咱函数的方法声明为<init>, GetMethodID方法的参数在下一步骤详细说明。
jobject obj;
jmethodID ctor;
ctor = env->GetMethodID(JavaClass,"<init>","()V");
if(ctor != 0)//获取方法成功
{
obj = env->NewObject(JavaClass, ctor);
}
4 调用方法
调用一个方法需要两个步骤:获取方法句柄和调用方法。
jmethodID methodID = env->GetMethodID( JavaClass, "setTest","(I)V");
if(methodID!=0)//获取方法成功
{
env->CallVoidMethod( obj, methodID,12);
}
GetStaticMethodID是用来获取静态方法的定义,GetMethodID则是获取非静态的方法定义。他们传入参数的参数依次为:类定义、方法名称和方法的定义,方法的定义可以用jdk中带的javap工具反编译class文件获取,其格式如下:
public void setTest(int inTest);
Signature: (I)V
Signature后面的内容就是方法的定义。
CallVoidMethod 是对获取的方法进行调用,JNI接口中提供了一系列的同 类方法,包括静态方法的调用函数(如:CallStaticXXXMethod)和非静态的方法(如:CallXXXMethod),其中XXX表示的不 同方法返回类型,包括int、object等等。
5 退出虚拟机
退出虚拟机调用方法如下:
jvm->DestroyJavaVM();
在JNI接口定义中,只有最后一个线程退出时,该方法才会返回,但是我只用一个线程,调用该方法也无法返回。故此建议系统退出时执行该方法,或者整个程序退出时,让虚拟机自己释放。
[注意]:
l 在处理中文字符串时,需要注意Java的char是双字节的,采用Unicode编码,在和C++中的char转换时,需要用到系统API:WideCharToMultiByte和MultiByteToWideChar。
l 注意对运行环境中对象引用时的释放,以免引起内存泄漏。
jstring str;
wchar_t *w_buffer =(wchar_t *)env->GetStringChars(str,0);
env->ReleaseStringChars(str,(const unsigned short *)w_buffer);
6 处理异常
C/C++中调用Java时,一定要捕获并处理Java方法抛出的异常信息,否则可能导致C/C++进程的核心转储(Core Dump)。
异常应在每个方法调用后检查:
msg = (jstring)env->CallObjectMethod(obj, mid);
if (env->ExceptionOccurred())
{
env->ExceptionDescribe();
env->ExceptionClear();
return 0;
}
二.Java调用C/C++
Java调用C/C++时,遵循几个步骤:
1、 用Java native 关键字声明方法为本地方法(非Java语言实现)。
2、 编译该声明类,得到XXX.class文件。
3、 用“javah –jni XXX”命令从该class文件生成C语言头文件(XXX.h)。
4、 采用C语言实现该头文件声明的方法,将实现类编译成库文件(libXXX.so)。
5、 在Java程序中使用System.loadLibrary(XXX)加载该库文件(需要设置-Djava.library.path环境变量指向该库文件存放路径)。
6、 即可象调用Java方法一样,调用native方式声明的本地方法。
Chap16:JNI的c代码中,另外一个线程获取 JNIEnv
2009-06-19 14:36
JNI 中,JNIEnv * 指针变量只对当前线程有效。如果是其他的线程,需要先获得 JVM* 指针,然后再获得当前线程的 JNIEnv * 指针。部分示例代码为:
/** Invoker.cpp, Invoker.java */
#include <jni.h>
#include <stdio.h>
#include "Invoker.h"
#include "invoker_include.h"
JavaVM * jvm;
JNIEnv * static_env;
jobject * jObject; // 线程间公用,必须使用 global reference
jclass c; // 必须使用 global reference
jmethodID m; // 必须使用 global reference
/*****************************
* Class: Invoker
* Method: register
* Signature: ()V
*****************************/
JNIEXPORT void JNICALL Java_Invoker_register (JNIEnv *env, jobject arg)
{
jObject = arg;
// printf("object: %x, %x. \n", &arg, &jObject);
printf("[main] Invoker registered. \n");
jclass bgpClass = (*env)->GetObjectClass(env, arg);
jmethodID methodId = (*env)->GetMethodID(env, bgpClass, "invoke", "()V");
printf("[main] -class: %d, method: %d \n", bgpClass, methodId);
(*env)->CallVoidMethod(env, arg, methodId);
// Global reference
(*env)->GetJavaVM (env, &jvm);
jObject = (*env)->NewGlobalRef(env, arg);
c = (*env)->NewGlobalRef(env, bgpClass);
m = (*env)->NewGlobalRef(env, methodId);
start(invoke_java_method);
(*env)->DeleteGlobalRef(env, c); // 手动销毁 global reference
(*env)->DeleteGlobalRef(env, m); // 手动销毁 global reference
(*env)->DeleteGlobalRef(env, jObject);
(*jvm)->DetachCurrentThread(jvm); // 销毁线程
(*jvm)->DestroyJavaVM (jvm); // ?销毁虚拟机
}
// Test method
JNIEXPORT void JNICALL Java_Invoker_println (JNIEnv *env, jobject obj, jstring string)
{
const char *str = (*env)->GetStringUTFChars(env, string, 0);
printf("[main] %s\n",str);
(*env)->ReleaseStringUTFChars(env, string, str);
}
// Callback method 回调函数
int invoke_java_method ()
{
(*jvm)->AttachCurrentThread(jvm, (void**)&static_env, 0); // 获得当前线程可以使用的 JNIEnv * 指针
(*static_env)->CallVoidMethod(static_env, jObject, m); // 调用 Java 方法
printf("[callback] java method invoked, invoker class: %x ... \n", &jObject);
}
chap 17:当JNI遇到多线程--java对象如何被C++中的多个线程访问?
java中要访问C++代码时, 使用JNI是唯一选择. 然而,在多线程的情况下, 可能出现以下问题:
问题描述:
一个java对象通过JNI调用DLL中一个send()函数向服务器发送消息,不等服务器消息到来就立即返回.同时把JNI接口的指针JNIEnv *env,和jobject obj保存在DLL中的变量里.
一段时间后,DLL中的消息接收线程接收到服务器发来的消息,并试图通过保存过的env和obj来调用先前的java对象的方法来处理此消息.
然而,JNI文档上说,JNI接口的指针JNIEnv*不能在c++的线程间共享,在我的程序中,如果接收线程试图调用java对象的方法,程序会突然退出.
不知道有没有方法突破JNI接口的指针不能在多个c++线程中共享的限制?
解决办法:
在 http://java.sun.com/docs/books/jni/html/pitfalls.html#29161 提到,
JNI接口指针不可为多个线程共用,但是java虚拟机的JavaVM指针是整个jvm公用的. 于是,在DLL中可以调用:
static JavaVM* gs_jvm;
env->GetJavaVM (&gs_jvm); 来获取JavaVM指针.获取了这个指针后,在DLL中的另一个线程里,可以调用:
JNIEnv *env;
gs_jvm->AttachCurrentThread((void **)&env, NULL);
来将DLL中的线程 "attached to the virtual machine"(不知如何翻译...),同时获得了这个线程在jvm中的 JNIEnv指针.
由 于我需要做的是在DLL中的一个线程里改变某个java对象的值,所以,还必须获取那个java对象的jobject指针.同 JNIEnv 指针一样,jobject指针也不能在多个线程中共享. 就是说,不能直接在保存一个线程中的jobject指针到全局变量中,然后在另外一个线程中使用它.幸运的是,可以用
gs_object=env->NewGlobalRef(obj);
来将传入的obj保存到gs_object中,从而其他线程可以使用这个gs_object来操纵那个java对象了.
示例代码如下:
(1)java代码:
//file name: Test.java
import java.io.*;
class Test implements Runnable
{
public int value = 0;
private Thread tx=null;
public Test()
{
tx=new Thread(this,"tx");
}
static
{
System.loadLibrary("Test");
}
public native void setEnev();
public static void main(String args[])
{
Test t = new Test();
t.setEnev();
System.out.println("ok in java main");
t.tx.start ();
try
{
Thread.sleep(10000000);
}catch(Exception e)
{
System.out.println("error in main");
}
}
public void run()
{
try
{
while(true)
{
Thread.sleep(1000);
System.out.println(value);
}
}catch(Exception e)
{
System.out.println("error in run");
}
}
}
(2) DLL代码:
//cpp file name: Test.cpp:
#include "test.h"
#include<windows.h>
#include<stdio.h>
static JavaVM *gs_jvm=NULL;
static jobject gs_object=NULL;
static int gs_i=10;
void WINAPI ThreadFun(PVOID argv)
{
JNIEnv *env;
gs_jvm->AttachCurrentThread((void **)&env, NULL);
jclass cls = env->GetObjectClass(gs_object);
jfieldID fieldPtr = env->GetFieldID(cls,"value","I");
while(1)
{
Sleep(100);
//在DLL中改变外面的java对象的value变量的值.
env->SetIntField(gs_object,fieldPtr,(jint)gs_i++);
}
}
JNIEXPORT void JNICALL Java_Test_setEnev(JNIEnv *env, jobject obj)
{
printf("come into test.dll\n");
//Returns “0” on success; returns a negative value on failure.
int retGvm=env->GetJavaVM(&gs_jvm);
//直接保存obj到DLL中的全局变量是不行的,应该调用以下函数:
gs_object=env->NewGlobalRef(obj);
HANDLE ht=CreateThread( NULL,0,
(LPTHREAD_START_ROUTINE)ThreadFun,0,
NULL,NULL);
printf("the Handle ht is:%d\n",ht);
}
chap 18:JNI在多线程中的应用
引文地址:http://blog.csdn.net/hust_liuX/archive/2006/12/25/1460486.aspx
我在这里将文章整理了一下,重新修改了部分描述和增加了一些重要的说明事项。修改文如下:
问题描述:
一个java对象通过JNI 调用DLL中一个send()函数向服务器发送消息,不等服务器消息到来就立即返回,同时把JNI 接口的指针JNIEnv *env(虚拟机环境指针),和jobject obj保存在DLL中的变量里.
一段时间后,DLL中的消息接收线程接收到服务器发来的消息,
并试图通过保存过的env和obj来调用先前的java对象的方法(相当于JAVA回调方法)来处理此消息.此时程序会突然退出(崩溃).
解决办法:
解决此问题首先要明白造成这个问题的原因。那么崩溃的原因是什么呢?
JNI 文档上有明确表述: The JNIEnv
pointer, passed as the first argument to every native method, can only be used in the thread with which it is associated. It is wrong to cache the JNIEnv
interface pointer obtained from one thread, and use that pointer in another thread.
意思就是JNIEnv指针不能直接在多线程 中共享使用。上面描述的程序崩溃的原因就在这里:回调时的线程和之前保存变量的线程共享了这个JNIEnv *env指针和jobject obj变量。
在 http://java.sun.com/docs/books/jni /html/other.html#26206 提到,JNIEnv *env指针不可为多个线程共用,但是java虚拟机的JavaVM指针是整个jvm公用的,我们可以通过JavaVM来得到当前线程的JNIEnv指针。
于是,在第一个线程A中调用:
JavaVM* gs_jvm;
env->GetJavaVM(&gs_jvm); //来获取JavaVM指针.获取了这个指针后,将该JavaVM保存起来。
在另一个线程B里,调用
JNIEnv *env;
gs_jvm->AttachCurrentThread((void **)&env, NULL);
//这里就获得了B这个线程在jvm中的JNIEnv指针.
这 里还必须获取那个java对象的jobject指针,因为我们要回调JAVA方法.同 JNIEnv 指针一样,jobject指针也不能在多个线程中共享. 就是说,不能直接在保存一个线程中的jobject指针到全局变量中,然后在另外一个线程中使用它.幸运的是,可以用
- gs_object=env->NewGlobalRef(obj);//创建一个全局变量
来将传入的obj(局部变量)保存到gs_object中,从而其他线程可以使用这个gs_object(全局变量)来操纵这个java对象了.
示例代码如下:
(1)java代码:Test.java:
- import java.io.*;
- class Test implements Runnable
- {
- public int value = 0;
- static{ System.loadLibrary("Test");}
- public native void setEnev();//本地方法
- public static void main(String args[]) throws Exception
- {
- Test t = new Test();
- t.setEnev(); //调用本地方法
- while(true)
- {
- Thread.sleep(1000);
- System.out.println(t.value);
- }
- }
- }
(2) DLL代码:Test.cpp:
1. #include "test.h"
2. #include<windows.h>
3. #include<stdio.h>
4. static JavaVM *gs_jvm=NULL;
5. static jobject gs_object=NULL;
6. static int gs_i=10;
7.
8. JNIEXPORT void JNICALL Java_Test_setEnev(JNIEnv *env, jobject obj)
9. {
10. env->GetJavaVM(&gs_jvm); //保存到全局变量中JVM
11. //直接赋值obj到DLL中的全局变量是不行的,应该调用以下函数:
12. gs_object=env->NewGlobalRef(obj);
13.
14. HANDLE ht=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)ThreadFun,0,NULL,NULL);
15. }
16.
17. void WINAPI ThreadFun(PVOID argv)//JNI中线程回调这个方法
18. {
19. JNIEnv *env;
20. gs_jvm->AttachCurrentThread((void **)&env, NULL);
21. jclass cls = env->GetObjectClass(gs_object);
22. jfieldID fieldPtr = env->GetFieldID(cls,"value","I");
23.
24. while(1)
25. {
26. Sleep(100);
27. //这里改变JAVA对象的属性值(回调JAVA)
28. env->SetIntField(gs_object,fieldPtr,(jint)gs_i++);
29. }
30. }
31.
32.
JNI
There are certain constraints that you must keep in mind when writing native methods that are to run in a multithreaded environment. By understanding and programming within these constraints, your native methods will execute safely no matter how many threads simultaneously execute a given native method. For example:
- A JNIEnv pointer is only valid in the thread associated with it. You must not pass this pointer from one thread to another, or cache and use it in multiple threads. The Java virtual machine passes a native method the same JNIEnv pointer in consecutive invocations from the same thread, but passes different JNIEnv pointers when invoking that native method from different threads. Avoid the common mistake of caching the JNIEnv pointer of one thread and using the pointer in another thread.
- Local references are valid only in the thread that created them. You must not pass local references from one thread to another. You should always convert local references to global references whenever there is a possibility that multiple threads may use the same reference.
chap 19:JNI限制(多线程)
JNI 限制:
There are certain constraints that you must keep in mind when writing native methods that are to run in a multithreaded environment. By understanding and programming within these constraints, your native methods will execute safely no matter how many threads simultaneously execute a given native method. For example:
A JNIEnv pointer is only valid in the thread associated with it. You must not pass this pointer from one thread to another, or cache and use it in multiple threads.
The Java virtual machine passes a native method the same JNIEnv pointer in consecutive invocations from the same thread, but passes different JNIEnv pointers when invoking that native method from different threads. Avoid the common mistake of caching the JNIEnv pointer of one thread and using the pointer in another thread.
Local references are valid only in the thread that created them. You must not pass local references from one thread to another. You should always convert local references to global references whenever there is a possibility that multiple threads may use the same reference.
本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/lovingprince/archive/2008/08/19/2793504.aspx
chap 20:使用 Java Native Interface 的最佳实践
JNI 的发展
JNI 自从 JDK 1.1 发行版以来一直是 Java 平台的一部分,并且在 JDK 1.2 发行版中得到了扩展。JDK 1.0 发行版包含一个早期的本机方法接口,但是未明确分隔本机代码和 Java 代码。在这个接口中,本机代码可以直接进入 JVM 结构,因此无法跨 JVM 实现、平台或者甚至各种 JDK 版本进行移植。使用 JDK 1.0 模型升级含有大量本机代码的应用程序,以及开发能支持多个 JVM 实现的本机代码的开销是极高的。
JDK 1.1 中引入的 JNI 支持:
- 版本独立性
- 平台独立性
- VM 独立性
- 开发第三方类库
有一个有趣的地方值得注意,一些较年轻的语言(如 PHP)在它们的本机代码支持方面仍然在努力克服这些问题。
、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、
2009 年 7 月 27 日
Java™ 本机接口(Java Native Interface,JNI)是一个标准的 Java API,它支持将 Java 代码与使用其他编程语言编写的代码相集成。如果您希望利用已有的代码资源,那么可以使用 JNI 作为您工具包中的关键组件 —— 比如在面向服务架构(SOA)和基于云的系统中。但是,如果在使用时未注意某些事项,则 JNI 会迅速导致应用程序性能低下且不稳定。本文将确定 10 大 JNI 编程缺陷,提供避免这些缺陷的最佳实践,并介绍可用于实现这些实践的工具。
Java 环境和语言对于应用程序开发来说是非常安全和高效的。但是,一些应用程序却需要执行纯 Java 程序无法完成的一些任务,比如:
- 与旧有代码集成,避免重新编写。
- 实现可用类库中所缺少的功能。举例来说,在 Java 语言中实现
ping
时,您可能需要 Internet Control Message Protocol (ICMP) 功能,但基本类库并未提供它。 - 最好与使用 C/C++ 编写的代码集成,以充分发掘性能或其他与环境相关的系统特性。
- 解决需要非 Java 代码的特殊情况。举例来说,核心类库的实现可能需要跨包调用或者需要绕过其他 Java 安全性检查。
JNI 允许您完成这些任务。它明确分开了 Java 代码与本机代码(C/C++)的执行,定义了一个清晰的 API 在这两者之间进行通信。从很大程度上说,它避免了本机代码对 JVM 的直接内存引用,从而确保本机代码只需编写一次,并且可以跨不同的 JVM 实现或版本运行。
借助 JNI,本机代码可以随意与 Java 对象交互,获取和设计字段值,以及调用方法,而不会像 Java 代码中的相同功能那样受到诸多限制。这种自由是一把双刃剑:它牺牲 Java 代码的安全性,换取了完成上述所列任务的能力。在您的应用程序中使用 JNI 提供了强大的、对机器资源(内存、I/O 等)的低级访问,因此您不会像普通 Java 开发人员那样受到安全网的保护。JNI 的灵活性和强大性带来了一些编程实践上的风险,比如导致性能较差、出现 bug 甚至程序崩溃。您必须格外留意应用程序中的代码,并使用良好的实践来保障应用程序的总体完整性。
本文介绍 JNI 用户最常遇到的 10 大编码和设计错误。其目标是帮助您认识到并避免它们,以便您可以编写安全、高效、性能出众的 JNI 代码。本文还将介绍一些用于在新代码或已有代码中查找这些问题的工具和技巧,并展示如何有效地应用它们。
JNI 编程缺陷可以分为两类:
- 性能 :代码能执行所设计的功能,但运行缓慢或者以某种形式拖慢整个程序。
- 正确性 :代码有时能正常运行,但不能可靠地提供所需的功能;最坏的情况是造成程序崩溃或挂起。
1.性能缺陷
程序员在使用 JNI 时的 5 大性能缺陷如下:
1.1.不缓存方法 ID、字段 ID 和类
要访问 Java 对象的字段并调用它们的方法,本机代码必须调用 FindClass()
、GetFieldID()
、GetMethodId()
和 GetStaticMethodID()
。对于 GetFieldID()
、GetMethodID()
和 GetStaticMethodID()
, 为特定类返回的 ID 不会在 JVM 进程的生存期内发生变化。但是,获取字段或方法的调用有时会需要在 JVM 中完成大量工作,因为字段和方法可能是从超类中继承而来的,这会让 JVM 向上遍历类层次结构来找到它们。由于 ID 对于特定类是相同的,因此您只需要查找一次,然后便可重复使用。同样,查找类对象的开销也很大,因此也应该缓存它们。
举例来说,清单 1 展示了调用静态方法所需的 JNI 代码:
清单 1. 使用 JNI 调用静态方法
int val=1; jmethodID method; jclass cls; cls = (*env)->FindClass(env, "com/ibm/example/TestClass"); if ((*env)->ExceptionCheck(env)) { return ERR_FIND_CLASS_FAILED; } method = (*env)->GetStaticMethodID(env, cls, "setInfo", "(I)V"); if ((*env)->ExceptionCheck(env)) { return ERR_GET_STATIC_METHOD_FAILED; } (*env)->CallStaticVoidMethod(env, cls, method,val); if ((*env)->ExceptionCheck(env)) { return ERR_CALL_STATIC_METHOD_FAILED; } |
当我们每次希望调用方法时查找类和方法 ID 都会产生六个本机调用,而不是第一次缓存类和方法 ID 时需要的两个调用。
缓存会对您应用程序的运行时造成显著的影响。考虑下面两个版本的方法,它们的作用是相同的。清单 2 使用了缓存的字段 ID:
清单 2. 使用缓存的字段 ID
int sumValues2(JNIEnv* env, jobject obj, jobject allValues){ jint avalue = (*env)->GetIntField(env, allValues, a); jint bvalue = (*env)->GetIntField(env, allValues, b); jint cvalue = (*env)->GetIntField(env, allValues, c); jint dvalue = (*env)->GetIntField(env, allValues, d); jint evalue = (*env)->GetIntField(env, allValues, e); jint fvalue = (*env)->GetIntField(env, allValues, f); return avalue + bvalue + cvalue + dvalue + evalue + fvalue; } |
|
清单 3 没有使用缓存的字段 ID:
清单 3. 未缓存字段 ID
int sumValues2(JNIEnv* env, jobject obj, jobject allValues){ jclass cls = (*env)->GetObjectClass(env,allValues); jfieldID a = (*env)->GetFieldID(env, cls, "a", "I"); jfieldID b = (*env)->GetFieldID(env, cls, "b", "I"); jfieldID c = (*env)->GetFieldID(env, cls, "c", "I"); jfieldID d = (*env)->GetFieldID(env, cls, "d", "I"); jfieldID e = (*env)->GetFieldID(env, cls, "e", "I"); jfieldID f = (*env)->GetFieldID(env, cls, "f", "I"); jint avalue = (*env)->GetIntField(env, allValues, a); jint bvalue = (*env)->GetIntField(env, allValues, b); jint cvalue = (*env)->GetIntField(env, allValues, c); jint dvalue = (*env)->GetIntField(env, allValues, d); jint evalue = (*env)->GetIntField(env, allValues, e); jint fvalue = (*env)->GetIntField(env, allValues, f); return avalue + bvalue + cvalue + dvalue + evalue + fvalue } |
清单 2 用 3,572 ms 运行了 10,000,000 次。清单 3 用了 86,217 ms — 多花了 24 倍的时间。
1.2.触发数组副本
JNI 在 Java 代码和本机代码之间提供了一个干净的接口。为了维持这种分离,数组将作为不透明的句柄传递,并且本机代码必须回调 JVM 以便使用 set 和 get 调用操作数组元素。Java 规范让 JVM 实现决定让这些调用提供对数组的直接访问,还是返回一个数组副本。举例来说,当数组经过优化而不需要连续存储时,JVM 可以返回一个副本。(参见 参考资料 获取关于 JVM 的信息)。
随后,这些调用可以复制被操作的元素。举例来说,如果您对含有 1,000 个元素的数组调用 GetLongArrayElements()
,则会造成至少分配或复制 8,000 字节的数据(每个 long
1,000 元素 * 8 字节)。当您随后使用 ReleaseLongArrayElements()
更新数组的内容时,需要另外复制 8,000 字节的数据来更新数组。即使您使用较新的 GetPrimitiveArrayCritical()
,规范仍然准许 JVM 创建完整数组的副本。
|
GetType ArrayRegion()
和 SetType ArrayRegion()
方法允许您获取和更新数组的一部分,而不是整个数组。通过使用这些方法访问较大的数组,您可以确保只复制本机代码将要实际使用的数组部分。
举例来说,考虑相同方法的两个版本,如清单 4 所示:
清单 4. 相同方法的两个版本
jlong getElement(JNIEnv* env, jobject obj, jlongArray arr_j, int element){ jboolean isCopy; jlong result; jlong* buffer_j = (*env)->GetLongArrayElements(env, arr_j, &isCopy); result = buffer_j[element]; (*env)->ReleaseLongArrayElements(env, arr_j, buffer_j, 0); return result; } jlong getElement2(JNIEnv* env, jobject obj, jlongArray arr_j, int element){ jlong result; (*env)->GetLongArrayRegion(env, arr_j, element,1, &result); return result; } |
第一个版本可以生成两个完整的数组副本,而第二个版本则完全没有复制数组。当数组大小为 1,000 字节时,运行第一个方法 10,000,000 次用了 12,055 ms;而第二个版本仅用了 1,421 ms。第一个版本多花了 8.5 倍的时间!
|
另一方面,如果您最终要获取数组中的所有元素,则使用 GetType ArrayRegion()
逐个获取数组中的元素是得不偿失的。要获取最佳的性能,应该确保以尽可能大的块的来获取和更新数组元素。如果您要迭代一个数组中的所有元素,则 清单 4 中这两个 getElement()
方法都不适用。比较好的方法是在一个调用中获取大小合理的数组部分,然后再迭代所有这些元素,重复操作直到覆盖整个数组。
1.3.回访而不是传递参数
在 调用某个方法时,您经常会在传递一个有多个字段的对象以及单独传递字段之间做出选择。在面向对象设计中,传递对象通常能提供较好的封装,因为对象字段的变 化不需要改变方法签名。但是,对于 JNI 来说,本机代码必须通过一个或多个 JNI 调用返回到 JVM 以获取需要的各个字段的值。这些额外的调用会带来额外的开销,因为从本机代码过渡到 Java 代码要比普通方法调用开销更大。因此,对于 JNI 来说,本机代码从传递进来的对象中访问大量单独字段时会导致性能降低。
考虑清单 5 中的两个方法,第二个方法假定我们缓存了字段 ID:
清单 5. 两个方法版本
int sumValues(JNIEnv* env, jobject obj, jint a, jint b,jint c, jint d, jint e, jint f){ return a + b + c + d + e + f; } int sumValues2(JNIEnv* env, jobject obj, jobject allValues){ jint avalue = (*env)->GetIntField(env, allValues, a); jint bvalue = (*env)->GetIntField(env, allValues, b); jint cvalue = (*env)->GetIntField(env, allValues, c); jint dvalue = (*env)->GetIntField(env, allValues, d); jint evalue = (*env)->GetIntField(env, allValues, e); jint fvalue = (*env)->GetIntField(env, allValues, f); return avalue + bvalue + cvalue + dvalue + evalue + fvalue; } |
|
sumValues2()
方法需要 6 个 JNI 回调,并且运行 10,000,000 次需要 3,572 ms。其速度比 sumValues()
慢 6 倍,后者只需要 596 ms。通过传递 JNI 方法所需的数据,sumValues()
避免了大量的 JNI 开销。
1.4.错误认定本机代码与 Java 代码之间的界限
本 机代码和 Java 代码之间的界限是由开发人员定义的。界限的选定会对应用程序的总体性能造成显著的影响。从 Java 代码中调用本机代码以及从本机代码调用 Java 代码的开销比普通的 Java 方法调用高很多。此外,这种越界操作会干扰 JVM 优化代码执行的能力。举例来说,随着 Java 代码与本机代码之间互操作的增加,实时编译器的效率会随之降低。经过测量,我们发现从 Java 代码调用本机代码要比普通调用多花 5 倍的时间。同样,从本机代码中调用 Java 代码也需要耗费大量的时间。
|
因 此,在设计 Java 代码与本机代码之间的界限时应该最大限度地减少两者之间的相互调用。消除不必要的越界调用,并且应该竭力在本机代码中弥补越界调用造成的成本损失。最大限 度地减少越界调用的一个关键因素是确保数据处于 Java/本机界限的正确一侧。如果数据未在正确的一侧,则另一侧访问数据的需求则会持续发起越界调用。
举例来说,如果我们希望使用 JNI 为某个串行端口提供接口,则可以构造两种不同的接口。第一个版本如清单 6 所示:
清单 6. 到串行端口的接口:版本 1
/** * Initializes the serial port and returns a java SerialPortConfig objects * that contains the hardware address for the serial port, and holds * information needed by the serial port such as the next buffer * to write data into * * @param env JNI env that can be used by the method * @param comPortName the name of the serial port * @returns SerialPortConfig object to be passed ot setSerialPortBit * and getSerialPortBit calls */ jobject initializeSerialPort(JNIEnv* env, jobject obj, jstring comPortName); /** * Sets a single bit in an 8 bit byte to be sent by the serial port * * @param env JNI env that can be used by the method * @param serialPortConfig object returned by initializeSerialPort * @param whichBit value from 1-8 indicating which bit to set * @param bitValue 0th bit contains bit value to be set */ void setSerialPortBit(JNIEnv* env, jobject obj, jobject serialPortConfig, jint whichBit, jint bitValue); /** * Gets a single bit in an 8 bit byte read from the serial port * * @param env JNI env that can be used by the method * @param serialPortConfig object returned by initializeSerialPort * @param whichBit value from 1-8 indicating which bit to read * @returns the bit read in the 0th bit of the jint */ jint getSerialPortBit(JNIEnv* env, jobject obj, jobject serialPortConfig, jint whichBit); /** * Read the next byte from the serial port * * @param env JNI env that can be used by the method */ void readNextByte(JNIEnv* env, jobject obj); /** * Send the next byte * * @param env JNI env that can be used by the method */ void sendNextByte(JNIEnv* env, jobject obj); |
在 清单 6 中,串行端口的所有配置数据都存储在由 initializeSerialPort()
方法返回的 Java 对象中,并且将 Java 代码完全控制对硬件中各数据位的设置。清单 6 所示版本的一些问题会造成其性能差于清单 7 中的版本:
清单 7. 到串行端口的接口:版本 2
/** * Initializes the serial port and returns an opaque handle to a native * structure that contains the hardware address for the serial port * and holds information needed by the serial port such as * the next buffer to write data into * * @param env JNI env that can be used by the method * @param comPortName the name of the serial port * @returns opaque handle to be passed to setSerialPortByte and * getSerialPortByte calls */ jlong initializeSerialPort2(JNIEnv* env, jobject obj, jstring comPortName); /** * sends a byte on the serial port * * @param env JNI env that can be used by the method * @param serialPortConfig opaque handle for the serial port * @param byte the byte to be sent */ void sendSerialPortByte(JNIEnv* env, jobject obj, jlong serialPortConfig, jbyte byte); /** * Reads the next byte from the serial port * * @param env JNI env that can be used by the method * @param serialPortConfig opaque handle for the serial port * @returns the byte read from the serial port */ jbyte readSerialPortByte(JNIEnv* env, jobject obj, jlong serialPortConfig); |
|
最显著的一个问题就是,清单 6 中的接口在设置或检索每个位,以及从串行端口读取字节或者向串行端口写入字节都需要一个 JNI 调用。这会导致读取或写入的每个字节的 JNI 调用变成原来的 9 倍。第二个问题是,清单 6 将串行端口的配置信息存储在 Java/本机界限的错误一侧的某个 Java 对象上。我们仅在本机侧需要此配置数据;将它存储在 Java 侧会导致本机代码向 Java 代码发起大量回调以获取/设置此配置信息。清单 7 将配置信息存储在一个本机结构中(比如,一个 struct
),并向 Java 代码返回了一个不透明的句柄,该句柄可以在后续调用中返回。这意味着,当本机代码正在运行时,它可以直接访问该结构,而不需要回调 Java 代码获取串行端口硬件地址或下一个可用的缓冲区等信息。因此,使用 清单 7 的实现的性能将大大改善。
1.5.使用大量本地引用而未通知 JVM
JNI 函数返回的任何对象都会创建本地引用。举例来说,当您调用 GetObjectArrayElement()
时,将返回对数组中对象的本地引用。考虑清单 8 中的代码在运行一个很大的数组时会使用多少本地引用:
清单 8. 创建本地引用
void workOnArray(JNIEnv* env, jobject obj, jarray array){ jint i; jint count = (*env)->GetArrayLength(env, array); for (i=0; i < count; i++) { jobject element = (*env)->GetObjectArrayElement(env, array, i); if((*env)->ExceptionOccurred(env)) { break; } /* do something with array element */ } } |
每次调用 GetObjectArrayElement()
时都会为元素创建一个本地引用,并且直到本机代码运行完成时才会释放。数组越大,所创建的本地引用就越多。
|
这些本地引用会在本机方法终止时自动释放。JNI 规范要求各本机代码至少能创建 16 个本地引用。虽然这对许多方法来说都已经足够了,但一些方法在其生存期中却需要更多的本地引用。对于这种情况,您应该删除不再需要的引用,方法是使用 JNI DeleteLocalRef()
调用,或者通知 JVM 您将使用更多的本地引用。
清单 9 向 清单 8 中的示例添加了一个 DeleteLocalRef()
调用,用于通知 JVM 本地引用已不再需要,以及将可同时存在的本地引用的数量限制为一个合理的数值,而与数组的大小无关:
清单 9. 添加 DeleteLocalRef()
void workOnArray(JNIEnv* env, jobject obj, jarray array){ jint i; jint count = (*env)->GetArrayLength(env, array); for (i=0; i < count; i++) { jobject element = (*env)->GetObjectArrayElement(env, array, i); if((*env)->ExceptionOccurred(env)) { break; } /* do something with array element */ (*env)->DeleteLocalRef(env, element); } } |
|
您可以调用 JNI EnsureLocalCapacity()
方法来通知 JVM 您将使用超过 16 个本地引用。这将允许 JVM 优化对该本机代码的本地引用的处理。如果无法创建所需的本地引用,或者 JVM 采用的本地引用管理方法与所使用的本地引用数量之间不匹配造成了性能低下,则未成功通知 JVM 会导致 FatalError
。
2.正确性缺陷
5 大 JNI 正确性缺陷包括:
2.1使用错误的 JNIEnv
执行本机代码的线程使用 JNIEnv
发起 JNI 方法调用。但是,JNIEnv
并不是仅仅用于分派所请求的方法。JNI 规范规定每个 JNIEnv
对于线程来说都是本地的。JVM 可以依赖于这一假设,将额外的线程本地信息存储在 JNIEnv
中。一个线程使用另一个线程中的 JNIEnv
会导致一些小 bug 和难以调试的崩溃问题。
|
线程可以调用通过 JavaVM
对象使用 JNI 调用接口的 GetEnv()
来获取 JNIEnv
。JavaVM
对象本身可以通过使用 JNIEnv
方法调用 JNI GetJavaVM ()
来获取,并且可以被缓存以及跨线程共享。缓存 JavaVM
对象的副本将允许任何能访问缓存对象的线程在必要时获取对它自己的 JNIEnv
访问。要实现最优性能,线程应该绕过 JNIEnv
,因为查找它有时会需要大量的工作。
2.2未检测异常
本 机能调用的许多 JNI 方法都会引起与执行线程相关的异常。当 Java 代码执行时,这些异常会造成执行流程发生变化,这样便会自动调用异常处理代码。当某个本机方法调用某个 JNI 方法时会出现异常,但检测异常并采用适当措施的工作将由本机来完成。一个常见的 JNI 缺陷是调用 JNI 方法而未在调用完成后测试异常。这会造成代码有大量漏洞以及程序崩溃。
举例来说,考虑调用 GetFieldID()
的代码,如果无法找到所请求的字段,则会出现 NoSuchFieldError
。如果本机代码继续运行而未检测异常,并使用它认为应该返回的字段 ID,则会造成程序崩溃。举例来说,如果 Java 类经过修改,导致 charField
字段不再存在,则清单 10 中的代码可能会造成程序崩溃 — 而不是抛出一个 NoSuchFieldError
:
清单 10. 未能检测异常
jclass objectClass; jfieldID fieldID; jchar result = 0; objectClass = (*env)->GetObjectClass(env, obj); fieldID = (*env)->GetFieldID(env, objectClass, "charField", "C"); result = (*env)->GetCharField(env, obj, fieldID); |
|
添加异常检测代码要比在事后尝试调试崩溃简单很多。经常,您只需要检测是否出现了某个异常,如果是则立即返回 Java 代码以便抛出异常。然后,使用常规的 Java 异常处理流程处理它或者显示它。举例来说,清单 11 将检测异常:
清单 11. 检测异常
jclass objectClass; jfieldID fieldID; jchar result = 0; objectClass = (*env)->GetObjectClass(env, obj); fieldID = (*env)->GetFieldID(env, objectClass, "charField", "C"); if((*env)->ExceptionOccurred(env)) { return; } result = (*env)->GetCharField(env, obj, fieldID); |
不检测和清除异常会导致出现意外行为。您可以确定以下代码的问题吗?
fieldID = (*env)->GetFieldID(env, objectClass, "charField", "C"); if (fieldID == NULL){ fieldID = (*env)->GetFieldID(env, objectClass,"charField", "D"); } return (*env)->GetIntField(env, obj, fieldID); |
问题在于,尽管代码处理了初始 GetFieldID()
未返回字段 ID 的情况,但它并未清除 此调用将设置的异常。因此,本机返回的结果会造成立即抛出一个异常。
2.3未检测返回值
许多 JNI 方法都通过返回值来指示调用成功与否。与未检测异常相似,这也存在一个缺陷,即代码未检测返回值却假定调用成功而继续运行。对于大多数 JNI 方法来说,它们都设置了返回值和异常状态,这样应用程序更可以通过检测异常状态或返回值来判断方法运行正常与否。
|
您可以确定以下代码的问题吗?
clazz = (*env)->FindClass(env, "com/ibm/j9//HelloWorld"); method = (*env)->GetStaticMethodID(env, clazz, "main", "([Ljava/lang/String;)V"); (*env)->CallStaticVoidMethod(env, clazz, method, NULL); |
问题在于,如果未发现 HelloWorld
类,或者如果 main()
不存在,则本机将造成程序崩溃。
2.4未正确使用数组方法
GetXXX ArrayElements()
和 ReleaseXXX ArrayElements()
方法允许您请求任何元素。同样,GetPrimitiveArrayCritical()
、ReleasePrimitiveArrayCritical()
、GetStringCritical()
和 ReleaseStringCritical()
允许您请求数组元素或字符串字节,以最大限度降低直接指向数组或字符串的可能性。这些方法的使用存在两个常见的缺陷。其一,忘记在 ReleaseXXX ()
方法调用中提供更改。即便使用 Critical
版本,也无法保证您能获得对数组或字符串的直接引用。一些 JVM 始终返回一个副本,并且在这些 JVM 中,如果您在 ReleaseXXX ()
调用中指定了 JNI_ABORT
,或者忘记调用了 ReleaseXXX ()
,则对数组的更改不会被复制回去。
举例来说,考虑以下代码:
void modifyArrayWithoutRelease(JNIEnv* env, jobject obj, jarray arr1) { jboolean isCopy; jbyte* buffer = (*env)-> (*env)->GetByteArrayElements(env,arr1,&isCopy); if ((*env)->ExceptionCheck(env)) return; buffer[0] = 1; } |
|
在提供直接指向数组的指针的 JVM 上,该数组将被更新;但是,在返回副本的 JVM 上则不是如此。这会造成您的代码在一些 JVM 上能够正常运行,而在其他 JVM 上却会出错。您应该始终始终包括一个释放(release)调用,如清单 12 所示:
清单 12. 包括一个释放调用
void modifyArrayWithRelease(JNIEnv* env, jobject obj, jarray arr1) { jboolean isCopy; jbyte* buffer = (*env)-> (*env)->GetByteArrayElements(env,arr1,&isCopy); if ((*env)->ExceptionCheck(env)) return; buffer[0] = 1; (*env)->ReleaseByteArrayElements(env, arr1, buffer, JNI_COMMIT); if ((*env)->ExceptionCheck(env)) return; } |
第二个缺陷是不注重规范对在 GetXXX Critical()
和 ReleaseXXX Critical()
之间执行的代码施加的限制。本机可能不会在这些方法之间发起任何调用,并且可能不会由于任何原因而阻塞。未重视这些限制会造成应用程序或 JVM 中出现间断性死锁。
举例来说,以下代码看上去可能没有问题:
void workOnPrimitiveArray(JNIEnv* env, jobject obj, jarray arr1) { jboolean isCopy; jbyte* buffer = (*env)->GetPrimitiveArrayCritical(env, arr1, &isCopy); if ((*env)->ExceptionCheck(env)) return; processBufferHelper(buffer); (*env)->ReleasePrimitiveArrayCritical(env, arr1, buffer, 0); if ((*env)->ExceptionCheck(env)) return; } |
|
但是,我们需要验证在调用 processBufferHelper()
时可以运行的所有代码都没有违反任何限制。这些限制适用于在 Get
和 Release
调用之间执行的所有代码,无论它是不是本机的一部分。
2.5未正确使用全局引用
本机可以创建一些全局引用,以保证对象在不再需要时才会被垃圾收集器回收。常见的缺陷包括忘记删除已创建的全局引用,或者完全失去对它们的跟踪。考虑一个本机创建了全局引用,但是未删除它或将它存储在某处:
lostGlobalRef(JNIEnv* env, jobject obj, jobject keepObj) { jobject gref = (*env)->NewGlobalRef(env, keepObj); } |
|
创 建全局引用时,JVM 会将它添加到一个禁止垃圾收集的对象列表中。当本机返回时,它不仅会释放全局引用,应用程序还无法获取引用以便稍后释放它 — 因此,对象将会始终存在。不释放全局引用会造成各种问题,不仅因为它们会保持对象本身为活动状态,还因为它们会将通过该对象能接触到的所有对象都保持为活 动状态。在某些情况下,这会显著加剧内存泄漏。
3.避免常见缺陷
假设您编写了一些新 JNI 代码,或者继承了别处的某些 JVI 代码,如何才能确保避免了常见缺陷,或者在继承代码中发现它们?表 1 提供了一些确定这些常见缺陷的技巧:
表 1. 确定 JNI 编程缺陷的清单
| 未缓存 | 触发数组副本 | 错误界限 | 过多回访 | 使用大量本地引用 | 使用错误的 JNIEnv | 未检测异常 | 未检测返回值 | 未正确使用数组 | 未正确使用全局引用 |
规范验证 |
|
|
|
|
| X | X |
| X |
|
方法跟踪 | X | X | X | X |
|
| X |
| X | X |
转储 |
|
|
|
|
|
|
|
|
| X |
|
|
|
|
| X |
|
|
|
|
|
代码审查 | X | X | X | X | X | X | X | X | X | X |
您可以在开发周期的早期确定许多常见缺陷,方法如下:
3.1根据 JNI 规范验证新代码
维持规范的限制列表并审查本机与列表的遵从性是一个很好的实践,这可以通过手动或自动代码分析来完成。确保遵从性的工作可能会比调试由于违背限制而出现的细小和间断性故障轻松很多。下面提供了一个专门针对新开发代码(或对您来说是新的)的规范顺从性检查列表:
- 验证
JNIEnv
仅与与之相关的线程使用。 - 确认未在
GetXXX Critical()
的ReleaseXXX Critical()
部分调用 JNI 方法。 - 对于进入关键部分的方法,验证该方法未在释放前返回。
- 验证在所有可能引起异常的 JNI 调用之前都检测了异常。
- 确保所有
Get
/Release
调用在各 JNI 方法中都是相匹配的。
IBM 的 JVM 实现包括开启自动 JNI 检测的选项,其代价是较慢的执行速度。与出色的代码单元测试相结合,这是一种极为强大的工具。您可以运行应用程序或单元测试来执行遵从性检查,或者确定所 遇到的 bug 是否是由本机引起的。除了执行上述规范遵从性检查之外,它还能确保:
- 传递给 JNI 方法的参数属于正确的类型。
- JNI 代码未读取超过数组结束部分之外的内容。
- 传递给 JNI 方法的指针都是有效的。
JNI 检测报告的所有结论并不一定都是代码中的错误。它们还包括一些针对代码的建议,您应该仔细阅读它们以确保代码功能正常。
您可以通过以下命令行启用 JNI 检测选项:
Usage: -Xcheck:jni:[option[,option[,...]]] all check application and system classes verbose trace certain JNI functions and activities trace trace all JNI functions nobounds do not perform bounds checking on strings and arrays nonfatal do not exit when errors are detected nowarn do not display warnings noadvice do not display advice novalist do not check for va_list reuse valist check for va_list reuse pedantic perform more thorough, but slower checks help print this screen |
使用 IBM JVM 的 -Xcheck:jni
选项作为标准开发流程的一部分可以帮助您更加轻松地找出代码错误。特别是,它可以帮助您确定在错误线程中使用 JNIEnv
以及未正确使用关键区域的缺陷的根源。
最新的 Sun JVM 提供了一个类似的 -Xcheck:jni
选项。它的工作原理不同于 IBM 版本,并且提供了不同的信息,但是它们的作用是相同的。它会在发现未符合规范的代码时发出警告,并且可以帮助您确定常见的 JNI 缺陷。
3.2分析方法跟踪
生成对已调用本机方法以及这些本机方法发起的 JNI 回调的跟踪,这对确定大量常见缺陷的根源是非常有用的。可确定的问题包括:
- 大量
GetFieldID()
和GetMethodID()
调用 — 特别是,如果这些调用针对相同的字段和方法 — 表示字段和方法未被缓存。 -
GetType ArrayElements()
调用实例(而非GetType ArrayRegion()
)有时表示存在不必要的复制。 - 在 Java 代码与本机代码之前来回快速切换(由时间戳指示)有时表示 Java 代码与本机代码之间的界限有误,从而造成性能较差。
- 每个本机函数调用后面都紧接着大量
GetFieldID()
调用,这种模式表示并未传递所需的参数,而是强制本机回访完成工作所需的数据。 - 调用可能抛出异常的 JNI 方法之后缺少对
ExceptionOccurred()
或ExceptionCheck()
的调用表示本机未正确检测异常。 -
GetXXX ()
和ReleaseXXX ()
方法调用的数量不匹配表示缺少释放操作。 - 在
GetXXX Critical()
和ReleaseXXX Critical()
调用之间调用 JNI 方法表示未遵循规范施加的限制。 - 如果调用
GetXXX Critical()
和ReleaseXXX Critical()
之间相隔的时间较长,则表示未遵循 “不要阻塞调用” 规范所施加的限制。 -
NewGlobalRef()
和DeleteGlobalRef()
调用之间出现严重失衡表示释放不再需要的引用时出现故障。
一些 JVM 实现提供了一种可用于生存方法跟踪的机制。您还可以通过各种外部工具来生成跟踪,比如探查器和代码覆盖工具。
IBM JVM 实现提供了许多用于生成跟踪信息的方法。第一种方法是使用 -Xcheck:jni:trace
选项。这将生成对已调用的本机方法以及它们发起的 JNI 回调的跟踪。清单 13 显示某个跟踪的摘录(为便于阅读,隔开了某些行):
清单 13. IBM JVM 实现所生成的方法跟踪
Call JNI: java/lang/System.getPropertyList()[Ljava/lang/String; { 00177E00 Arguments: void 00177E00 FindClass("java/lang/String") 00177E00 FindClass("com/ibm/oti/util/Util") 00177E00 Call JNI: com/ibm/oti/vm/VM.useNativesImpl()Z { 00177E00 Arguments: void 00177E00 Return: (jboolean)false 00177E00 } 00177E00 Call JNI: java/security/AccessController.initializeInternal()V { 00177E00 Arguments: void 00177E00 FindClass("java/security/AccessController") 00177E00 GetStaticMethodID(java/security/AccessController, "doPrivileged", "(Ljava/security/PrivilegedAction;)Ljava/lang/Object;") 00177E00 GetStaticMethodID(java/security/AccessController, "doPrivileged", "(Ljava/security/PrivilegedExceptionAction;)Ljava/lang/Object;") 00177E00 GetStaticMethodID(java/security/AccessController, "doPrivileged", "(Ljava/security/PrivilegedAction;Ljava/security/AccessControlContext;) Ljava/lang/Object;") 00177E00 GetStaticMethodID(java/security/AccessController, "doPrivileged", "(Ljava/security/PrivilegedExceptionAction; Ljava/security/AccessControlContext;)Ljava/lang/Object;") 00177E00 Return: void 00177E00 } 00177E00 GetStaticMethodID(com/ibm/oti/util/Util, "toString", "([BII)Ljava/lang/String;") 00177E00 NewByteArray((jsize)256) 00177E00 NewObjectArray((jsize)118, java/lang/String, (jobject)NULL) 00177E00 SetByteArrayRegion([B@0018F7D0, (jsize)0, (jsize)30, (void*)7FF2E1D4) 00177E00 CallStaticObjectMethod/CallStaticObjectMethodV(com/ibm/oti/util/Util, toString([BII)Ljava/lang/String;, (va_list)0007D758) { 00177E00 Arguments: (jobject)0x0018F7D0, (jint)0, (jint)30 00177E00 Return: (jobject)0x0018F7C8 00177E00 } 00177E00 ExceptionCheck() |
清单 13 中的跟踪摘录显示了已调用的本机方法(比如 AccessController.initializeInternal()V
)以及本机方法发起的 JNI 回调。
3.3使用 -verbose:jni
选项
Sun 和 IBM JVM 还提供了一个 -verbose:jni
选项。对于 IBM JVM 而言,开启此选项将提供关于当前 JNI 回调的信息。清单 14 显示了一个示例:
清单 14. 使用 IBM JVM 的 -verbose:jni
列出 JNI 回调
<JNI GetStringCritical: buffer=0x100BD010> <JNI ReleaseStringCritical: buffer=100BD010> <JNI GetStringChars: buffer=0x03019C88> <JNI ReleaseStringChars: buffer=03019C88> <JNI FindClass: java/lang/String> <JNI FindClass: java/io/WinNTFileSystem> <JNI GetMethodID: java/io/WinNTFileSystem.<init> ()V> <JNI GetStaticMethodID: com/ibm/j9/offload/tests/HelloWorld.main ([Ljava/lang/String;)V> <JNI GetMethodID: java/lang/reflect/Method.getModifiers ()I> <JNI FindClass: java/lang/String> |
对于 Sun JVM 而言,开启 -verbose:jni
选项不会提供关于当前调用的信息,但它会提供关于所使用的本机方法的额外信息。清单 15 显示了一个示例:
清单 15. 使用 Sun JVM 的 -verbose:jni
[Dynamic-linking native method java.util.zip.ZipFile.getMethod ... JNI] [Dynamic-linking native method java.util.zip.Inflater.initIDs ... JNI] [Dynamic-linking native method java.util.zip.Inflater.init ... JNI] [Dynamic-linking native method java.util.zip.Inflater.inflateBytes ... JNI] [Dynamic-linking native method java.util.zip.ZipFile.read ... JNI] [Dynamic-linking native method java.lang.Package.getSystemPackage0 ... JNI] [Dynamic-linking native method java.util.zip.Inflater.reset ... JNI] |
开启此选项还会让 JVM 针对使用过多本地引用而未通知 JVM 的情况发起警告。举例来说,IBM JVM 生成了这样一个消息:
JVMJNCK065W JNI warning in FindClass: Automatically grew local reference frame capacity from 16 to 48. 17 references are in use. Use EnsureLocalCapacity or PushLocalFrame to explicitly grow the frame. |
虽然 -verbose:jni
和 -Xcheck:jni:trace
选项可帮助您方便地获取所需的信息,但手动审查此信息是一项艰巨的任务。一个不错的提议是,创建一些脚本或实用工具来处理由 JVM 生成的跟踪文件,并查看 警告 。
3.4生成转储
运行中的 Java 进程生成的转储包含大量关于 JVM 状态的信息。对于许多 JVM 来说,它们包括关于全局引用的信息。举例来说,最新的 Sun JVM 在转储信息中包括这样一行:
JNI global references: 73 |
通过生成前后转储,您可以确定是否创建了任何未正常释放的全局引用。
您可以在 UNIX® 环境中通过对 java
进程发起 kill -3
或 kill -QUIT
来请求转储。在 Windows® 上,使用 Ctrl+Break 组合键。
对于 IBM JVM,使用以下步骤获取关于全局引用的信息:
- 将
-Xdump:system:events=user
添加到命令行。这样,当您在 UNIX 系统上调用kill -3
或者在 Windows 上按下 Ctrl+Break 时,JVM 便会生成转储。 - 程序在运行中时会生成后续转储。
- 运行
jextract -nozip core.XXX output.xml
,这将会将转储信息提取到可读格式的 output.xml 中。 - 查找 output.xml 中的
JNIGlobalReference
条目,它提供关于当前全局引用的信息,如清单 16 所示:
清单 16. output.xml 中的 JNIGlobalReference
条目
<rootobject type="Thread" id="0x10089990" reachability="strong" /> <rootobject type="Thread" id="0x10089fd0" reachability="strong" /> <rootobject type="JNIGlobalReference" id="0x100100c0" reachability="strong" /> <rootobject type="JNIGlobalReference" id="0x10011250" reachability="strong" /> <rootobject type="JNIGlobalReference" id="0x10011840" reachability="strong" /> <rootobject type="JNIGlobalReference" id="0x10011880" reachability="strong" /> <rootobject type="JNIGlobalReference" id="0x10010af8" reachability="strong" /> <rootobject type="JNIGlobalReference" id="0x10010360" reachability="strong" /> <rootobject type="JNIGlobalReference" id="0x10081f48" reachability="strong" /> <rootobject type="StringTable" id="0x10010be0" reachability="weak" /> <rootobject type="StringTable" id="0x10010c70" reachability="weak" /> <rootobject type="StringTable" id="0x10010d00" reachability="weak" /> <rootobject type="StringTable" id="0x10011018" reachability="weak" /> |
通过查看后续 Java 转储中报告的数值,您可以确定全局引用是否出现的泄漏。
参见 参考资料 获取关于使用转储文件以及 IBM JVM 的 jextract
的更多信息。
3.5执行代码审查
代 码审查经常可用于确定常见缺陷,并且可以在各种级别上完成。继承新代码时,快速扫描可以发现各种问题,从而避免稍后花费更多时间进行调试。在某些情况下, 审查是确定缺陷实例(比如未检查返回值)的唯一方法。举例来说,此代码的问题可能可以通过代码审查轻松确定,但却很难通过调试来发现:
int calledALot(JNIEnv* env, jobject obj, jobject allValues){ jclass cls = (*env)->GetObjectClass(env,allValues); jfieldID a = (*env)->GetFieldID(env, cls, "a", "I"); jfieldID b = (*env)->GetFieldID(env, cls, "b", "I"); jfieldID c = (*env)->GetFieldID(env, cls, "c", "I"); jfieldID d = (*env)->GetFieldID(env, cls, "d", "I"); jfieldID e = (*env)->GetFieldID(env, cls, "e", "I"); jfieldID f = (*env)->GetFieldID(env, cls, "f", "I"); } jclass getObjectClassHelper(jobject object){ /* use globally cached JNIEnv */ return cls = (*globalEnvStatic)->GetObjectClass(globalEnvStatic,allValues); } |
代码审查可能会发现第一个方法未正确缓存字段 ID,尽管重复使用了相同的 ID,并且第二个方法所使用的 JNIEnv
并不在应该在的线程上。
|
4.结束语
现在,您已经了解了 10 大 JNI 编程缺陷,以及一些用于在已有或新代码中确定它们的良好实践。坚持应用这些实践有助于提高 JNI 代码的正确率,并且您的应用程序可以实现所需的性能水平。
有 效集成已有代码资源的能力对于面向对象架构(SOA)和基于云的计算这两种技术的成功至关重要。JNI 是一项非常重要的技术,用于将非 Java 旧有代码和组件集成到基于 Java 的平台中,充当 SOA 或基于云的系统的基本元素。正确使用 JNI 可以加速将这些组件转变为服务的过程,并允许您从现有投资中获得最大优势。
参考资料
学习
- Java Native Interface :您可以在此处找到 JNI 规范、FAQ、示例和其他资源。
- “用 JNI 进行 Java 编程 ”(Scott Stricker,developerWorks,2002 年 3 月):在本教程中,学习 JNI 基础知识以及一些比较高级的编程挑战。
- IBM Java SDK Info Center :了解关于在 IBM JVM 中使用转储文件以及
jextract
的更多信息。 - “JNI Programming on AIX ”(Nikolay Yevik,developerWorks,2004 年 3 月):获取使用 IBM JDK for AIX 开发 JNI 应用程序的通用指南。
- “Design and Implementation of a Comprehensive Real-time Java Virtual Machine ”(Joshua Auerbach 等,Proceedings of the Seventh ACM and IEEE International Conference on Embedded Software ,2007 年):了解可返回数组副本的 JVM。
- 浏览 技术书店 查找有关这些主题和其他技术主题的图书。
- developerWorks Java 技术专区 :数百篇关于 Java 编程各方面的文章。
Chap21:JNI设计实践之路
一、 前言
本 文为在 32 位 Windows 平台上实现 Java 本地方法提供了实用的示例、步骤和准则。本文中的示例使用 Sun公司的 Java Development Kit (JDK) 版本 1.4.2。用 C ++语言编写的本地代码是用 Microsoft Visual C++ 6.0编译器编译生成。规定在Java程序中function/method称为方法,在C++程序中称为函数。
本 文将围绕求圆面积逐步展开,探讨java程序如何调用现有的DLL?如何在C++程序中创建,检查及更新Java对象?如何在C++和Java程序中互抛 异常,并进行异常处理?最后将探讨Eclipse及JBuilder工具可执行文件为什么不到100K大小以及所采用的技术方案?
二、 JNI基础知识简介
Java 语言及其标准API应付应用程序的编写已绰绰有余。但在某些情况下,还是必须使用非Java代码,例如:打印、图像转换、访问硬件、访问现有的非Java 代码等。与非Java代码的沟通要求获得编译器和JVM的专门支持,并需附加的工具将Java代码映射成非Java代码。目前,不同的开发商为我们提供了 不同的方案,主要有以下方法:
1. JNI(Java Native Interface)
2. JRI(Java Runtime Interface)
3. J/Direct
4. RNI(Raw Native Interface)
5. Java/COM集成方案
6. CORBA(Common Object Request Broker Architecture)
其中方案1是JDK自带的一部分,方案2由网景公司所提供,方案3 、 4 、 5是微软所提供的方案,方案6是一家非盈利组织开发的一种集成技术,利用它可以在由不同语言实现的对象之间进行“相互操作”的能力。
在开发过程中,我们一般采用第1种方案――JNI技术。因为只用当程序用Microsoft Java编译器编译,而且只有在Microsoft Java虚拟机(JVM)上运行的时候,才采用方案3 、 4 、 5。而方案6一般应用在大型的分布式应用中。
JNI是一种包容极广的编程接口,允许我们从Java应用程序里调用本地化方法。也就是说,JNI允许运行在虚拟机上的Java程序能够与其它语言(例如 C/ C++/汇编语言)编写的程序或者类库进行相互间的调用。同时JNI也提供了一整套的API,允许将Java虚拟机直接嵌入到本地的应用程序中。其中 JNI所扮演的角色可用图一描述:
图一 JNI基本结构描述图
目前JNI只能与用C和C++编写的本地化方法打交道。利用JNI,我们本地化方法可以:
1. 创建、检查及更新Java对象
2. 调用Java和非Java程序所编写的方法(函数),以及win32 API等.
3. 捕获和抛出“异常”
4. 装载类并获取类信息
5. 进行运行期类型检查
所以,原来在Java程序中能对类及对象所做的几乎所有事情都可以在本地化方法中实现。
下图表示了通过JNI,Java程序和非Java程序相互调用原理。
图二 Java程序和非Java程序通过JNI相互调用原理
通过JNI,编写Java程序调用非Java程序一般步骤:
1.) 编写对本地化方法及自变量进行声明的Java代码
2.) 利用头文件生成器javah生成本地化方法对应的头文件
3.) 利用C和C++实现本地化方法(可调用非Java程序),并编译、链接生成DLL文件
4.) Java程序通过生成的DLL调用非Java程序
同时我们也可以通过JNI,将Java虚拟机直接嵌入到本地的应用程序中,步骤很简单,只需要在C/C++程序中以JNI API函数为媒介调用Java程序。
以上步骤虽简单,但有很多地方值得注意。如果一招不慎,可能造成满盘皆输。
三、 Java程序调用非Java程序
3.1 本地化方法声明及头文件生成
任务:现有一求圆面积的Circle.dll(用MFC编写,参数:圆半径;返回值:圆面积)文件,在Java程序中调用该Dll。
在本地化声明中,可分无包和有包两种情况。我们主要对有包的情况展开讨论。
实例1:
package com.testJni;
public class Circle
{
public native void cAreas(int radius) ;
static
{
//System.out.println(System.getProperty("java.library.path"));
System.loadLibrary("CCircle");
}
}
在Java程序中,需要在类中声明所调用的库名称System.loadLibrary( String libname );
该 函数是将一个Dll/so库载入内存,并建立同它的链接。定位库的操作依赖于具体的操作系统。在windows下,首先从当前目录查找,然后再搜 寻”PATH”环境变量列出的目录。如果找不到该库,则会抛出异常UnsatisfiedLinkError。库的扩展名可以不用写出来,究竟是Dll还 是so,由系统自己判断。这里加载的是3.2中生成的DLL,而不是其他应用程序生成的Dll。还需要对将要调用的方法做本地声明,关键字为native 。表明此方法在本地方法中实现,而不是在Java程序中,有点类似于关键字abstract。
我们写一个Circle.bat批处理文件编译Circle.java文件,内容如下(可以用其他工具编译):
javac -d . Circle.java
javah com.testJni.Circle
pause
对于有包的情况一定要注意这一点 , 就是在用javah时有所不同。开始时我的程序始终运行都不成功,问题就出在这里。本类名称的前面均是包名。这样生成的头文件就 是:com_testJni_Circle.h。开始时,在包含包的情况下我用javah Circle生成的头文件始终是Circle.h。在网上查资料时,看见别人的头文件名砸那长,我的那短。但不知道为什么,现在大家和我一样知道为什么了 吧。:)。
如果是无包的情况,则将批处理文件换成如下内容:
javac Circle.java
javah Circle
pause
3.2 本地化方法实现
刚才生成的com_testJni_Circle.h头文件内容如下:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_testJni_Circle */
#ifndef _Included_com_testJni_Circle
#define _Included_com_testJni_Circle
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_testJni_Circle
* Method: cAreas
* Signature : (I)V
*/
JNIEXPORT void JNICALL Java_com_testJni_Circle_cAreas
(JNIEnv *, jobject, jint);
#ifdef __cplusplus
}
#endif
#endif
如果在本地化方法声明中,方法cAreas ()声明为static类型,则与之相对应的Java_com_testJni_Circle_cAreas()函数中的第二个参数类型为jclass。也就是
JNIEXPORT void JNICALL Java_com_testJni_Circle_cAreas(JNIEnv *env, jclass newCircle ,jint radius)。
这里JNIEXPORT 和JNICALL 都是JNI的关键字,其实是一些宏(具体参看jni_md.h文件)。
从以上头文件中,可以看出函数名生成规则为:Java[ _包名]_类名_方法名[ _函数签名] (其中[ ]是可选项),均以字符下划线( _ )分割。如果是无包的情况,则不包含[ _包名]选项。如果本地化方法中有方法重载,则在该函数名最后面追加函数签名,也就是Signature对应的值,函数签名参见表一。
函数签名 | Java类型 |
V | void |
Z | boolean |
B | byte |
C | char |
S | short |
I | int |
J | long |
F | float |
D | double |
L fully-qualified-class ; | fully-qualified-class |
[ type | type[] |
( arg-types ) ret-type | method type |
表一函数签名与Java类型的映射
在具体实现的时候,我们只关心函数原型:
JNIEXPORT void JNICALL Java_com_testJni_Circle_cAreas(JNIEnv *, jobject, jint);
现 在就让我们开始激动人心的一步吧 : ) 。启动VC集成开发环境,新建一工程,在project里选择win32 Dynamic-link Library,输入工程名,然后点击ok,接下去步骤均取默认(图三)。如果不取默认,生成的工程将会有DllMain ()函数,反之将无这个函数。我在这里取的是空。
图三 新建DLL工程
然 后选择菜单File->new->Files->C++ Source File,生成一个空*.cpp文件,取名为CCircle。与3.1中System.loadLibrary("CCircle");参 数保持一致。将JNIEXPORT void JNICALL Java_com_testJni_Circle_cAreas(JNIEnv *, jobject, jint);拷贝到CPP文件中,并包含其头文件。
对应的CCircle.cpp内容如下:
#include<iostream.h>
#include"com_testJni_Circle.h"
#include"windows.h"
JNIEXPORT void JNICALL Java_com_testJni_Circle_cAreas(JNIEnv *env, jobject newCircle,jint radius)
{
//调用求圆面积的Circle.dll
typedef void (*PCircle)(int radius);
HINSTANCE hDLL;
PCircle Circle;
hDLL=LoadLibrary("Circle.dll");//加载动态链接库Circle.dll文件
Circle=(PCircle)GetProcAddress(hDLL,"Circle");
Circle(8);
FreeLibrary(hDLL);//卸载Circle.dll文件;
}
在编译前一定要注意下列情况。
注意: 一定要把SDK目录下include文件夹及其下面的win32文件夹中的头文件拷贝到VC目录的include文件夹下。或者在VC的tools\options\directories中设置,如图四所示。
图四 头文件设置
我们知道dll文件有两种指明导出函数的方法,一种是在.def文件中定义,另一种是在定义函数时使用关键字__declspec(dllexport)。而关键字JNIEXPORT实际在jni_md.h中如下定义,
#define JNIEXPORT __declspec(dllexport),可见JNI默认的导出函数使用第二种。使用第二种方式产生的导出函数名会根据编译器发生变化,在有的情况下 会发生找不到导出函数的问题(我们在java控制台程序中调用很正常,但把它移植到JSP页面时,就发生了该问题,JVM开始崩溃,百思不得其解,后来加 入一个.def文件才解决问题)。其实在《windows 核心编程》一书中,第19.3.2节就明确指出创建用于非Visual C++工具的DLL时,建议加入一个def文件,告诉Microsoft编译器输出没有经过改变的函数名。因此最好采用第一种方法,定义一个.def文件 来指明导出函数。本例中可以新建一个CCircle.def文件,内容如下:
; CCircle.def : Declares the module parameters for the DLL.
LIBRARY "CCircle"
DESCRIPTION 'CCircle Windows Dynamic Link Library'
EXPORTS
; Explicit exports can go here
Java_com_testJni_Circle_cAreas
现在开始对所写的程序进行编译。选择build->rebuild all对所写的程序进行编译。点击build->build CCirclee.DLL生成DLL文件。
也可以用命令行cl来编译。语法格式参见JDK文档JNI部分。
再次强调(曾经为这个东西大伤脑筋) :DLL放置地方
1) 当前目录。
2) Windows的系统目录及Windows目录
3) 放在path所指的路径中
4) 自己在path环境变量中设置一个路径,要注意所指引的路径应该到.dll文件的上一级,如果指到.dll,则会报错。
下面就开始测试我们的所写的DLL吧(假设DLL已放置正确)。
import com.testJni.Circle;
public class test
{
public static void main(String argvs[])
{
Circle myCircle;
myCircle = new Circle();
myCircle.cAreas(2);
}
}
编译,运行程序,将会弹出如下界面:
图五 运行结果
以上是我们通过JNI方法调用的一个简单程序。实际情况要比这复杂的多。
现在开始来讨论JNI中参数的情况,我们来看一个程序片断。
实例二:
JNIEXPORT jstring JNICALL Java_MyNative_cToJava
(JNIEnv *env, jclass obj)
{
jstring jstr;
char str[]="Hello,word!\n";
jstr=env->NewStringUTF(str);
return jstr;
}
在 C和Java编程语言之间传送值时,需要理解这些值类型在这两种语言间的对应关系。这些都在头文件jni.h中定义,用typedef语句声明了这些类在 目标平台上的代价类。头文件也定义了常量如:JNI_FALSE=0 和JNI_TRUE=1;表二和表三说明了Java类型和C类型之间的映射关系。
Java语言 | C/C++语言 | bit位数 |
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 | 0 |
表二 Java基本类型到本地类型的映射
表三 Java中的类到本地类的映射
JNI 函数NewStringUTF()是从一个包含UTF格式编码字符的char类型数组中创建一个新的jstring对象。jstring是以JNI为中介 使Java的String类型与本地的string沟通的一种类型,我们可以视而不见 (具体对应见表二和表三)。如果你使用的函数是GetStringUTFChars()(将jstring转换为UTF-8字符串),必须同时使用 ReleaseStringUTFChars()函数,通过它来通知虚拟机去回收UTF-8串占用的内存,否则将会造成内存泄漏,最终导致系统崩溃。因为 JVM在调用本地方法时,是在虚拟机中开辟了一块本地方法栈供本地方法使用,当本地方法使用完UTF-8串后,得释放所占用的内存。其中程序片断 jstr=env->NewStringUTF(str);是C++中的写法,不必使用env指针。因为JNIEnv函数的C++版本包含有直接插 入成员函数,他们负责查找函数指针。而对于C的写法,应改为:jstr=(*env)-> NewStringUTF(env ,str);因为所有JNI函数的调用都使用env指针,它是任意一个本地方法的第一个参数。env指针是指向一个函数指针表的指针。因此在每个JNI函数访问前加前缀(*env)->,以确保间接引用函数指针。
C/C++和Java互传参数需要自己在编程过程中仔细摸索与体味。
四、 C/C++访问Java成员变量和成员方法
我们修改3.1中的Java程序声明,加入如下代码:
private int circleRadius;
public Circle()
{
circleRadius=0;
}
public void setCircleRadius(int radius)
{
circleRadius=radius;
}
public void javaAreas()
{
float PI = 3.14f;
if(circleRadius<=0)
{
System.out.println (“error!”);
}
else
{
System.out.println (PI*circleRadius*circleRadius);
}
}
在C++程序中访问Circle类中的private私有成员变量circleRadius,并设置它的值,同时调用Java方法javaAreas()。在函数Java_com_testJni_Circle_cAreas()中加入如下代码:
jclass circle;
jmethodID AreasID;
jfieldID radiusID;
jint newRadius=5;
circle = env->GetObjectClass(newCircle);//get current class
radiusID=env->GetFieldID(circle,"circleRadius","I");//get field ID
env->SetIntField(newCircle,radiusID,newRadius);//set field value
AreasID=env->GetMethodID(circle,"javaAreas","()V");//get method ID
env->CallVoidMethod(newCircle,AreasID,NULL);//invoking method
在C++代码中,创建、检查及更新Java对象,首先要得到该类,然后再根据类得到其成员的ID,最后根据该类的对象,ID号调用成员变量或者成员方法。
得 到类,有两个API函数,分别为FindClass()和GetObjectClass();后者顾名思义用于已经明确知道其对象,然后根据对象找类。前 者用于得到没有实例对象的类。这里也可以改成circle = env-> FidnClass("com/testJni/Circle");其中包的分隔符用字符" /"代替。如果已知一个类,也可以在C++代码中创建该类对象,其JNI函数为NewObject();示例代码如下:
jclass circle =env->FindClass("com/testJni/ Circle ");
jmethodID circleID=env->GetMethodID(circle,"<init>","()V");//得到构造函数的ID
jobject newException=env->NewObject(circle, circleID,NULL);
得 到成员变量的ID,根据其在Java代码中声明的类型不同而不同。具体分为两大类:非static型和static型,分别对应GetFieldID() 和GetStaticFieldID()。同时也可以获得和设置成员变量的值,根据其声明的type而变化,获得其值的API函数为:Get typeField ()和GetStatic typeField ();与之相对应的设置其值的函数为Set typeField ()和SetStatic typeField ();在本例中,成员变量circleRadius声明成int型,则对应的函数分别为GetIntField()和SetIntField();
其实JNI API函数名是很有规律的,从上面已窥全貌。获得成员方法的ID也是同样的分类方法。具体为GetMethodID()和GetStaticMethodID()。调用成员方法跟获得成员变量的值相类似,也根据其方法返回值的type不同而不同,分别为Call typeMethod ()和CallStatic typeMethod ()。对于返回值为void的类型,其相应JNI函数为CallVoidMethod();
以上获得成员ID函数的形参均一致。第一个参数为jclass,第二个参数为成员变量或方法,第三个参数为该成员的签名(签名可参见表一)。但调用或设置成员变量或方法时,第一个参数为实例对象(即jobject),其余形参与上面相同。
特别要注意的是得到构造方法的ID时,第二个参数不遵循上面的原则,为jmethodID constructorID = env->GetMethodID(jclass, "<init>" ," 函数签名");
从上面代码中可以看出,在C++中可以访问java程序private类型的变量,严重破坏了类的封装原则。从而可以看出其不安全性。
五、 异常处理
本地化方法稳定性非常差,调用任何一个JNI函数都会出错,为了程序的健壮性 ,非常有必要在本地化方法中加入异常处理。我们继续修改上面的类。
我们声明一个异常类,其代码如下:
package com.testJni;
import com.testJni.*;
public class RadiusIllegal extends Exception
{
protected String MSG="error!";
public RadiusIllegal(String message)
{
MSG=message;
}
public void print()
{
System.out.println(MSG);
}
}
同时也修改Circle.java中的方法,加入异常处理。
public void javaAreas() throws RadiusIllegal //修改javaAreas(),加入异常处理
{
float PI = 3.14f;
if(circleRadius<=0)
{
throw new RadiusIllegal("warning:radius is illegal!");
}
else
{
System.out.println (PI*circleRadius*circleRadius);
}
}
public native void cAreas(int radius) throws RadiusIllegal; //修改cAreas (),加入异常处理
修改C++代码中的函数,加入异常处理,实现Java和C++互抛异常,并进行异常处理。
JNIEXPORT void JNICALL Java_com_testJni_Circle_cAreas(JNIEnv *env, jobject newCircle,jint radius)
{
//此处省略部分代码
radiusIllegal=env->FindClass("com/testJni/RadiusIllegal");//get the exception class
if((exception=env->ExceptionOccurred())!=NULL)
{
cout<<"errors in com_testJni_RadiusIllegal"<<endl;
env->ExceptionClear();
}
//此处省略部分代码
env->CallVoidMethod(newCircle,AreasID,NULL);//invoking
if((exception=env->ExceptionOccurred())!=NULL)
{
if(env->IsInstanceOf(exception,radiusIllegal)==JNI_TRUE)
{
cout<<"errors in java method"<<endl;
env->ExceptionClear();
}
else
{
cout<<"errors in invoking javaAreas() method of Circle"<<endl;
env->ExceptionClear();
}
}
if(radius<=0)
{
env->ThrowNew(radiusIllegal,"errors in C function!");//throw exception
return ;
}
else
{
//此处为调用计算圆面积的DLL
}
}
在本地化方法(C++)中,可以自己处理异常,也可以重新抛出异常,让Java程序来捕获该异常,进行相关处理。
如 果调用JNI函数发生异常,不及时进行处理,再次调用其他JNI函数时,可能会使JVM崩溃(crash),大多数JNI函数都具有此特性。可以调用函数 ExceptionOccurred()来判断是否发生了异常。该函数返回jthrowable的实例对象,如本例 if((exception=env->ExceptionOccurred())!=NULL)就用来判断是否发生了异常。当要判断具体是哪个异 常发生时,可以用IsInstanceOf()来进行测试,此函数非彼IsInstanceOf(Java语言中的IsInstanceOf)。在上面的 代码中,我们在本地化方法中给circleRadius设置了一非法值,然后调用方法javaAreas(),此时java代码会抛出异常,在本地化方法 中进行捕获,然后用IsInstanceOf()来进行测试是否发生了RadiusIllegal类型的异常,以便进行相关处理。在调用其他JNI函数之 前,应当首先清除异常,其函数为ExceptionClear()。
如果是C++的程序发生异常,则可以用 JNI API函数ThrowNew()抛出该异常。但此时本地化方法并不返回退出,直到该程序执行完毕。所以当在本地化方法中发生异常时,应该人为的退出,及时 进行处理,避免程序崩溃。函数ThrowNew()中第一个参数为jclass的类,第二个参数为附加信息,用来描述异常信息。
如果要知道异常发生的详细信息,或者对程序进行调试时,可以用函数ExceptionDescribe()来显示异常栈里面的内容。
六、 MFC程序中嵌入Java虚拟机
可能大家天天都在用Eclipse和Jbulider这两款优秀的IDE进行程序开发,可能还不知道他们的可执行文件不到100KB大小,甚则连一副图片都可能比他们大。其实隐藏在他们背后的技术是JNI,可执行文件只是去启动Java程序,所以也只有那么小。
我 们只需要在MFC程序中创建一个JVM,然后基于这个JVM调用Java的方法,启动Java程序,就可以模拟出Eclipse和Jbulider的那种 效果,使java程序更专业。其实要实现这种效果,用上面的方法足有够有。创建JVM,只需包含相应的类库,设置相关的属性。
首先进行环境设置,在VC环境的tools-->options-->Directories下的Library files选项中包含其创建JVM的库文件jvm.lib,该库文件位于JDK \ lib目录下,如图6所示:
图六库文件路径设置
然后,在环境变量path中设置jvm.dll的路径。该Dll 位于jdk\jre\bin\server目录或jdk\jre\bin\client目录下。注意:一定不要将jvm.dll和jvm.lib拷贝到你应用程序路径下 ,这样会引起JVM初始化失败。因为Java虚拟机是以相对路径来寻找和调用用到的库文件和其他相关文件。
接下来,我们在MFC程序(该程序请到《程序员》杂志频道下载)中进行创建JVM初始化工作。示例代码如下:
JNIEnv *env;
JavaVM *jvm;
jint res;
jclass cls;
jmethodID mid;
JavaVMInitArgs vm_args;
JavaVMOption options[3];
memset(&vm_args, 0, sizeof(vm_args));
//进行初始化工作
options[0].optionString = "-Djava.compiler=NONE";
options[1].optionString = "-Djava.class.path=.";
options[2].optionString = "-verbose:jni";
vm_args.version=JNI_VERSION_1_4; //版本号设置
vm_args.nOptions = 3;
vm_args.options = options;
vm_args.ignoreUnrecognized = JNI_TRUE;
res = JNI_CreateJavaVM(&jvm,(void**)&env,&vm_args); //创建JVM
if (res < 0)
{
MessageBox( "Can't create Java VM","Error",MB_OK|MB_ICONERROR);
exit(1);
}
cls = env->FindClass("prog");
if(env->ExceptionOccurred()!=NULL)
{
MessageBox( "Can't find Prog class!","Error",MB_OK|MB_ICONERROR);
exit(1);
}
mid = env->GetStaticMethodID(cls, "main", "([Ljava/lang/String;)V");
if(env->ExceptionOccurred()!=NULL)
{
MessageBox("Can't find Prog.main!","Error",MB_OK|MB_ICONERROR);
exit(1);
}
env->CallStaticVoidMethod( cls, mid, NULL); //调用Java程序main()方法,启动Java程序
if(env->ExceptionOccurred()!=NULL)
{
MessageBox( "Fatal Error!","Error",MB_OK|MB_ICONERROR);
exit(1);
}
jvm->DestroyJavaVM();//释放JVM资源
程序首先进行JVM初始化设置。我们观察jni.h 文件关于JavaVMOption和JavaVMInitArgs的定义
typedef struct JavaVMOption {
char *optionString;
void *extraInfo;
} JavaVMOption;
typedef struct JavaVMInitArgs {
jint version;
jint nOptions;
JavaVMOption *options;
jboolean ignoreUnrecognized;
} JavaVMInitArgs;
结 构体JavaVMInitArgs中有四个参数,我们在程序中都得必须设置。其中版本号一定要设置正确,不同的版本有不同的设置方法,关于版本1.1和 1.2的设置方法参看sun公司的文档,这里只给出版本1.4的设置方法。第二个参数表示JavaVMOption结构体变量的维数,这里设置为三维,其 中options[0].optionString = "-Djava.compiler=NONE";表示disable JIT;options[1].optionString = "-Djava.class.path=.";表示你所调用Java程序的Class文件的路径,这里设置为该exe应用程序的根路径(最后一个字 符"."表示根路径);options[2].optionString = "-verbose:jni";用于跟踪运行时的信息。第三个参数是一个JavaVMOption的指针变量。第四个参数意思我们可以参看帮助文档的解释 If ignoreUnrecognized is JNI_FALSE, JNI_CreateJavaVM returns JNI_ERR as soon as it encounters any unrecognized option strings。
初始化完毕后,就可以调用创建JVM的函数jint JNICALL JNI_CreateJavaVM(JavaVM **pvm, void **penv, void *args);如果返回值小于0表示创建JVM失败。最可能的原因就是jvm.dll和jvm.lib 设置错误。
如果在运行的过程中找不到java程序的类,那么就是-Djava.class.path设置错误。只要JVM创建成功,就可以根据上面的方法调用java程序。最后当程序结束后,调用函数DestroyJavaVM()摧毁JVM,释放资源。
七、 附录
利 用JNI函数,我们可以从本地化方法的内部与JVM打交道。正如在前面的例子中所看到的那样,每个本地化方法中都会接收一个特殊的自变量作为自己的第一个 参数:JNIEnv――它是指向类型为JNIEnv_的一个特殊JNI数据结构的指针。JNI数据结构的一个元素是指向由JVM生成的一个指针的数组;该 数组的每个元素都是指向一个JNI函数的指针。可以从本地化方法的内部对JNI函数的调用。第二个参数会根据Java类中本地方法的定义不同而不同,如果 是定义为static方法,类型会是jclass,表示对特定Class对象的引用,如果是非static方法,类型是jobject,表示当前对象的引 用,相当于” this”。可以说这两个变量是本地化方法返回JAVA的大门。
注意: 在本地化方法中生成的Dll不具备到处运行的特性,而具有”牵一发而动全身”的特点。只要包名一改变,那么你所有的工作就得重新做一遍。原因就是当用javah生成头文件时,函数名的生成规则为Java[ _包名]_类名_方法名[ _函数签名] ;当你的包名改变时,生成的函数名也跟着改变了,那么你再次调用以前编写的Dll时,会抛出异常。
八、 参考文献
1. 《Java 编程思想》Bruce Eckel 机械工业出版社
2. 《Java2 核心技术卷2》(第6版)Cay S.Horstmann,Gary Cornell机械工业出版社
3. 《高级Java2 大学教程》(英文版) Harvey M.Deitel ,Paul J.Deitel,Sean E.Santry 电子工业出版社
4. 《windows 核心编程》Jeffrey Richter 机械工业出版社
6. sun公司文档
如对本文有任何疑问和异议,欢迎与作者探讨:normalnotebook@126.com
注:本文最初发表在2004年《开发高手》第12期上。
=====================================================================
利用jawin完成调用window中dll的调用 最近由于项目的特殊需求,我们必须在程序调用window的dll。 》至此在Editplus中调试Jawin/NJawin的例子,可以通过。 而在Eclipse中有时还会出上面的错误:COMException : no jawin in java.library.path。 》调用 dll,dll 的方式不需要导出了,直接调用就可以了,下面是下载的包中提供的一个例子: import org.jawin.ReturnFlags; /** public static void main(String[] args) { try { FuncPtr msgBox = new FuncPtr("USER32.DLL", "MessageBoxW");
msgBox.invoke_I(0, "Hello From a DLL", "From Jawin", 0, ReturnFlags.CHECK_NONE); } catch (Exception e) { e.printStackTrace(); } } } |
static int32_t readInputStreamData(int32_t handle, uint8_t* buf, int32_t bufLen)
{
JNIEnv* env;
jobject* pInputStream;
int32_t len;
DrmData* p;
jclass cls;
jmethodID mid;
jbyteArray tmp;
int tmpLen;
jbyte* pNativeBuf;
p = (DrmData *)handle;
if (NULL == p || NULL == buf || bufLen <- 0)
return 0;
env = p->env;
pInputStream = p->pInData;
len = p->len;
if (NULL == env || p->len <= 0 || NULL == pInputStream)
return 0;
cls = (*env)->GetObjectClass(env, *pInputStream);
mid = (*env)->GetMethodID(env, cls, "read", "([BII)I");
tmp = (*env)->NewByteArray(env, bufLen);
bufLen = (*env)->CallIntMethod(env, *pInputStream, mid, tmp, 0, bufLen);
(*env)->DeleteLocalRef(env, cls);
if (-1 == bufLen)
return -1;
pNativeBuf = (*env)->GetByteArrayElements(env, tmp, NULL);
memcpy(buf, pNativeBuf, bufLen);
(*env)->ReleaseByteArrayElements(env, tmp, pNativeBuf, 0);
(*env)->DeleteLocalRef(env, tmp);
return bufLen;
}
Ch22:用Java代码处理本地对象的事件
当您需要使用以其他语言编写的对象时,本地事件源和 Java? 侦听器之间的通信可能需要一些小技巧 —— 尤其是在多线程环境中。本文通过使用一种透明处理从本地代码到 JVM 的事件通信的设计模式,帮助您有效地处理传统的本地库。 在面向对象系统中,对象可以触发一组事件。Java 编程语言为定义基于观察者设计模式(Observer design pattern)的事件侦听器提供了支持,但当您需要使用以其他语言编写的对象时,这还不够。使用 Java Native Interface (JNI) 在本地事件源和 Java 侦听器之间进行通信,可能需要一些技巧,尤其是有多线程环境中。在本文中,我们描述了一种透明处理从本地代码到 JVM 的事件通信的设计模式。您可以使用这种设计来提供到遗留本地应用程序的 Java 接口,或者构建带 Java 侦听器的本地应用程序。
1.观察者设计模式
观察者设计模式定义了事件侦听器与事件创建者之间的多对一依赖关系。当事件创建者触发一个事件时,其所有侦听器接收到该事件的通知。由于事件创建者和侦 听器是无关的,您可以单独使用或修改它们。这种设计模式是事件驱动编程的核心,被广泛用于 GUI 框架,比如 Swing 和 SWT。
如果整个应用程序都使用 Java 编程语言编写,实现观察者设计模式相当简单。图 1 中的类图给出了一个例子: 图 1. 观察者设计模式
Java 本地应用程序侦听器
不过,在某些情况下,我们想要让本地应用程序支持 Java 侦听器。大量应用程序(包括应用程序入口点)可能以本地代码编写,而应用程序以与其用户界面交互的方式生成事件。在这种情形下,支持基于Java 用户界面的最佳方式是让 Java 类将自身注册为应用程序生成的各种事件的侦听器。简而言之,通过支持以 Java 语言编写的侦听器,可以获得支持 Java 的 本地应用程序。
图 2 所示的类图给出了一个示例场景。CEventSource 类用 C++ 语言编写。它使用 addMouseDownListener() 和 removeMouseDownListener() 让侦听器注册和取消注册其 “鼠标按下” 事件。它想要所有侦听器实现 IMouseDownListener 接口。 图 2. 示例场景的类图 注意,IMouseDownListener 是一个 C++ 抽象类。那么,Java 类如何注册事件才不会引入 Java 侦听器和 CEventSource 类之间的编译时绑定呢?在其注册事件后,CEventSource 类如何调用 Java 类中的方法呢?这正是 Java Invocation API 的用武之地。
2.Java Invocation API
Invocation API 让您可以将 JVM 加载到一个本地应用程序中,而不必显式地链接 JVM 源。 通过在 jvm.dll 中调用一个函数,可以创建一个 JVM,jvm.dll 还将当前本地线程连接到 JVM。然后,您可以在 JVM 中从本地线程调用所有 Java 方法。
然而,Invocation API 无法彻底解决问题。您不希望 CEventSource 类具有与 Java 侦听器的编译时依赖关系。另外,Java 侦听器不应该承担使用 JNI 来注册带 CEventSource 的侦听器的责任。
3.代理设计模式
通过使用代理设计模式(Proxy design pattern),可以避免这一弊端。通常来说,代理是另一个对象的占位符。客户端对象可以处理代理对象,而代理封装了所有使用本地方法的细节。代理模式的细节显示在图 3 中:
图 3. 代理设计模式示例
EventSourceProxy 是 CEventSource 类的代理。要将其自身注册为一个侦听器,客户端应该实现 IJMouseDownListener 接口。该接口类似于 IMouseDownListener,但它是用 Java 代码编写的。当第一个客户端调用其 addMouseDownListener() 方法时,它使用 registerListener() 本地方法来将其自身注册为一个带 CEventSource 的侦听器。registerListener() 方法用 C++ 语言来实现。它创建一个 JMouseDownListener 对象,并将其注册为一个带 CEventSource 的侦听器。当 JMouseDownListener 的 onMouseDown 事件被触发时,JMouseDownListener 中的 onMouseDownListener() 方法使用 Invocation API 来通知 EventSourceProxy。 EventSourceProxy 还维持一组使用它来注册的侦听器。无论其 onMouseDown 何时被触发,它将通知该组中的所有侦听器。注意,即使针对该事件存在多个 Java 侦听器,只有代理的一个实例被注册为具有 CEventSource。代理将 onMouseDown 事件委托给它的所有侦听器。这防止了本地代码和 Java 代码之间不必要的上下文切换。
4.多线程问题
本地方法接收 JNI 接口指针作为一个参数。但是,一个想要将事件委托回其关联 Java 代理的本地侦听器没有现成的 JNI 接口指针。一旦获得 JNI 接口指针,应该将其保存起来以便后续使用。
JNI 接口指针只在当前线程中有效。实现 JNI 的 JVM 可以在 JNI 接口指针指向的区域中分配和存储本地线程数据。这意味着您也需要以本地线程数据保存 JNI 接口指针。
JNI 接口指针可以两种方式获得:
- 一旦线程使用 JNI_CreateJavaVM 创建了 JVM,JNI 将接口指针值放在由第二个参数指定的位置。然后该值可以保存在本地线程区域中。
- 如果 JVM 已由进程中某个其他线程创建,当前线程可以调用 AttachCurrentThread。JNI 将接口指针值放在由第一个参数指定的位置。
但是这还没有完。需要记住的是,程序是以 C/C++ 语言编写的,因此,无法使用自动垃圾回收,因为程序不是使用 Java 语言编写的。一旦线程完成 JNI 调用,它需要通过调用 DetachCurrentThread 来释放接口指针。如果未做此调用并且线程存在,进程将无法正常终止。相反,它将一直等待现在已不存在的线程以 DestroyJavaVM 调用的方式从 JVM 中离开。
5.环境设置
公共接口
IMouseDownListener 和 IEventSource 接口定义在 common.h 中。IMouseDownListener 只有一个方法:onMouseDown()。该方法接收鼠标单击的屏幕位置。IEventSource 接口包含了 addMouseDownListener() 和 removeMouseDownListener() 方法,用于注册和取消注册侦听器。
Java Invocation API 的帮助例程
有 7 个必需的常用工具方法可用于简化 Java Invocation API 的使用,它们定义在 Jvm.h 中,在 Jvm.cpp 中实现:
- CreateJavaObject() 创建一个 Java 对象,给出其类名和针对本地 IEventSource 的句柄。这个创建好的 Java 对象将用于侦听来自该本地句柄的事件。该句柄通过其构造方法传递给对象。
- ReleaseJObject() 调用 Java 对象的 release() 方法。该方法用于从 EventSourceProxy 取消注册对象的侦听器。
- DetachThread() 从 JVM 分离当前线程(如果当前线程连接到 JVM)。当线程正被连接时,该调用对于释放特定于线程的、已分配给 JNI 的资源是很必要的。
其余方法都自己附带有解释:
- CreateJVM()
- DestroyJVM()
- GetJVM()
- GetJNIEnv()
CJvm 类还在特定于线程的位置保存一个 JNI 环境指针。这是很有必要的,因为 JNI 环境指针是线程相关的。 本地事件源 在 EventSource.h 和 EventSource.cpp 中,CEventSource 是一个简单而直观的 IEventSource 接口的实现。 本地事件侦听器 在 MouseDownListener.h 和 MouseDownListener.cpp 中,CMouseDownListener 是 IMouseDownListener 接口的实现。该本地侦听器仅出于解释目的而编写。 入口点 main.cpp 包含 main() 和 ThreadMain()。 main() 创建一个本地 EventSource、一个本地侦听器和一个 Java 侦听器。然后创建线程,并在睡眠几秒后让它执行。最后,它释放 Java 侦听器并销毁 JVM。 ThreadMain() 简单地触发一个事件,然后将自身从 JVM 分离出来。
Java 模块
IJMouseDownListener.java 中的 IJMouseDownListener 只是本地接口针对 Java 平台的一个克隆。
MouseDownListener 是 Java 中的一个示例侦听器,在 MouseDownListener.java 中实现。它在其构造方法中接收本地 EventSource 句柄。它定义了一个 release() 方法,该方法取消注册带 EventSourceProxy 的侦听器。
EventSourceProxy 是一个用于来自本地模块的 EventSource 的占位符或代理项。它在 EventSourceProxy.java 中实现。它维持一个静态哈希表,以将一个代理映射到实际 EventSource。
addMouseDownListener() 和 removeMouseDownListener() 允许您维持一个 Java 侦听器集合。单个本地 EventSource 可以有多个 Java 侦听器,但只有在必要时代理才注册/取消注册本地 EventSource。
当从本地 EventSource 转发事件时,EventSourceProxy 的本地实现调用 fireMouseDownEvent()。该方法迭代 Java 侦听器的哈希集合,并通知它们。
EventSourceProxy 的本地部分还维持一个到自身的全局引用。这对于稍后调用 fireMouseDownEvent() 是很必要的。
构建并执行示例代码
示例代码中的所有 Java 类都使用普通过程构建,无需特殊步骤。对于 EventSourceProxy 的本地实现,您需要使用 javah 生成头文件:
javah -classpath .\java\bin events.EventSourceProxy |
为了构建针对 Win32 平台的 C++ 模块,我们提供了 Microsoft Developer Studio 项目文件和 cpp.dsw 工作区。您可以打开工作区,简单地构建 main 项目。工作区中的所有项目都以适当的依赖关系相关联。确保您的 Developer Studio 可以找到 JNI 头和编译时 JNI 库。可以通过选择 Tools > Options > Directories 菜单项完成这一工作。
构建成功之后,在可以执行示例程序之前,还需要完成几个步骤。
首先,因为用于构建 Java 类并包含 JNI 头和库的 JDK 可能有针对 Java Invocation API 的运行时组件,例如 jvm.dll,您必需设置它。最简单的方法是更新 PATH 变量。
其次,main 程序带有命令行参数,这些参数是简单的 JVM 参数。您需要至少传递两个参数给 JVM:
main.exe "-Djava.class.path=.\\java\\bin" "-Djava.library.path=.\\cpp\\listener\\Debug" |
得到的控制台输出如下:
In CMouseDownListener::onMouseDown X = 50 Y = 100 In MouseDownListener.onMouseDown X = 50 Y = 100 |
正如您从控制台输出所看到的,Java 侦听器产生与出于解释目的而构建的本地侦听器相同的结果。
结束语
本文展示了如何为本地应用程序生成的事件注册一个 Java 类作为侦听器。通过使用观察者设计模式,您已经减少了事件源与侦听器之间的耦合。您还通过使用代理设计模式隐藏了来自 Java 侦听器的事件源的实现细节。您可以使用该设计模式组合来将一个 Java UI 添加到现有的本地应用程序。
Ch23: JNI and Thread
The Java virtual machine supports multiple threads of control concurrently executing in the same address space. This concurrency introduces a degree of complexity that you do not have in a single-threaded environment. Multiple threads may access the same objects, the same file descriptors--in short, the same shared resources--at the same time.
To get the most out of this section, you should be familiar with the concepts of multithreaded programming. You should know how to write Java applications that utilize multiple threads and how to synchronize access of shared resources. A good reference on multithreaded programming in the Java programming language is Concurrent Programming in Java TM, Design Principles and Patterns , by Doug Lea (Addison-Wesley, 1997).
1.1 Constraints
There are certain constraints that you must keep in mind when writing native methods that are to run in a multithreaded environment. By understanding and programming within these constraints, your native methods will execute safely no matter how many threads simultaneously execute a given native method. For example:
- A
JNIEnv
pointer is only valid in the thread associated with it. You must not pass this pointer from one thread to another, or cache and use it in multiple threads. The Java virtual machine passes a native method the sameJNIEnv
pointer in consecutive invocations from the same thread, but passes differentJNIEnv
pointers when invoking that native method from different threads. Avoid the common mistake of caching theJNIEnv
pointer of one thread and using the pointer in another thread. - Local references are valid only in the thread that created them. You must not pass local references from one thread to another. You should always convert local references to global references whenever there is a possibility that multiple threads may use the same reference.
1.2 Monitor Entry and Exit
Monitors are the primitive synchronization mechanism on the Java platform. Each object can be dynamically associated with a monitor. The JNI allows you to synchronize using these monitors, thus implementing the functionality equivalent to a synchronized block in the Java programming language:
synchronized (obj) {
... // synchronized block
}
The Java virtual machine guarantees that a thread acquires the monitor associated with the object obj
before it executes any statements in the block. This ensures that there can be at most one thread that holds the monitor and executes inside the synchronized block at any given time. A thread blocks when it waits for another thread to exit a monitor.
Native code can use JNI functions to perform equivalent synchronization on JNI references. You can use the MonitorEnter
function to enter the monitor and the MonitorExit
function to exit the monitor:
if ((*env)->MonitorEnter(env, obj) != JNI_OK) {
... /* error handling */
}
... /* synchronized block */
if ((*env)->MonitorExit(env, obj) != JNI_OK) {
... /* error handling */
};
Executing the code above, a thread must first enter the monitor associated with obj
before executing any code inside the synchronized block. The Monitor-Enter
operation takes a jobject
as an argument and blocks if another thread has already entered the monitor associated with the jobject
. Calling MonitorExit
when the current thread does not own the monitor results in an error and causes an Illegal-MonitorStateException
to be raised. The above code contains a matched pair of MonitorEnter
and MonitorExit
calls, yet we still need to check for possible errors. Monitor operations may fail if, for example, the underlying thread implementation cannot allocate the resources necessary to perform the monitor operation.
MonitorEnter
and MonitorExit
work on jclass
, jstring
, and jarray
types, which are special kinds of jobject
references.
Remember to match a MonitorEnter
call with the appropriate number of MonitorExit
calls, especially in code that handles errors and exceptions:
if ((*env)->MonitorEnter(env, obj) != JNI_OK) ...;
...
if ((*env)->ExceptionOccurred(env)) {
... /* exception handling */
/* remember to call MonitorExit here */
if ((*env)->MonitorExit(env, obj) != JNI_OK) ...;
}
... /* Normal execution path.
if ((*env)->MonitorExit(env, obj) != JNI_OK) ...;
Failure to call MonitorExit
will most likely lead to deadlocks. By comparing the above C code segment with the code segment at the beginning of this section, you can appreciate how much easier it is to program with the Java programming language than with the JNI. Thus, it is preferable to express synchronization constructs in the Java programming language. If, for example, a static native method needs to enter the monitor associated with its defining class, you should define a static synchronized native method as opposed to performing JNI-level monitor synchronization in native code.
1.3 Monitor Wait and Notify
The Java API contains several other methods that are useful for thread synchronization. They are Object.wait
, Object.notify
, and Object.notifyAll
. No JNI functions are supplied that correspond directly to these methods because monitor wait and notify operations are not as performance critical as monitor enter and exit operations. Native code may instead use the JNI method call mechanism to invoke the corresponding methods in the Java API:
/* precomputed method IDs */
static jmethodID MID_Object_wait;
static jmethodID MID_Object_notify;
static jmethodID MID_Object_notifyAll;
void
JNU_MonitorWait(JNIEnv *env, jobject object, jlong timeout)
{
(*env)->CallVoidMethod(env, object, MID_Object_wait,
timeout);
}
void
JNU_MonitorNotify(JNIEnv *env, jobject object)
{
(*env)->CallVoidMethod(env, object, MID_Object_notify);
}
void
JNU_MonitorNotifyAll(JNIEnv *env, jobject object)
{
(*env)->CallVoidMethod(env, object, MID_Object_notifyAll);
}
We assume that the method IDs for Object.wait
, Object.notify
, and Object.notifyAll
have been calculated elsewhere and are cached in the global variables. Like in the Java programming language, you can call the above monitor-related functions only when holding the monitor associated with the jobject
argument.
1.4 Obtaining a JNIEnv Pointer in Arbitrary Contexts
We explained earlier that a JNIEnv
pointer is only valid in its associated thread. This is generally not a problem for native methods because they receive the JNIEnv
pointer from the virtual machine as the first argument. Occasionally, however, it may be necessary for a piece of native code not called directly from the virtual machine to obtain the JNIEnv
interface pointer that belongs to the current thread. For example, the piece of native code may belong to a "callback" function called by the operating system, in which case the JNIEnv
pointer will probably not be available as an argument.
You can obtain the JNIEnv
pointer for the current thread by calling the AttachCurrentThread
function of the invocation interface:
JavaVM *jvm ; /* already set */
f()
{
JNIEnv *env;
(*jvm )->AttachCurrentThread(jvm , (void **)&env, NULL);
... /* use env */
}
When the current thread is already attached to the virtual machine, Attach-Current-Thread
returns the JNIEnv
interface pointer that belongs to the current thread.
There are many ways to obtain the JavaVM
pointer: by recording it when the virtual machine is created, by querying for the created virtual machines using JNI_GetCreatedJavaVMs
, by calling the JNI function GetJavaVM
inside a regular native method, or by defining a JNI_OnLoad
handler. Unlike the JNIEnv
pointer, the JavaVM
pointer remains valid across multiple threads so it can be cached in a global variable.
Java 2 SDK release 1.2 provides a new invocation interface function GetEnv
so that you can check whether the current thread is attached to the virtual machine, and, if so, to return the JNIEnv
pointer that belongs to the current thread. GetEnv
and AttachCurrentThread
are functionally equivalent if the current thread is already attached to the virtual machine.
1.5 Matching the Thread Models
Suppose that native code to be run in multiple threads accesses a global resource. Should the native code use JNI functions MonitorEnter
and MonitorExit
, or use the native thread synchronization primitives in the host environment (such as mutex_lock
on Solaris)? Similarly, if the native code needs to create a new thread, should it create a java.lang.Thread
object and perform a callback of Thread.start
through the JNI, or should it use the native thread creation primitive in the host environment (such as thr_create
on Solaris)?
The answer is that all of these approaches work if the Java virtual machine implementation supports a thread model that matches that used by the native code. The thread model dictates how the system implements essential thread operations such as scheduling, context switching, synchronization, and blocking in system calls. In a native thread model the operating system manages all the essential thread operations. In a user thread model, on the other hand, the application code implements the thread operations. For example, the "Green thread" model shipped with JDK and Java 2 SDK releases on Solaris uses the ANSI C functions setjmp
and longjmp
to implement context switches.
Many modern operating systems (such as Solaris and Win32) support a native thread model. Unfortunately, some operating systems still lack native thread support. Instead, there may be one or many user thread packages on these operating systems.
If you write application strictly in the Java programming language, you need not worry about the underlying thread model of the virtual machine implementation. The Java platform can be ported to any host environment that supports the required set of thread primitives. Most native and user thread packages provide the necessary thread primitives for implementing a Java virtual machine.
JNI programmers, on the other hand, must pay attention to thread models. The application using native code may not function properly if the Java virtual implementation and the native code have a different notion of threading and synchronization. For example, a native method could be blocked in a synchronization operation in its own thread model, but the Java virtual machine, running in a different thread model, may not be aware that the thread executing the native method is blocked. The application deadlocks because no other threads will be scheduled.
The thread models match if the native code uses the same thread model as the Java virtual machine implementation. If the Java virtual machine implementation uses native thread support, the native code can freely invoke thread-related primitives in the host environment. If the Java virtual machine implementation is based on a user thread package, the native code should either link with the same user thread package or rely on no thread operations at all. The latter may be harder to achieve than you think: most C library calls (such as I/O and memory allocation functions) perform thread synchronization underneath. Unless the native code performs pure computation and makes no library calls, it is likely to use thread primitives indirectly.
Most virtual machine implementations support only a particular thread model for JNI native code. Implementations that support native threads are the most flexible, hence native threads, when available, are typically preferred on a given host environment. Virtual machine implementations that rely on a particular user thread package may be severely limited as to the type of native code with which they can operate.
Some virtual machine implementations may support a number of different thread models. A more flexible type of virtual machine implementation may even allow you to provide a custom thread model implementation for virtual machine's internal use, thus ensuring that the virtual machine implementation can work with your native code. Before embarking on a project likely to require native code, you should consult the documentation that comes with your virtual machine implementation for thread model limitations.
8.4 Load and Unload Handlers
Load and unload handlers allow the native library to export two functions: one to be called when System.loadLibrary
loads the native library, the other to be called when the virtual machine unloads the native library. This feature was added in Java 2 SDK release 1.2.
8.4.1 The JNI_OnLoad
Handler
When System.loadLibrary
loads a native library, the virtual machine searches for the following exported entry in the native library:
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm , void *reserved);
You can invoke any JNI functions in an implementation of JNI_Onload
. A typical use of the JNI_OnLoad
handler is caching the JavaVM
pointer, class references, or method and field IDs, as shown in the following example:
JavaVM *cached_jvm ;
jclass Class_C;
jmethodID MID_C_g;
JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM *jvm , void *reserved)
{
JNIEnv *env;
jclass cls;
cached_jvm = jvm ; /* cache the JavaVM pointer */
if ((*jvm )->GetEnv (jvm , (void **)&env, JNI_VERSION_1_2)) {
return JNI_ERR; /* JNI version not supported */
}
cls = (*env)->FindClass(env, "C");
if (cls == NULL) {
return JNI_ERR;
}
/* Use weak global ref to allow C class to be unloaded */
Class_C = (*env)->NewWeakGlobalRef(env, cls);
if (Class_C == NULL) {
return JNI_ERR;
}
/* Compute and cache the method ID */
MID_C_g = (*env)->GetMethodID(env, cls, "g", "()V");
if (MID_C_g == NULL) {
return JNI_ERR;
}
return JNI_VERSION_1_2;
}
The JNI_OnLoad
function first caches the JavaVM
pointer in the global variable cached_jvm
. It then obtains the JNIEnv
pointer by calling GetEnv
. It finally loads the C
class, caches the class reference, and computes the method ID for C.g
. The JNI_OnLoad
function returns JNI_ERR
(§12.4 ) on error and otherwise returns the JNIEnv
version JNI_VERSION_1_2
needed by the native library.
We will explain in the next section why we cache the C
class in a weak global reference instead of a global reference.
Given a cached JavaVM
interface pointer it is trivial to implement a utility function that allows the native code to obtain the JNIEnv
interface pointer for the current thread (§8.1.4 ) :
JNIEnv *JNU_GetEnv ()
{
JNIEnv *env;
(*cached_jvm )->GetEnv (cached_jvm ,
(void **)&env,
JNI_VERSION_1_2);
return env;
}
8.4.2 The JNI_OnUnload
Handler
Intuitively, the virtual machine calls the JNI_OnUnload
handler when it unloads a JNI native library. This is not precise enough, however. When does the virtual machine determine that it can unload a native library? Which thread runs the JNI_OnUnload
handler?
The rules of unloading native libraries are as follows:
- The virtual machine associates each native library with the class loader
L
of the classC
that issues theSystem.loadLibrary
call. - The virtual machine calls the
JNI_OnUnload
handler and unloads the native library after it determines that the class loaderL
is no longer a live object. Because a class loader refers to all the classes it defines, this implies thatC
can be unloaded as well. - The
JNI_OnUnload
handler runs in a finalizer, and is either invoked synchroniously byjava.lang.System.runFinalization
or invoked asynchronously by the virtual machine.
Here is the definition of a JNI_OnUnload
handler that cleans up the resources allocated by the JNI_OnLoad
handler in the last section:
JNIEXPORT void JNICALL
JNI_OnUnload(JavaVM *jvm , void *reserved)
{
JNIEnv *env;
if ((*jvm )->GetEnv (jvm , (void **)&env, JNI_VERSION_1_2)) {
return;
}
(*env)->DeleteWeakGlobalRef(env, Class_C);
return;
}
The JNI_OnUnload
function deletes the weak global reference to the C
class created in the JNI_OnLoad
handler. We need not delete the method ID MID_C_g
because the virtual machine automatically reclaims the resources needed to represent C
's method IDs when unloading its defining class C
.
We are now ready to explain why we cache the C
class in a weak global reference instead of a global reference. A global reference would keep C
alive, which in turn would keep C
's class loader alive. Given that the native library is associated with C
's class loader L
, the native library would not be unloaded and JNI_OnUnload
would not be called.
The JNI_OnUnload
handler runs in a finalizer. In contrast, the JNI_OnLoad
handler runs in the thread that initiates the System.loadLibrary
call. Because JNI_OnUnload
runs in an unknown thread context, to avoid possible deadlocks, you should avoid complex synchronization and locking operations in JNI_OnUnload
. The JNI_OnUnload
handler typically carries out simple tasks such as releasing the resources allocated by the native library.
The JNI_OnUnload
handler runs when the class loader that loaded the library and all classes defined by that class loader are no longer alive. The JNI_OnUnload
handler must not use these classes in any way. In the above JNI_OnUnload
definition, you must not perform any operations that assume Class_C
still refers to a valid class. The DeleteWeakGlobalRef
call in the example frees the memory for the weak global reference itself, but does not manipulate the referred class C
in any way.
In summary, you should be careful when writing JNI_OnUnload
handlers. Avoid complex locking operations that may introduce deadlocks. Keep in mind that classes have been unloaded when the JNI_OnUnload
handler is invoked.
8.5 Reflection Support
Reflection generally refers to manipulating language-level constructs at runtime. For example, reflection allows you to discover at run time the name of arbitrary class objects and the set of fields and methods defined in the class. Reflection support is provided at the Java programming language level through the java.lang.reflect
package as well as some methods in the java.lang.Object
and java.lang.Class
classes. Although you can always call the corresponding Java API to carry out reflective operations, the JNI provides the following functions to make the frequent reflective operations from native code more efficient and convenient:
-
GetSuperclass
returns the superclass of a given class reference. -
IsAssignableFrom
checks whether instances of one class can be used when instances of another class are expected. -
GetObjectClass
returns the class of a givenjobject
reference. -
IsInstanceOf
checks whether ajobject
reference is an instance of a given class. -
FromReflectedField
andToReflectedField
allow the native code to convert between field IDs andjava.lang.reflect.Field
objects. They are new additions in Java 2 SDK release 1.2. -
FromReflectedMethod
andToReflectedMethod
allow the native code to convert between method IDs,java.lang.reflect.Method
objects andjava.lang.reflect.Constructor
objects. They are new additions in Java 2 SDK release 1.2.
Ch24:测量Java应用程序的CPU和内存占用率
测量CPU和内存的占用率常常是检查Java 应用程序是否达到特定性能的一个重要环节。尽管Java 提 供了一些重要的方法用于测量其堆栈大小,但是使用标准的API是无法测量本机Java进程的大小和 CPU当前的使用率的。这种测量的结果对于开发人员来说非常重要,它会提供应用程序的实时性能和效率信息。不幸的是,这样的信息只能从操作系统直接获取, 而这已经超出了Java标准的可移植能力。
一个主要的解决方案是使用操作系统自带的本机系统调用,将数据通过JNI(Java Native Interface,Java本机接口)传输给Java.与调用各个平台专用的外部命令(比如ps)并分析输出结果不同,这种方案始终是一种很可靠的方 式。以前碰到这样的问题时,我尝试过使用Vladimir Roubtsov自己编写的一个很小的库,它只能在Win32系统下测量进程的CPU占用率。但是,这个库的能力十分有限,所以我需要某种方式能够同时在Windows 和Solaris平台上测量CPU和内存的占用率。
我扩展了这个库的能力,在Windows 和Solaris 8平台上实现了所有功能。新的库能够测量纯CPU使用时间、CPU使用的百分比、本机剩余内存和已经使用的内存、Java进程的本机内存大小、系统信息 (比如操作系统的名字、补丁程序、硬件信息等)。它由三部分实现: Java通用的部分、Windows实现,以及Solaris实现。依靠操作系统的部分用纯C语言实现。
编辑提示:本文可以下载 ,所有的源代码都以单独的文本方式列出来了。
库
所以,我们将创建一个简单的JNI库,用于同C层里的操作系统进行沟通,并把生成的数据提供给Java应用程序。首先,我们要创建一个 SystemInformation类(列表A),为测量和记录CPU的使用率和其他与系统相关的信息提供一个简单的API.这个类是抽象的,因为它公开 的是一个完全静态的API.
列表A
<PRE>package com.vladium.utils; public abstract class SystemInformation // Custom exception class for throwing /** /** /** /** /** /** /** /** /** /** /** /** // protected: ............................................................. // package: ............................................................... // private: ............................................................... } // end of class |
最重要的方法是getProcessCPUTime(),它会返回当前进程的CPU时间(内核和用户)的毫秒数,或者是PID在初始化期间被传送给本机库 的进程所消耗的CPU时间。返回的值应该根据系统的处理器的个数进行调整;下面我们来看看这是如何在本机代码里做到的。我们用修改符native来声明这 个方法,这意味着它必须在JNI代码里实现。getProcessCPUTime()方法用来给CPU的使用率数据进行快照,方式是将经过测量的CPU时 间与当前的系统时间进行关联。这样的数据库快照在makeCPUUsageSnapshot()方法里进行,并输出一个CPUUsageSnapshot 容器对象。这样,测量CPU使用率的原理就很容易理解了:我们按照给定的时间间隔进行两次CPU快照,按1.0的分数来计算两个时间点之间的CPU使用 率,方式是两点所在的CPU时间差除以两点所在系统时间差。下面就是getProcessCPUUsage()方法的工作原理:
public static double getProcessCPUUsage (final CPUUsageSnapshot start, final CPUUsageSnapshot end) |
只要我们知道分母里的快照之间的时间间隔,以及分子里进程花在活动状态上的CPU时间,我们就会得到所测量的时间间隔过程中进程的CPU使用率;1.0就代表所有处理器100%的使用率。
事实上这种方式可以用在所有版本的UNIX的ps工具和Windows的任务管理器上,这两个都是用于监视特定进程的CPU使用率的程序。很显然,时间间 隔越长,我们得到的结果就越平均、越不准确。但是最小时差应该被强制输入getProcessCPUUsage()。这种限制的原因是某些系统上的 System.currentTimeMillis()的解析度很低。Solaris 8操作系统提供了一个系统调用,用于从内核表里直接获得CPU使用率。出于这个目的,我们拥有的getProcessCPUPercentage()方法 会以百分比的形式返回进程所使用的CPU时间。如果这个特性不被操作系统支持(比如在Windows下),那么JNI库就会根据我们应用程序的设计返回一 个负值。
还有其他一些本机声明要在本机部分实现:
getCPUs()用来返回机器上处理器的个数
getMaxMem()用来返回系统上可用的最大物理内存
getFreeMem()用来返回系统上当前可用内存
getSysInfo()用来返回系统信息,包括一些硬件和操作系统的详细信息
getMemoryUsage()用来返回分配给进程的空间,以KB为单位(这些页面文件可能在内存里,也有可能不在内存里)
getMemoryResident()用来返回当前进程驻留在内存里的空间,以KB为单位。
所有这些方法对于不同的Java开发人员来说常常都是非常有用的。为了确保本机JNI库被调入内存并在调用任何本机方法之前被初始化,这个库被加载到一个静态初始值里:
static |
在初始化一个.dll(或者.so)库之后,我们可以直接使用本机声明的方法:
final SystemInformation.CPUUsageSnapshot m_prevSnapshot = |
现在让我们来分别看看针对Windows和Solaris的JNI本机实现。C头文件silib.h(列表B)能够用JDK里的Javah工具生成,或者手动编写。
列表B
/* DO NOT EDIT THIS FILE - it is machine generated */ #ifndef _Included_com_vladium_utils_SystemInformation /* /* /* /* /* #ifdef __cplusplus |
Windows
首先我们来看看Windows的实现(列表C)。
列表C
/* ------------------------------------------------------------------------- */ #include #include "com_vladium_utils_SystemInformation.h" static jint s_PID; #define INFO_BUFFER_SIZE 32768
/* _time.LowPart = time->dwLowDateTime; return _time.QuadPart; /* s_PID = _getpid (); GetSystemInfo (& systemInfo); return JNI_VERSION_1_2; JNIEXPORT void JNICALL if (!alreadyDetached && s_currentProcess!=NULL) { } /* /*
// Test for the specific product. if ( osvi.dwMajorVersion == 5 && osvi.dwMinorVersion == 1 ) if ( osvi.dwMajorVersion == 5 && osvi.dwMinorVersion == 0 ) if ( osvi.dwMajorVersion <= 4 ) // Test for specific product on Windows NT 4.0 SP6 and later. // Test for the server type. else if( osvi.dwMajorVersion == 5 && osvi.dwMinorVersion == 0 ) else // Windows NT 4.0 lRet = RegOpenKeyEx( HKEY_LOCAL_MACHINE, lRet = RegQueryValueEx( hKey, "ProductType", NULL, NULL, RegCloseKey( hKey ); if ( lstrcmpi( "WINNT", szProductType) == 0 ) sprintf(buf2, "%d.%d ", (int)osvi.dwMajorVersion, (int)osvi.dwMinorVersion ); // Display service pack (if any) and build number. if( osvi.dwMajorVersion == 4 && // Test for SP6 versus SP6a. RegCloseKey( hKey );
// Test for the Windows Me/98/95. if (osvi.dwMajorVersion == 4 && osvi.dwMinorVersion == 0) if (osvi.dwMajorVersion == 4 && osvi.dwMinorVersion == 10) if (osvi.dwMajorVersion == 4 && osvi.dwMinorVersion == 90) case VER_PLATFORM_WIN32s: strcat(buf,"Win32s "); next_label: strcat(buf," next_label_2: lRet = RegQueryValueEx( hKey, "ProcessorNameString", NULL, NULL, strcat(buf,")"); jstring retval = (*env)->NewStringUTF(env,buf);
/* printf("[CPUmon] Could not attach to native process. /* /* BOOL resultSuccessful = GetProcessTimes (s_currentProcess, & creationTime, & exitTime, & kernelTime, & userTime); printf("[CPUmon] An error occured while trying to get CPU time. fflush(stdout); return (jlong) ((fileTimeToInt64 (& kernelTime) + fileTimeToInt64 (& userTime)) / /* /*
/* BOOL resultSuccessful = GetProcessTimes (s_currentProcess, & creationTime, & exitTime, & kernelTime, & userTime); printf("[CPUmon] An error occured while trying to get CPU time. /* elapsedTime = fileTimeToInt64 (& nowTime) - fileTimeToInt64 (& creationTime); if (elapsedTime < MIN_ELAPSED_TIME) /* /* if ( GetProcessMemoryInfo( s_currentProcess, &pmc, sizeof(pmc)) ) /* if ( GetProcessMemoryInfo( s_currentProcess, &pmc, sizeof(pmc)) )
/* ------------------------------------------------------------------------- */ |
JNI里有两个特殊的函数——JNI_OnLoad和JNI_OnUnload,它们分别在加载和卸载库的时候被调用。JNI_OnLoad在调用其他任 何方法之前被执行,而且能够很方便地用于初始化在这一进程的生命周期中没有发生变化的变量,并用于协调JNI规范的版本。在默认情况下,库会测量它自己的 进程的参数,但是通过调用systemInformation.setPid()方法它可以从Java应用程序被重载。s_PID C变量用来保存PID,而s_currentProcess用来保存进程句柄(用于Windows的是HANDLE类型,而用于Solaris的是int 类型)。为了读取的一些参数,应该首先打开进程句柄,我们需要在库关闭使用的时候停止同一个进程句柄(通常它在JVM因为相同的原因而关闭的时候发生)。 这就是JNI_OnUnload()函数起作用的地方。但是,JVM的一些实现事实上没有调用JNI_OnUnload(),还有发生句柄会永远打开的危 险。为了降低这种可能性,我们应该在Java应用程序里加入一个明确调用detachProcess() C函数的关闭挂钩。下面就是我们加入关闭挂钩的方法:
if (pid!=-1) { |
通过调用WinAPI里的GetSystemInfo(),我们还可以获得关于中央处理器的一些信息。只要它是CPU的占用率根据这个值来调节,测量进程 最重要的值就是处理器的个数(s_numberOfProcessors)。SystemInformation.getSysInfo()的Win32 实现相当麻烦,因为在各个版本的Windows里,关于操作系统的版本、补丁、服务包以及相关硬件等信息被以不同的方式保存。所以需要读者来分析相关的源 代码和代码中的注释。下面就是Windows XP上代码输出的示例:
System.out.println("SysInfo: ”+SystemInformation.getSysInfo()): And the same code on Solaris will give: SysInfo: SunOS 5.8 sxav-dev Generic_108528-29 sun4u sparc |
为了获得CPU上进程所占用的总时间,我们使用了WinAPI的函数GetProcessTimes.其他的函数实现都非常简单,直接调用WinAPI, 所以没有什么必要讨论它们。列表D里是用于Windows版本的GCC的make.bat文件,用来帮助读者创建相关的。dll库。
列表D
gcc -D_JNI_IMPLEMENTATION_ -Wl,——kill-at -IC:/jdk1.3.1_12/include -IC:/jdk1.3.1_12/include/win32 -shared C:/cpu_profile/src/native/com_vladium_utils_SystemInformation.c -o C:/cpu_profile/dll/silib.dll C:/MinGW/lib/libpsapi.a 这个库的Solaris实现见列表E和列表F.这两个都是C语言文件,应该被编译到一个共享库(.so)里。用于编译共享库的帮助器make.sh见列表 G.所有基于Solaris系统的调用被移到列表F里,这使得列表E就是一个JNI的简单包装程序。Solaris实现要比Win32实现更加复杂,要求 更多的临时数据结构、内核和进程表。列出的代码里有更多的注释。
列表E
/* ------------------------------------------------------------------------- */ #include #include "com_vladium_utils_SystemInformation.h" // Helper Solaris8-dependent external routines static jint s_PID; /* ------------------------------------------------------------------------- */ /* ......................................................................... */ /* /* use kstat to update all processor information */ return JNI_VERSION_1_2; /* /*
/* /* * Class: com_vladium_utils_SystemInformation * Method: detachProcess * Signature: ()I */ JNIEXPORT jint JNICALL Java_com_vladium_utils_SystemInformation_detachProcess (JNIEnv * env, jclass cls) { if (externalCPUmon && !alreadyDetached) { alreadyDetached = 1; /* /* /*
/* /*
/*
#undef MIN_ELAPSED_TIME /* ------------------------------------------------------------------------- */ #include #define _STRUCTURED_PROC 1
static struct nlist nlst[] = }; static kstat_ctl_t *kc = NULL; /* pagetok function is really a pointer to an appropriate function */ int pagetok_none(int size) { int pagetok_left(int size) { int pagetok_right(int size) { #define UPDKCID(nk,ok) void initKVM() { /* perform the kvm_open - suppress error here */ /* calculate pageshift value */ /* calculate an amount to shift to K values */ /* now determine which pageshift function is appropriate for the #define SI_LEN 512 char * sol_getSysInfo() { for (ks = kc->kc_chain; ks; cpu_ks[ncpu] = ks; /* return the number of cpus found */ unsigned long sol_getMaxMem() { unsigned long sol_getFreeMem() { // Returns the number of milliseconds (not nanoseconds and seconds) elapsed on processor // Returns percentage CPU by pid // Returns current space allocated for the process, in bytes. Those pages may or may not be in memory. // Returns current process space being resident in memory. /* |
列表G
#!/bin/sh |
在本文里,我已告诉你如何编程测量Java进程的内存和CPU占用率。当然用户可以通过查看Windows任务管理器或者ps的输出来达到相同的目的,但是重要的一点是,用户现在能够进行任何长期运行和/或自动的软件性能测试 ,这对于开发一个分布式或者可伸缩的,或者实时的性能关键的应用程序十分重要。加入获取系统软件和硬件信息的能力对于开发人员同样十分有用。这个库可以在Win32和Solaris平台上实现。但是,把它移植到其他UNIX平台上应该也不是问题,比如Linux.
Ch25:C++调用JAVA方法详解
本文主要参考http://tech.ccidnet.com/art/1081/20050413/237901_1.html 上的文章。
?
C++ 调用JAVA主要用到了SUN公司的JNI技术, JNI是Java Native Interface的 缩写。从Java 1.1开始,Java Native Interface (JNI)标准成为java平台的一部分,它允许Java代码和其他语言写的代码进行交互。相关资料见http://java.sun.com/j2se/1.5.0/docs/guide/jni/spec/jniTOC.html
?
开发环境安装及配置
?
1.1? 安装JDK
??????? 到SUN公司网站可以下载到最新版的JDK。下载下来后开始安装,一路选择默认配置即可,本文档中假定安装的是JDK1.4,安装目录为C:\j2sdk1.4.2_15。
?
1.2? 配置VC6.0
???????? 通过Visual C++ 6的菜单Tools→Options打开选项对话框。在Directories标签页下添加JDK的相关目录到Include和目录下。
????????????
?
?
?开发测试用到的JAVA类
2.1? 开发JAVA类
??????? 在硬盘的任意地方新建一个名叫test的文件夹,本文档示例中将test文件夹建立在C盘根目录,然后在里面新建一个名称叫Demo.java的JAVA文件,将下面测试用的代码粘贴到该文件中。
?
?
package test;
/**
* 该类是为了演示JNI如何访问各种对象属性等
*/
public class Demo
{
//用于演示如何访问静态的基本类型属性
public static int COUNT = 8;
//演示对象型属性
private String msg;
private int[] counts;
public Demo()
{
this("缺省构造函数");
}
/**
* 演示如何访问构造器
*/
public Demo(String msg)
{
this.msg = msg;
this.counts = null;
}
public String getMessage()
{
return msg;
}
/**
* 该方法演示如何访问一个静态方法
*/
public static String getHelloWorld()
{
return "Hello world!";
}
/**
* 该方法演示参数的传入传出及中文字符的处理
*/
public String append(String str, int i)
{
return str + i;
}
/**
* 演示数组对象的访问
*/
public int[] getCounts()
{
return counts;
}
/**
* 演示如何构造一个数组对象
*/
public void setCounts(int[] counts)
{
this.counts = counts;
}
/**
* 演示异常的捕捉
*/
public void throwExcp()throws IllegalAccessException
{
throw new IllegalAccessException("exception occur.");
}
}
?
2.2 编译JAVA类
????? 运行CMD控制台程序进入命令行模式,输入命令javac -classpath c:\ c:\test\Demo.java,-classpath参数指定classpath的路径,这里就是test目录所在的路径。(注意:如果你没有将 JDK的环境变量设置好,就需要先进入JDK的bin目录下,如下图所示。)
?
2.3 查看方法的签名
????? 我们知道Java中允许方法的多态,仅仅是通过方法名并没有办法定位到一个具体的方法,因此需要一个字符串来唯一表示一个方法。但是怎么利用一个字符串来 表示方法的具体定义呢?JDK中已经准备好一个反编译工具javap,通过这个工具就可以得到类中每个属性、方法的签名。在CMD下运行javap -s -p -classpath c:\ test.Demo即可看到属性和方法的签名。如下图红色矩形框起来的字符串为方法String append(String str, int i)的签名。
?
?
在VC中调用JAVA类
?
3.1 快速调用JAVA中的函
????? 在VC中新建一个控制台程序,然后新建一个CPP文件,将下面的代码添加到该文件中。运行该文件,即可得到Demo类中String append(String str, int i)函数返回的字符串。
#include "windows.h"
#include "jni.h"
#include <string>
#include <iostream>
using namespace std;
jstring NewJString(JNIEnv *env, LPCTSTR str);
string JStringToCString (JNIEnv *env, jstring str);
int main()
{
//定义一个函数指针,下面用来指向JVM中的JNI_CreateJavaVM函数
typedef jint (WINAPI *PFunCreateJavaVM)(JavaVM **, void **, void *);
int res;
JavaVMInitArgs vm_args;
JavaVMOption options[3];
JavaVM *jvm;
JNIEnv *env;
/*设置初始化参数*/
//disable JIT,这是JNI文档中的解释,具体意义不是很清楚 ,能取哪些值也不清楚。
//从JNI文档里给的示例代码中搬过来的
options[0].optionString = "-Djava.compiler=NONE";
//设置classpath,如果程序用到了第三方的JAR包,也可以在这里面包含进来
options[1].optionString = "-Djava.class.path=.;c:\\";
//设置显示消息的类型,取值有gc、class和jni,如果一次取多个的话值之间用逗号格开,如-verbose:gc,class
//该参数可以用来观察C++调用JAVA的过程,设置该参数后,程序会在标准输出设备上打印调用的相关信息
options[2].optionString = "-verbose:NONE";
//设置版本号,版本号有JNI_VERSION_1_1,JNI_VERSION_1_2和JNI_VERSION_1_4
//选择一个根你安装的JRE版本最近的版本号即可,不过你的JRE版本一定要等于或者高于指定的版本号
vm_args.version = JNI_VERSION_1_4;
vm_args.nOptions = 3;
vm_args.options = options;
//该参数指定是否忽略非标准的参数,如果填JNI_FLASE,当遇到非标准参数时,JNI_CreateJavaVM会返回JNI_ERR
vm_args.ignoreUnrecognized = JNI_TRUE;
//加载JVM.DLL动态库
HINSTANCE hInstance = ::LoadLibrary("C:\\j2sdk1.4.2_15\\jre\\bin\\client\\jvm.dll");
if (hInstance == NULL)
{
return false;
}
//取得里面的JNI_CreateJavaVM函数指针
PFunCreateJavaVM funCreateJavaVM = (PFunCreateJavaVM)::GetProcAddress(hInstance, "JNI_CreateJavaVM");
//调用JNI_CreateJavaVM创建虚拟机
res = (*funCreateJavaVM)(&jvm, (void**)&env, &vm_args);
if (res < 0)
{
return -1;
}
//查找test.Demo类,返回JAVA类的CLASS对象
jclass cls = env->FindClass("test/Demo");
//根据类的CLASS对象获取该类的实例
jobject obj = env->AllocObject(cls);
//获取类中的方法,最后一个参数是方法的签名,通过javap -s -p 文件名可以获得
jmethodID mid = env->GetMethodID(cls, "append","(Ljava/lang/String;I)Ljava/lang/String;");
//构造参数并调用对象的方法
const char szTest[] = "电信";
jstring arg = NewJString(env, szTest);
jstring msg = (jstring) env->CallObjectMethod(obj, mid, arg, 12);
cout<<JStringToCString(env, msg);
//销毁虚拟机并释放动态库
jvm->DestroyJavaVM();
::FreeLibrary(hInstance);
return 0;
}
string JStringToCString (JNIEnv *env, jstring str)// (jstring str, LPTSTR desc, int desc_len)
{
if(str==NULL)
{
return "";
}
//在VC中wchar_t是用来存储宽字节字符(UNICODE)的数据类型
int len = env->GetStringLength(str);
wchar_t *w_buffer = new wchar_t[len+1];
char *c_buffer = new char[2*len+1];
ZeroMemory(w_buffer,(len+1)*sizeof(wchar_t));
//使用GetStringChars 而不是GetStringUTFChars
const jchar * jcharString = env->GetStringChars (str, 0);
wcscpy(w_buffer,jcharString);
env->ReleaseStringChars(str,jcharString);
ZeroMemory(c_buffer,(2*len+1)*sizeof(char));
/调用字符编码转换函数(Win32 API)将UNICODE转为ASCII编码格式字符串
len = WideCharToMultiByte(CP_ACP,0,w_buffer,len,c_buffer,2*len,NULL,NULL);
string cstr = c_buffer;
delete[] w_buffer;
delete[] c_buffer;
return cstr;
}
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;
}
?
3.2 调用步骤分析及注意事项
?
???? a、加载jvm.dll动态库,然后获取里面的JNI_CreateJavaVM函数。这个步骤也可以通过在VC工程的LINK标签页里添加对 jvm.lib的连接,然后在环境变量里把jvm.dll所在的路径加上去来实现。但后面这种方法在部署的时候会比前一个方法麻烦。
???? b、利用构造好的参数,调用JNI_CreateJavaVM函数创建JVM。JNI_CreateJavaVM函数内部会自动根据jvm.dll的路径 来获取JRE的环境,所以千万不要把jvm.dll文件拷贝到别的地方,然后再通过LoadLibrary函数导入。
???? c、JVM创建成功后,JNI_CreateJavaVM函数会传出一个JNI上下文环境对象(JNIEnv),利用该对象的相关函数就可以调用JAVA类的属性和方法了。
???? d、以上面的代码为例:先调用JNIEnv的FindClass方法,该函数传入一个参数,该参数就是java类的全局带包名的名称,如上面示例中的 test/Demo表示test包中的Demo类。这个方法会在你创建JVM时设置的classpath路径下找相应的类,找到后就会返回该类的 class对象。 Class是JAVA中的一个类,每个JAVA类都有唯一的一个静态的Class对象,Class对象包含类的相关信息。为了使FindClass方法能 找到你的类,请确保创建JVM时-Djava.class.path=参数设置正确。注意:系统环境变量中的CLASSPATH对这里创建JVM没有影 响,所以不要以为系统CLASSPATH设置好了相关路径后这里就不用设置了。
???? e、利用FindClass返回的class对象,调用GetMethodID函数可以获得里面方法的ID,在这里GetMethodID函数传入了三个 参数:第一个参数是class对象,因为方法属于某个具体的类;第二个参数是方法的名称;第三个参数是方法的签名,这个签名可以在前面3.3中介绍的方法 获得。
???? f、利用class对象,可以通过调用AllocObject函数获得该class对象对应类的一个实例,即Demo类的对象。
???? g、利用上面获取的函数ID和Demo类的对象,就可以通过CallObjectMethod函数调用相应的方法,该函数的参数跟printf函数的参数 一样,个数是不定的。第一个参数是类的对象;第二个参数是要调用的方法的ID;后面的参数就是需要传给调用的JAVA类方法的参数,如果调用的JAVA类 方法没有参数,则调用CallObjectMethod时传前两个参数就可以了。
???? h、从上面的示例中可以看到,在调用JAVA的方法前,构造传入的字符串时,用到了NewJString函数;在调用该方法后,对传出的字符串调用了 JstringToCString函数。这是由于Java中所有的字符都是Unicode编码,但是在本地方法中,例如用VC编写的程序,如果没有特殊的 定义一般都没有使用Unicode的编码方式。为了让本地方法能够访问Java中定义的中文字符及Java访问本地方法产生的中文字符串,定义了两个方法 用来做相互转换。
?? ? i、避免在被调用的JAVA类中使用静态final成员变量,因为在C++中生成一个JAVA类的对象时,静态final成员变量不会像JAVA中new 对象时那样先赋值。如果出现这种情况,在C++中调用该对象的方法时会发现该对象的静态final成员变量值全为0或者null(根据成员变量的类型而 定)。
?
3.3 调用JAVA中的静态方法
?
//调用静态方法
jclass cls = env->FindClass("test/Demo");
jmethodID mid = env->GetStaticMethodID(cls, "getHelloWorld","()Ljava/lang/String;");
jstring msg = (jstring)env->CallStaticObjectMethod(cls, mid);
cout<<JStringToCString(env, msg);
?
3.4 调用JAVA中的静态属性
?
//调用静态方法
jclass cls = env->FindClass("test/Demo");
jfieldID fid = env->GetStaticFieldID(cls, "COUNT","I");
int count = (int)env->GetStaticIntField(cls, fid);
cout<<count<<endl;
?
3.5 调用JAVA中的带参数构造函数
?
//调用构造函数
jclass cls = env->FindClass("test/Demo");
jmethodID mid = env->GetMethodID(cls,"<init>","(Ljava/lang/String;)V");
const char szTest[] = "电信";
jstring arg = NewJString(env, szTest);
jobject demo = env->NewObject(cls,mid,arg);
//验证是否构造成功
mid = env->GetMethodID(cls, "getMessage","()Ljava/lang/String;");
jstring msg = (jstring)env->CallObjectMethod(demo, mid);
cout<<JStringToCString(env, msg);
?
3.6 传入传出数组
?
//传入传出数组
//构造数组
long arrayCpp[] = {1,3,5,7,9};
jintArray array = env->NewIntArray(5);
env->SetIntArrayRegion(array, 0, 5, arrayCpp);
//传入数组
jclass cls = env->FindClass("test/Demo");
jobject obj = env->AllocObject(cls);
jmethodID mid = env->GetMethodID(cls,"setCounts","([I)V");
env->CallVoidMethod(obj, mid, array);
//获取数组
mid = env->GetMethodID(cls,"getCounts","()[I");
jintArray msg = (jintArray)env->CallObjectMethod(obj, mid, array);
int len =env->GetArrayLength(msg);
jint* elems =env-> GetIntArrayElements(msg, 0);
for(int i=0; i< len; i++)
{
cout<<"ELEMENT "<<i<<" IS "<<elems[i]<<endl;
}
env->ReleaseIntArrayElements(msg, elems, 0);
?
3.7 异常处理
???? 由于调用了Java的方法,因此难免产生操作的异常信息,如JAVA函数返回的异常,或者调用JNI方法(如GetMethodID)时抛出的异常。这些 异常没有办法通过C++本身的异常处理机制来捕捉到,但JNI可以通过一些函数来获取Java中抛出的异常信息。
//异常处理
jclass cls = env->FindClass("test/Demo");
jobject obj = env->AllocObject(cls);
jmethodID mid = env->GetMethodID(cls,"throwExcp","()V");
env->CallVoidMethod(obj, mid);
//获取异常信息
string exceptionInfo = "";
jthrowable excp = 0;
excp = env->ExceptionOccurred();
if(excp)
{
jclass cls = env->GetObjectClass(excp);
env->ExceptionClear();
jmethodID mid = env->GetMethodID(cls, "toString","()Ljava/lang/String;");
jstring msg = (jstring) env->CallObjectMethod(excp, mid);
out<<JStringToCString(env, msg)<<endl;
env->ExceptionClear();
}
?
Ch26:JNI中文处理问题小结
由于工作关系,需要利用JNI在C++与Java程序之间进行方法调用和数据传递,但以前总是在英文环境下工作,对中文(其他语言编码同理)问题反倒没有太关注,最近抽了点时间研究了一下,将自己的体会整理如下,供大家讨论或参考。
在进一步讨论之前,有几点基础知识需要说明:
在 Java内部,所有的字符串编码采用的是Unicode即UCS-2。Unicode是用两个字节表示每个字符的字符编码方案。Unicode有一个特 性:它包括了世界上所有的字符字形。所以,各个地区的语言都可以建立与Unicode的映射关系,而Java正是利用了这一点以达到异种语言之间的转换;
UTF-8是另一种不同于UCS-2/UCS-4的编码方案,其中UTF代表UCS Transformation Format,它采用变长的方式进行编码,编码长度可以是1~3(据说理论上最长可以到6,不懂)。
由 于UCS-2/UCS-4编码定长的原因,编码产生的字符串会包含一些特殊的字符,如\0(即0x0,所有0~256的字符Unicode编码的第一个 字节),这在有些情况下(如传输或解析时)会给我们带来一些麻烦,而且对于一般的英文字母浪费了太多的空间,此外,据说UTF-8还有Unicode所没 有的纠错能力(不懂!),因此,Unicode往往只是被用作一种中间码,用于逻辑表示。关于Unicode/UTF-8的更多信息,见参考1;
Java中文乱码问题在很多情况下都可能发生:不同应用间,不同平台间等等,但以上问题已有大量优秀的文章讨论过,这里不作深入探讨,详见参考2、3、4、5。下面简要总结一下:
当我们使用默认编码方式保存源文件时,文件内容实际上是按照我们的系统设定进行编码保存的,这个设定值即file.encoding可以通过下面的程序获得:
public class Encoding {
public static void main(String[] args) {
System.out.println(
System.getProperty("file.encoding")
);
}
}
javac在不指定encoding参数时,如果区域设定不正确,则可能造成编/解码错误,这个问题在编译一个从别的环境传过来的文件时可能发生;
2、虽然在Java内部(即运行期间,Runtime)字符串是以Unicode形式存在的,但在class文件中信息是以UTF-8形式存储的(Unicode仅被用作逻辑表示中间码) ;
3. 对 于Web应用,以Tomcat为例,JSP/Servlet引擎提供的JSP转换工具(jspc)搜索JSP文件中用<%@ page contentType ="text/html; charset=<Jsp-charset>"%>指定的charset。如果在JSP文件中未指定<Jsp-charset& amp; amp; gt;,则取系统默认的file.encoding(这个值在中文平台上是GBK),可通过控制面板的Regional Options进行修改;jspc用相当于“javac –encoding <Jsp-charset>”的命令解释JSP文件中出现的所有字符,包括中文字符和ASCII字符,然后把这些字符转换成Unicode字 符,再转化成UTF-8格式,存为JAVA文件。
我曾经偶然将jsp文件存成UTF-8,而在文件内部使用的charset却是GB2312, 结果运行时总是无法正常显示中文,后来转存为默认编码方式才 正常。只要文件存储格式与JSP开头的charset设置一致,就都可以正常显示(不过将文件保存成UTF-16的情况下我还没有试验成功);
4. 在 XML文件中,encoding表示的是文件本身的编码方式,如果这个参数设定与文件本身实际的编码方式不一致的话,则可能解码失败,所以应该总是将 encoding设置成与文件编码方式一致的值;而JSP/HTML的charset则表示按照何种字符集来解码从文件中读取出来的字符串(在理解中文问 题时应该把字符串理解成一个二进制或16进制的串,按照不同的charset可能映射成不同的字符)。
我曾经在网上就encoding的具体含义跟别人讨论过:如果encoding指的是文件本身的编码方式,那么读取该文件的应用程序在不知道encoding设置的情况下如何正确解读该文件呢?
根 据讨论及个人理解,处理程序(如jspc)总是按ISO8859-1来读取输入文件,然后检查文件开始的几个字节(即Byte Order Mark,BOM,具体如何判断,可以参考Tomcat源码$SOURCE_DIR\jasper\jasper2\src\share\org \apache\jasper\xmlparser\XMLEncodingDetector.java的getEncodingName方法,在JSP Specification的Page Character Encoding一节也有详细论述)以探测文件是以何种格式保存的,当解析到encoding选项时,若encoding设置与文件实际保存格式不一致, 会尝试进行转换,但这种转换可能在文件实际以ISO8859-1/UTF-8等单字节编码而encoding被设置成Unicode、UTF-16等双字 节编码时发生错误。
下面重点讨论JNI中在C++程序与Java程序间进行数据传递时需要注意的问题。
在JNI中jstring采用的是UCS-2编码,与Java中String的编码方式一致。但是在C++中,字符串是用char(8位)或者 wchar_t(16位,Unicode编码与jchar一致,但并非所有开发平台上都是Unicode编码,详见参考6),下面的程序证明了这一点(编 译环境:VC6):
#include <iostream>
using namespace std;
int main()
{
locale loc( "Chinese-simplified" );
//locale loc( "chs" );
//locale loc( "ZHI" );
//locale loc( ".936" );
wcout.imbue( loc );
wcout << L"中文" << endl; //若没有L,会出问题
wchar_t wch[] = {0x4E2D, 0x6587, 0x0};
//"中文"二字的Unicode编码
wcout << wch << endl;
return 0;
}
JNI提供了几个方法来实现jstring与char/wchar_t之间的转换。
jsize GetStringLength(jstring str)
const jchar *GetStringChars(jstring str, jboolean *isCopy)void ReleaseStringChars(jstring str, const jchar *chars)
此外,为了便于以UTF-8方式进行传输、存储,JNI还提供了几个操作UTF格式的方法:
jsize GetStringUTFLength(jstring str)const char* GetStringUTFChars(jstring str, jboolean *isCopy)void ReleaseStringUTFChars(jstring str, const char* chars)
GetStringChars返回的是Unicode格式的编码串,而GetStringUTFChars返回的是UTF-8格式的编码串。要创建一个 jstring,可以用如下方式:
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;
}
而要将一个jstring对象转为一个char字符串数组,可以:
int JStringToChar( JNIEnv * env,
jstring str,
LPTSTR desc,
int desc_len )
{
int len = 0;
if (desc == NULL || str == NULL)
return -1;
// Check buffer size
if (env->GetStringLength(str) * 2 + 1 > desc_len)
{ return -2; }
memset(desc, 0, desc_len);
const wchar_t * w_buffer = env->GetStringChars(str, 0);
len = WideCharToMultiByte(CP_ACP, 0,
w_buffer, wcslen(w_buffer) + 1, desc, desc_len, NULL, NULL);
env->ReleaseStringChars(str, w_buffer);
if (len > 0 && len < desc_len)
desc[len] = 0;
return strlen(desc);
}
当 然,按照上面的分析,你也可以直接将GetStringChars的返回结果作为wchar_t串来进行操作。或者,如果你愿意,你也可以将 GetStringUTFChars的结果通过MultiByteToWideChar转换为UCS2编码串,再通过 WideCharToMultiByte转换为多字节串。
const char* pstr = env->GetStringUTFChars(str, false);
int nLen = MultiByteToWideChar( CP_UTF8, 0, pstr, -1, NULL, NULL );
//得到UTF-8编码的字符串长度
LPWSTR lpwsz = new WCHAR[nLen];
MultiByteToWideChar( CP_UTF8, 0, pstr, -1, lpwsz, nLen );
//转换的结果是UCS2格式的编码串
int nLen1 = WideCharToMultiByte( CP_ACP,
0, lpwsz, nLen,
NULL, NULL, NULL, NULL );
LPSTR lpsz = new CHAR[nLen1];
WideCharToMultiByte( CP_ACP, 0, lpwsz, nLen, lpsz, nLen1, NULL, NULL );
//将UCS2格式的编码串转换为多字节
cout << "Out:" << lpsz << endl;
delete [] lpwsz;
delete [] lpsz;
当然,我相信很少有人想要或者需要这么做。这里需要注意一点,GetStringChars的返回值是jchar,而 GetStringUTFChars的返回值是const char*。
除 了上面的办法外,当需要经常在jstring和char*之间进行转换时我们还有一个选择,那就是下面的这个类。这个类本来是一个叫 Roger S. Reynolds的老外提供的,想法非常棒,但用起来却不太灵光,因为作者将考虑的重心放在UTF格式串上,但在实际操作中,我们往往使用的却是 ACP(ANSI code page)串。下面是原作者的程序:
class UTFString {
private: UTFString ();
// Default ctor - disallowed
public:
// Create a new instance from the specified jstring
UTFString(JNIEnv* env, const jstring& str) :
mEnv (env),
mJstr (str),
mUtfChars ((char* )mEnv->GetStringUTFChars (mJstr, 0)), mString (mUtfChars) { }
// Create a new instance from the specified string
UTFString(JNIEnv* env, const string& str) :
mEnv (env),
mString (str),
mJstr (env->NewStringUTF (str.c_str ())),
mUtfChars ((char* )mEnv->GetStringUTFChars (mJstr, 0)) { }
// Create a new instance as a copy of the specified UTFString
UTFString(const UTFString& rhs) :
mEnv (rhs.mEnv),
mJstr (mEnv->NewStringUTF (rhs.mUtfChars)),
mUtfChars ((char* )mEnv->GetStringUTFChars (mJstr, 0)), mString (mUtfChars) { }
// Delete the instance and release allocated storage
~UTFString() {
mEnv->ReleaseStringUTFChars (mJstr, mUtfChars);
}
// assign a new value to this instance from the given string UTFString & operator =(const string& rhs) {
mEnv->ReleaseStringUTFChars (mJstr, mUtfChars);
mJstr = mEnv->NewStringUTF (rhs.c_str ());
mUtfChars = (char* )mEnv->GetStringUTFChars (mJstr, 0); mString = mUtfChars;
return *this;
}
// assign a new value to this instance from the given char*
UTFString & operator =(const char* ptr) {
mEnv->ReleaseStringUTFChars (mJstr, mUtfChars);
mJstr = mEnv->NewStringUTF (ptr);
mUtfChars = (char* )mEnv->GetStringUTFChars (mJstr, 0); mString = mUtfChars;
return *this;
}
// Supply operator methods for converting the UTFString to a string
// or char*, making it easy to pass UTFString arguments to functions // that require string or char* parameters.
string & GetString() { return mString; }
operator string() { return mString; }
operator const char* () {
return mString.c_str ();
}
operator jstring() {
return mJstr;
}
private:
JNIEnv* mEnv;
// The enviroment pointer for this native method.
jstring mJstr;
// A copy of the jstring object that this UTFString represents char* mUtfChars;
// Pointer to the data returned by GetStringUTFChars string mString;
// string buffer for holding the "value" of this instance };
我将它改了改:
class JNIString {private: JNIString (); // Default ctor - disallowedpublic: // Create a new instance from the specified jstring JNIString(JNIEnv* env, const jstring& str) : mEnv (env) { const jchar* w_buffer = env->GetStringChars (str, 0); mJstr = env->NewString (w_buffer, wcslen (w_buffer)); // Deep Copy, in usual case we only need // Shallow Copy as we just need this class to // provide some convenience for handling jstring mChars = new char[wcslen (w_buffer) * 2 + 1]; WideCharToMultiByte (CP_ACP, 0, w_buffer, wcslen (w_buffer) + 1, mChars, wcslen (w_buffer) * 2 + 1, NULL, NULL); env->ReleaseStringChars (str, w_buffer); mString = mChars; } // Create a new instance from the specified string JNIString(JNIEnv* env, const string& str) : mEnv (env) { int slen = str.length (); jchar* buffer = new jchar[slen]; int len = MultiByteToWideChar (CP_ACP, 0, str.c_str (), str.length (), buffer, slen); if (len > 0 && len < slen) buffer[len] = 0; mJstr = env->NewString (buffer, len); delete [] buffer; mChars = new char[str.length () + 1]; strcpy (mChars, str.c_str ()); mString.empty (); mString = str.c_str (); } // Create a new instance as a copy of the specified JNIString JNIString(const JNIString& rhs) : mEnv (rhs.mEnv) { const jchar* wstr = mEnv->GetStringChars (rhs.mJstr, 0); mJstr = mEnv->NewString (wstr, wcslen (wstr)); mEnv->ReleaseStringChars (rhs.mJstr, wstr); mChars = new char[strlen (rhs.mChars) + 1]; strcpy (mChars, rhs.mChars); mString = rhs.mString.c_str (); } // Delete the instance and release allocated storage ~JNIString() { delete [] mChars; } // assign a new value to this instance from the given string JNIString & operator =(const string& rhs) { delete [] mChars; int slen = rhs.length (); jchar* buffer = new jchar[slen]; int len = MultiByteToWideChar (CP_ACP, 0, rhs.c_str (), rhs.length (), buffer, slen); if (len > 0 && len < slen) buffer[len] = 0; mJstr = mEnv->NewString (buffer, len); delete [] buffer; mChars = new char[rhs.length () + 1]; strcpy (mChars, rhs.c_str ()); mString = rhs.c_str (); return *this; } // Supply operator methods for converting the JNIString to a string // or char*, making it easy to pass JNIString arguments to functions // that require string or char* parameters. string & GetString() { return mString; } operator string() { return mString; } operator const char* () { return mString.c_str (); } operator jstring() { return mJstr; }private: JNIEnv* mEnv; // The enviroment pointer for this native method. jstring mJstr; // A copy of the jstring object that this JNIString represents char* mChars; // Pointer to a ANSI code page char array string mString; // string buffer for holding the "value" of this instance (ANSI code page)};
后者除了将面向 UTF编码改成了面向ANSI编码外,还去掉了operator =(const char* ptr)的定义,因为 operator =(const string& rhs)可以在需要的时候替代前者而无需任何额外编码。(因为按照C++规范,const reference可以自动转换,详见本人另一文章“关于 const reference 的几点说明 ”)
如果你愿意,给JNIString再加个JNIString(JNIEnv* env, const wstring& str)和一个operator =(const wstring& rhs)操作符重载就比较完美了,:),很简单,留给用得到的朋友自己加吧。
下面是一个使用该类的例子(真正跟用于演示的code很少,大部分都是些routine code,:)):
#include <iostream>#include <string>#include <assert.h>#include <jni.h>using namespace std;int main() { int res; JavaVM* jvm; JNIEnv* env; JavaVMInitArgs vm_args; JavaVMOption options[3]; options[0].optionString = "-Djava.compiler=NONE"; options[1].optionString = "-Djava.class.path=.;.."; // .. is specially for this project options[2].optionString = "-verbose:jni"; vm_args.version = JNI_VERSION_1_4; 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"); return 1; } jclass cls = env->FindClass ("jni/test/Demo"); assert (0 != cls); jmethodID mid = env->GetMethodID (cls, "", "(Ljava/lang/String;)V"); assert (0 != mid); wchar_t* p = L"中国"; jobject obj = env->NewObject (cls, mid, env->NewString (reinterpret_cast (p), wcslen (p))); assert (0 != obj); mid = env->GetMethodID (cls, "getMessage", "()Ljava/lang/String;"); assert (0 != mid); jstring str = (jstring)env->CallObjectMethod (obj, mid); // use JNIString for easier handling. JNIString jnistr (env, str); cout << "JNIString:" << jnistr.GetString () << endl; jnistr = "中文"; cout << jnistr.GetString () << endl; jvm->DestroyJavaVM (); fprintf (stdout, "Java VM destory.\n"); return 0;}
参考资料:
- UTF-8 and Unicode FAQ for Unix/Linuxs,http://www.cl.cam.ac.uk/~mgk25/unicode.html ,
其中文翻译见http://www.linuxforum.net/books/UTF-8-Unicode.html - 深入剖析Java编程中的中文问题及建议最优解决方法,http://blog.csdn.net/abnerchai/archive/2004/04/28/18576.aspx
- 关于Java中文问题的几条分析原则,http://www-900.ibm.com/developerWorks/cn/java/l-javachinese/index.shtml
- Java 编程技术中汉字问题的分析及解决,http://www-900.ibm.com/developerWorks/cn/java/java_chinese/index.shtml
- 深入剖析JSP和Servlet对中文的处理过程,http://blog.csdn.net/deuso/archive/2005/12/01/541511.aspx
- 宽字符标量L"xx"在VC6.0/7.0和GNU g++中的不同实现,http://blog.vckbase.com/smileonce/archive/2004/12/09/1972.html
XML Encoding,http://www.w3schools.com/xml/xml_encoding.asp
最新评论
// 编码长度可以是1~3(据说理论上最长可以到6,不懂)。
直到Unicode 2.0,Unicode还是一个很简单的编码,每个字符16位——两个字节
到了Unicode 3.0,为了支持庞大的东亚象形文字,Unicode编码空间增加为0~10FFFF,提出代理对机制(用两个w_char存储一个图形字符)来支持10000~10FFFF之间的编码(这就是UTF-16的前身)
到了Unicode 4.0,直接定义为31位空间——群、面、行、格 四级,并提出多种编码方案:UTF-7、UTF-8、UTF-16、UTF-32
UTF-8是变长编码,首字节标示了长度值,其余字节带有6位数据。由于设计得很巧妙,存在冗余位,所以可以纠错。
其携带信息:
1 Byte:7bit(7位ASCII)
2 Byte:5 + 6*1 = 11bit
3 Byte:4 + 6*2 = 16bit(16位基本语义平面字符)
4 Byte:3 + 6*3 = 21bit(21位代理对)
5 Byte:2 + 6*4 = 26bit
6 Byte:1 + 6*5 = 31bit
Ch27:JNI的crash终于搞定
今天终于搞定困扰我一周的一个问题了。
我们的算法通过jni封装,在java 调 用的时候总是随机的crash掉,具体的位置在jvm里面,应该可以肯定是jvm做垃圾回收的时候死掉的。但是并不知道是在回收哪块内存出的问题,所以也 就无从知道死的具体原因了。我们的程序是在jni层创建了一些java对象,然后返回给java层,大体结构像下面代码一样,我只能基本判断是我们的 jni层在创建对象的时候(也就是createInfo函数)出问题了,至于具体什么问题,我也不清楚。
public class Test {
public class Info {
public int x;
public int y;
public Info() {
x = 0;
y = 0;
}
}
public native Info createInfo();
// ...
}
因 为我对java不是很熟悉,所以只好一边学,一边弄。最初就是在local/glbal reference这些概念上下功夫,来回的读jni的specification,也没有发现自己的问题。后期又学着使用一些java的调试工具,比如 jhat啊,hpjmeter啊,但是仍然没有什么头绪。上周一周,就在这个问题上不断的尝试,也没结果。
今天终于发现了问题所在,其实 说来也很简单。jni要 创建的那些返回对象,是作为内部类定义的,所以在构造的时候需要传一个外层类实例才能初始化。也就是说,虽然看上去Info类的构造函数是无参数的,但实 际上它是有一个隐含参数的,相当于Info(Test outer)。如果在java层构造这个对象,那么outer参数会被自动传入,但我们在jni层构造,就需要自己传入这个参数了。如果没有给出这个参 数,jni编译运行都没有问题,但实际上,它是用了一个未知的对象(就是在栈里面的一个随机值)来作为这个outer参数的,所以当这个对象需要释放的时 候(一般也就是在垃圾回收的时候)就会crash了。
现在想起来,其实这个问题我原来曾经有过一次小遭遇,那时我在使用有参数构造函数来 创建一个内部嵌套类,发现构造出来的对象值是错掉的。其实就是因为少传了一个outer参数啊,但是当时我没有去解决这个问题,而是绕过问题,采用构造函 数无参数,然后在创建之后,再手工给每个数据字段赋值的方法。这样虽然表面上也达到了目的,但是隐藏了问题。
事实一次次的告诉我们,遇到问题一定要解决。就算你暂时绕过这个问题,但早晚它还会出来的。