Jni函数调用大全

Chap1:JNI完全手册... 3

Chap2:JNI-百度百科... 11

Chap 3:javah命令帮助信息... 16

Chap 4:用javah产生一个.h文件... 17

Chap5:jni教程(very very good) 19

Chap6: JNI传递返回值... 26

15.2.2.3 传递字符串... 28

15.2.2.4 传递整型数组 ... 29

15.2.2.5 传递字符串数组 ... 30

15.2.2.6 传递对象数组 ... 31

Chap7:Jni中C++和Java的参数传递... 33

Chap8:如何将java传递过来的jbyteArray转换成C/C++中的BYTE数组... 47

Chap5:使用JNI技术实现java程序调用第三方dll(c/c++)文件的功能... 47

Chap9:如何编写jni方法(转载)... 55

1、实例一:在jni中调用标准c中自带的函数printf(): 57

2、实例二、调用c 语言用户定义的函数... 58

3、实例三、在jni函数中访问java类中的对象实例域... 58

4、实例四:在jni函数中访问类的静态实例域... 60

5、实例五:在jni函数中调用java对象的方法... 60

6、实例六:在jni函数中调用java类的静态方法... 61

7、实例七:jni函数中传递基本数据类型参数... 62

8、实例八:在jni函数中传递对象类型参数... 62

9、实例九:在jni函数中处理字符串... 63

10、实例十:在jni函数中处理数组... 64

11、实例十一:在jni中的返回值问题... 65

12、实例十二:在jni中创建java类对象:... 66

Chap10:在 Windows 中实现 Java 本地方法... 66

1.Java 调用 C. 67

2.调试... 76

3.其他信息... 79

Chap11:如何在C/C++中调用Java. 80

1.环境搭建... 81

2.初始化虚拟机... 83

3.访问类方法... 85

4访问类属性... 87

5.访问构造函数... 88

6.数组处理... 89

7.中文处理... 89

8.异常... 91

9.线程和同步访问... 91

10.时间... 92

Chap12:基本JNI调用技术(c/c++与java互调) 93

Chap13:JNI的c代码中,另外一个线程获取 JNIEnv. 96

chap 14:当JNI遇到多线程--java对象如何被C++中的多个线程访问?. 97

chap 15:JNI在多线程中的应用... 101

chap 16:JNI限制(多线程)... 105

chap 17:使用 Java Native Interface 的最佳实践... 106

1.性能缺陷... 107

2.正确性缺陷... 117

3.避免常见缺陷... 121

4.结束语... 128

Chap18:JNI设计实践之路... 129

一、       前言... 129

二、       JNI基础知识简介... 130

三、       Java程序调用非Java程序... 131

四、       C/C++访问Java成员变量和成员方法... 138

五、       异常处理... 140

六、       MFC程序中嵌入Java虚拟机... 142

Chap19:JNI编程系列之基础篇... 148

System.loadLibrary("HelloWorld"); 149

JNIEXPORT void JNICALL Java_HelloWorld_print (JNIEnv *, jobject); 150

Chap20:JNI编程系列之中级篇(上)... 151

1. Java基本类型的传递... 151

2. String参数的传递... 151

3. 数组类型的传递... 153

4. 二维数组和String数组... 154

Chap21:JNI编程系列之高级篇... 155

1. 在一般的Java类中定义native方法... 156

2. 访问Java类的域和方法... 156

3. 在native方法中使用用户定义的类... 157

4. 异常处理... 158

 

注: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-百度百科

 

 

目录

定义

设计目的

书写步骤

简要使用例子

调用中考虑的问题

对JAVA传入数据的处理

 

定义

  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 ++ 代码的过程由六个步骤组成。我们将在下面几页中深入讨论每个步骤,但还是先让我们迅速地浏览一下它们。

  1. 编写 Java 代码 。我们将从编写 Java 类开始,这些类执行三个任务:声明将要调用的本机方法;装入包含本机代码的共享库;然后调用该本机方法。
  2. 编译 Java 代码 。在使用 Java 类之前,必须成功地将它们编译成字节码。
  3. 创建 C/C++ 头文件 。C/C++ 头文件将声明想要调用的本机函数说明。然后,这个头文件与 C/C++ 函数实现(请参阅步骤 4)一起来创建共享库(请参阅步骤 5)。
  4. 编写 C/C++ 代码 。这一步实现 C 或 C++ 源代码文件中的函数。C/C++ 源文件必须包含步骤 3 中创建的头文件。
  5. 创建共享库文件 。从步骤 4 中创建的 C 源代码文件来创建共享库文件。
  6. 运行 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]

where [options] include:

        -help                 Print this help message and exit

        -classpath      Path from which to load classes

        -bootclasspath Path from which to load bootstrap classes

        -d

              Output directory

        -o              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

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

/* 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

  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

     {   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

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

     {   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

      #include   

     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

      #include    

      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

         *(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

           System.out.println(nums[i]);

     }

}

Sample2.java调用libSample2.so中的函数intMethod()。Sample2.c的源代码如下。

代码清单15-9 在Linux平台上调用C函数的例程——Sample2.c

      #include

 

      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

        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[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

      #include

      #include

 

      #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

          {

            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

              System.out.println(mails[i].topic);

    }

}

Sample4.java调用libSample4.so中的objectMethod()函数。Sample4.c的源代码如下。

代码清单15-14 在Linux平台上调用C函数的例程——Sample4.c

     #include

     #include

     #include     

     #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

    {

        (*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的最简单的例子>

这 些资料的例子中,大多数只是输入一些简单的参数,获取没有参数。而在实际的使用过程中,往往需要对参数进行处理转换。才可以被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:[email protected]

* @version 1.0

* @since 2005-4-30

*

* 修改记录:

*

* 日期              修改人                 描述

* ----------------------------------------------------------------------------------------------

*

*

*

*/

// JniManage.cpp : 定义 DLL 应用程序的入口点。

//

package com.sundy.jnidemo;

#include "stdafx.h"

#include

#include

#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

#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()方法名。请问该怎么解决这个问题?

我的邮箱:[email protected] 回复 更多评论  

# 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
#include
#include
#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
#include
#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,"","(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 测试。但是,如果必须执行本地调用,则要考虑几个准则:

  1. 将您的所有本地方法都封装在单个类中,这个类调用单个 DLL。对于每种目标操作系统,都可以用特定于适当平台的版本替换这个 DLL。这样就可以将本地代码的影响减至最小,并有助于将以后所需的移植问题包含在内。
  2. 本地方法要简单。尽量将您的 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

#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

#include

#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 项目来调试本地方法的步骤。

  1. 在 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。可以通过 [email protected] 与他联系。

 

 

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

/* 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,只要你安装了JDK就能在安装目录下找到它。

不要修改这个文件的内容,现在要做的是写一个名为HelloWorld.cpp程序,实现上面这个.h文件里的函数,

 

//------------

#include "HelloWorld.h"

#include

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

#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

 

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, "", "(D)V");

jdouble dd = 0.033;

jvalue args[1];

args[0].d = dd;

jobject obj = env->NewObjectA(cls, id, args);

首 先要创建一个自定义类的引用,通过FindClass函数来完成,参数同前面介绍的创建String对象的引用类似,只不过类名称变成自定义类的名称。然 后通过GetMethodID函数获得这个类的构造函数,注意这里方法的名称是"",它表示这是一个构造函数。

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(":" + 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  
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  
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.调用方法

 

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

 

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.读取和设置属性值

 

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

jfieldID field = (*env)->GetFieldID(env,obj,"msg"," Ljava/lang/String;");

jstring msg = (*env)->GetObjectField(env, cls, field); //msg就是对应Demo的msg

jfieldID field2 = (*env)->GetStaticFieldID(env,obj,"COUNT","I");

jint count = (*env)->GetStaticIntField(env,cls,field2);

5.访问构造函数

 

 

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

 

 

jclass cls = (*env)->FindClass(env, "jni/test/Demo");

/** 

 首先通过类的名称获取类的定义,相当于Java中的Class.forName方法

*/

if (cls == 0)   

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

if(mid == 0)  

jobject demo = (*env)->NewObject( env ,cls,mid,0 );

/**

 访问构造函数必须使用NewObject的函数来调用前面获取的构造函数的定义

 上面的代码我们构造了一个Demo的实例并传一个空串null

*/

 

 

 

6.数组处理

 


6.1创建一个新数组

 

要 创建一个数组,我们首先应该知道数组元素的类型及数组长度。JNI定义了一批数组的类型jArray及数组操作的函数 NewArray,其中就是数组中元素的类型。例如,要创建一个大小为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]=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( &env ), 0 );

if(result>=0)

{

     //获取运行环境成功

}

 

当线程退出时,需要释放本线程使用的运行环境。

jvm->DetachCurrentThread();

 

2 获取类

在进行方法调用之前,需要先获取相应的类,类名称必须包括包名,其中的“.”用“/”代替。

jclass JavaClass;

JavaClass = env->FindClass("com/test/TestInterface");

   if(JavaClass != 0)

   {

            //获取成功

   }

 

3 创建类对象

如果需要调用的方法静态方法,则可以跳过本步骤。反之,则需要构造该对象。构造对象是通过调用类的构造函数来实现的,构咱函数的方法声明为, GetMethodID方法的参数在下一步骤详细说明。

jobject obj;

jmethodID ctor;

ctor = env->GetMethodID(JavaClass,"","()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

#include

#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

#include

 

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指针到全局变量中,然后在另外一个线程中使用它.幸运的是,可以用   

  1. gs_object=env->NewGlobalRef(obj);//创建一个全局变量

来将传入的obj(局部变量)保存到gs_object中,从而其他线程可以使用这个gs_object(全局变量)来操纵这个java对象了.


示例代码如下:

(1)java代码:Test.java:

  1. import java.io.*;
  2. class Test implements Runnable
  3. {
  4. public int value   = 0;
  5. static{ System.loadLibrary("Test");}
  6.  
  7. public native void setEnev();//本地方法
  8.  
  9. public static void main(String args[]) throws Exception
  10. {
  11.     Test t = new Test();
  12.     t.setEnev(); //调用本地方法
  13.  
  14.     while(true)
  15.      {
  16.        Thread.sleep(1000);
  17.        System.out.println(t.value);
  18.      }
  19.    }
  20. }
  21.  
  22.  

(2) DLL代码:Test.cpp:

 

1.    #include "test.h"

2.    #include

3.    #include

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 大性能缺陷如下:

  • 不缓存方法 ID、字段 ID 和类
  • 触发数组副本
  • 回访(Reaching back)而不是传递参数
  • 错误认定本机代码与 Java 代码之间的界限
  • 使用大量本地引用,而未通知 JVM

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;
}

 

性能技巧 #1

查找并全局缓存常用的类、字段 ID 和方法 ID。

清单 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 创建完整数组的副本。

性能技巧 #2

获取和更新仅本机代码需要的数组部分。在只要数组的一部分时通过适当的 API 调用来避免复制整个数组。

GetTypeArrayRegion() 和 SetTypeArrayRegion() 方法允许您获取和更新数组的一部分,而不是整个数组。通过使用这些方法访问较大的数组,您可以确保只复制本机代码将要实际使用的数组部分。

举例来说,考虑相同方法的两个版本,如清单 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 倍的时间!

性能技巧 #3

在单个 API 调用中尽可能多地获取或更新数组内容。如果可以一次较多地获取和更新数组内容,则不要逐个迭代数组中的元素。

另一方面,如果您最终要获取数组中的所有元素,则使用 GetTypeArrayRegion() 逐个获取数组中的元素是得不偿失的。要获取最佳的性能,应该确保以尽可能大的块的来获取和更新数组元素。如果您要迭代一个数组中的所有元素,则 清单 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;
}

 

性能技巧 #4

如果可能,将各参数传递给 JNI 本机代码,以便本机代码回调 JVM 获取所需的数据。

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 代码也需要耗费大量的时间。

性能技巧 #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

构造应用程序的数据,使它位于界限的正确的侧,并且可以由使用它的代码访问,而不需要大量跨界调用。

最显著的一个问题就是,清单 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() 时都会为元素创建一个本地引用,并且直到本机代码运行完成时才会释放。数组越大,所创建的本地引用就越多。

性能技巧 #7

当本机代码造成创建大量本地引用时,在各引用不再需要时删除它们。

这些本地引用会在本机方法终止时自动释放。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);
   }
}

 

性能技巧 #8

如果某本机代码将同时存在大量本地引用,则调用 JNI EnsureLocalCapacity() 方法通知 JVM 并允许它优化对本地引用的处理。

您可以调用 JNI EnsureLocalCapacity() 方法来通知 JVM 您将使用超过 16 个本地引用。这将允许 JVM 优化对该本机代码的本地引用的处理。如果无法创建所需的本地引用,或者 JVM 采用的本地引用管理方法与所使用的本地引用数量之间不匹配造成了性能低下,则未成功通知 JVM 会导致 FatalError 。

 

 

2.正确性缺陷

5 大 JNI 正确性缺陷包括:

  • 使用错误的 JNIEnv
  • 未检测异常
  • 未检测返回值
  • 未正确使用数组方法
  • 未正确使用全局引用

2.1使用错误的 JNIEnv

执行本机代码的线程使用 JNIEnv 发起 JNI 方法调用。但是,JNIEnv 并不是仅仅用于分派所请求的方法。JNI 规范规定每个 JNIEnv 对于线程来说都是本地的。JVM 可以依赖于这一假设,将额外的线程本地信息存储在 JNIEnv中。一个线程使用另一个线程中的 JNIEnv 会导致一些小 bug 和难以调试的崩溃问题。

正确性技巧 #1

仅在相关的单一线程中使用 JNIEnv 。

线程可以调用通过 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);

 

正确性技巧 #2

在发起可能会导致异常的 JNI 调用后始终检测异常。

添加异常检测代码要比在事后尝试调试崩溃简单很多。经常,您只需要检测是否出现了某个异常,如果是则立即返回 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 方法来说,它们都设置了返回值和异常状态,这样应用程序更可以通过检测异常状态或返回值来判断方法运行正常与否。

正确性技巧 #3

始终检测 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未正确使用数组方法

GetXXXArrayElements() 和 ReleaseXXXArrayElements() 方法允许您请求任何元素。同样,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;
}

 

正确性技巧 #4

不要忘记为每个 GetXXX() 使用模式 0 (复制回去并释放内存)调用 ReleaseXXX() 。

在提供直接指向数组的指针的 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;
}

 

第二个缺陷是不注重规范对在 GetXXXCritical() 和 ReleaseXXXCritical() 之间执行的代码施加的限制。本机可能不会在这些方法之间发起任何调用,并且可能不会由于任何原因而阻塞。未重视这些限制会造成应用程序或 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;
}

 

正确性技巧 #5

确保代码不会在 GetXXXCritical() 和 ReleaseXXXCritical() 调用之间发起任何 JNI 调用或由于任何原因出现阻塞。

但是,我们需要验证在调用 processBufferHelper() 时可以运行的所有代码都没有违反任何限制。这些限制适用于在 Get 和 Release 调用之间执行的所有代码,无论它是不是本机的一部分。

2.5未正确使用全局引用

本机可以创建一些全局引用,以保证对象在不再需要时才会被垃圾收集器回收。常见的缺陷包括忘记删除已创建的全局引用,或者完全失去对它们的跟踪。考虑一个本机创建了全局引用,但是未删除它或将它存储在某处:

lostGlobalRef(JNIEnv* env, jobject obj, jobject keepObj) {
   jobject gref = (*env)->NewGlobalRef(env, keepObj);
}

 

正确性技巧 #6

始终跟踪全局引用,并确保不再需要对象时删除它们。

创 建全局引用时,JVM 会将它添加到一个禁止垃圾收集的对象列表中。当本机返回时,它不仅会释放全局引用,应用程序还无法获取引用以便稍后释放它 — 因此,对象将会始终存在。不释放全局引用会造成各种问题,不仅因为它们会保持对象本身为活动状态,还因为它们会将通过该对象能接触到的所有对象都保持为活 动状态。在某些情况下,这会显著加剧内存泄漏。

 

 

3.避免常见缺陷

假设您编写了一些新 JNI 代码,或者继承了别处的某些 JVI 代码,如何才能确保避免了常见缺陷,或者在继承代码中发现它们?表 1 提供了一些确定这些常见缺陷的技巧:


表 1. 确定 JNI 编程缺陷的清单

 

未缓存

触发数组副本

错误界限

过多回访

使用大量本地引用

使用错误的 JNIEnv

未检测异常

未检测返回值

未正确使用数组

未正确使用全局引用

规范验证

 

 

 

 

 

X

X

 

X

 

方法跟踪

X

X

X

X

 

 

X

 

X

X

转储

 

 

 

 

 

 

 

 

 

X

-verbose:jni

 

 

 

 

X

 

 

 

 

 

代码审查

X

X

X

X

X

X

X

X

X

X

您可以在开发周期的早期确定许多常见缺陷,方法如下:

  • 根据规范验证新代码
  • 分析方法跟踪
  • 使用 -verbose:jni 选项
  • 生成转储
  • 执行代码审查

3.1根据 JNI 规范验证新代码

维持规范的限制列表并审查本机与列表的遵从性是一个很好的实践,这可以通过手动或自动代码分析来完成。确保遵从性的工作可能会比调试由于违背限制而出现的细小和间断性故障轻松很多。下面提供了一个专门针对新开发代码(或对您来说是新的)的规范顺从性检查列表:

  • 验证 JNIEnv 仅与与之相关的线程使用。
  • 确认未在 GetXXXCritical() 的 ReleaseXXXCritical() 部分调用 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() 调用 — 特别是,如果这些调用针对相同的字段和方法 — 表示字段和方法未被缓存。
  • GetTypeArrayElements() 调用实例(而非 GetTypeArrayRegion() )有时表示存在不必要的复制。
  • 在 Java 代码与本机代码之前来回快速切换(由时间戳指示)有时表示 Java 代码与本机代码之间的界限有误,从而造成性能较差。
  • 每个本机函数调用后面都紧接着大量 GetFieldID() 调用,这种模式表示并未传递所需的参数,而是强制本机回访完成工作所需的数据。
  • 调用可能抛出异常的 JNI 方法之后缺少对 ExceptionOccurred() 或 ExceptionCheck() 的调用表示本机未正确检测异常。
  • GetXXX() 和 ReleaseXXX() 方法调用的数量不匹配表示缺少释放操作。
  • 在 GetXXXCritical() 和 ReleaseXXXCritical() 调用之间调用 JNI 方法表示未遵循规范施加的限制。
  • 如果调用 GetXXXCritical() 和 ReleaseXXXCritical() 之间相隔的时间较长,则表示未遵循 “不要阻塞调用” 规范所施加的限制。
  • 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 回调

                               
 ()V>

 

对于 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,使用以下步骤获取关于全局引用的信息:

  1. 将 -Xdump:system:events=user 添加到命令行。这样,当您在 UNIX 系统上调用 kill -3 或者在 Windows 上按下 Ctrl+Break 时,JVM 便会生成转储。
  2. 程序在运行中时会生成后续转储。
  3. 运行 jextract -nozip core.XXX output.xml ,这将会将转储信息提取到可读格式的 output.xml 中。
  4. 查找 output.xml 中的 JNIGlobalReference 条目,它提供关于当前全局引用的信息,如清单 16 所示:


清单 16. output.xml 中的 JNIGlobalReference条目

                               

 

通过查看后续 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

/* 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

#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,"","()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, "" ," 函数签名");

从上面代码中可以看出,在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"<

              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"<

                     env->ExceptionClear();

              }

              else

              {

                     cout<<"errors in invoking javaAreas() method of Circle"<

                     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);

              e

你可能感兴趣的:(java,jni,java,string,jvm,class,buffer)