Android JNI --函數調用大全

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] <classes>

where [options] include:

        -help                 Print this help message and exit

        -classpath <path>     Path from which to load classes

        -bootclasspath <path> Path from which to load bootstrap classes

        -d <dir>              Output directory

        -o <file>             Output file (only one of -d or -o may be used)

        -jni                  Generate JNI-style header file (default)

        -version              Print version information

        -verbose              Enable verbose output

        -force                Always write output files

<classes> are specified with their fully qualified names (for

instance, java.lang.Object).

在 Sample1.java 上運行 javah.exe 的結果

下面的 Sample1.h 是對我們的 Java 代碼運行 javah 工具所生成的 C/C++ 頭文件:

/* DO NOT EDIT THIS FILE - it is machine generated */

#include <jni.h>

/* Header for class com_ibm_course_jni_Sample1 */

#ifndef _Included_com_ibm_course_jni_Sample1

#define _Included_com_ibm_course_jni_Sample1

#ifdef __cplusplus

extern "C" {

#endif

/*

 * Class:     com_ibm_course_jni_Sample1

 * Method:    intMethod

 * Signature: (I)I

 */

JNIEXPORT jint JNICALL Java_com_ibm_course_jni_Sample1_intMethod

  (JNIEnv *, jobject, jint);

/*

 * Class:     com_ibm_course_jni_Sample1

 * Method:    booleanMethod

 * Signature: (Z)Z

 */

JNIEXPORT jboolean JNICALL Java_com_ibm_course_jni_Sample1_booleanMethod

  (JNIEnv *, jobject, jboolean);

/*

 * Class:     com_ibm_course_jni_Sample1

 * Method:    stringMethod

 * Signature: (Ljava/lang/String;)Ljava/lang/String;

 */

JNIEXPORT jstring JNICALL Java_com_ibm_course_jni_Sample1_stringMethod

  (JNIEnv *, jobject, jstring);

/*

 * Class:     com_ibm_course_jni_Sample1

 * Method:    intArrayMethod

 * Signature: ([I)I

 */

JNIEXPORT jint JNICALL Java_com_ibm_course_jni_Sample1_intArrayMethod

  (JNIEnv *, jobject, jintArray);

#ifdef __cplusplus

}

#endif

#endif

關於 C/C++ 頭文件

正 如您可能已經注意到的那樣,Sample1.h 中的 C/C++ 函數說明和 Sample1.java 中的 Java native 方法聲明有很大差異。JNIEXPORT 和 JNICALL 是用於導出函數的、依賴於編譯器的指示符。返回類型是映射到 Java 類型的 C/C++ 類型。附錄 A:JNI 類型中完整地說明了這些類型。

除了 Java 聲明中的一般參數以外,所有這些函數的參數表中都有一個指向 JNIEnv 和 jobject 的指針。指向 JNIEnv 的指針實際上是一個指向函數指針表的指針。正如將要在步驟 4 中看到的,這些函數提供各種用來在 C 和 C++ 中操作 Java 數據的能力。

jobject 參數引用當前對象。因此,如果 C 或 C++ 代碼需要引用 Java 函數,則這個 jobject 充當引用或指針,返回調用的 Java 對象。函數名本身是由前綴“Java_”加全限定類名,再加下劃線和方法名構成的。

JNI類型

JNI 使用幾種映射到 Java 類型的本機定義的 C 類型。這些類型可以分成兩類:原始類型和僞類(pseudo-classes)。在 C 中,僞類作爲結構實現,而在 C++ 中它們是真正的類。

Java 原始類型直接映射到 C 依賴於平臺的類型,如下所示:

C 類型 jarray 表示通用數組。在 C 中,所有的數組類型實際上只是 jobject 的同義類型。但是,在 C++ 中,所有的數組類型都繼承了 jarray,jarray 又依次繼承了 jobject。下列表顯示了 Java 數組類型是如何映射到 JNI C 數組類型的。

這裏是一棵對象樹,它顯示了 JNI 僞類是如何相關的。

步驟 4:編寫 C/C++ 代碼

當談到編寫 C/C++ 函數實現時,有一點需要牢記:說明必須和 Sample1.h 的函數聲明完全一樣。我們將研究用於 C 實現和 C++ 實現的完整代碼,然後討論兩者之間的差異。

C函數實現

以下是 Sample1.c,它是用 C 編寫的實現:

  #include "com_ibm_course_jni_Sample1.h"

 #include <string.h>

  JNIEXPORT jint JNICALL Java_com_ibm_course_jni_Sample1_intMethod

    (JNIEnv *env, jobject obj, jint num) {

     return num * num;

  }

  JNIEXPORT jboolean JNICALL Java_com_ibm_course_jni_Sample1_booleanMethod

   (JNIEnv *env, jobject obj, jboolean boolean) {

   return !boolean;

}

JNIEXPORT jstring JNICALL Java_com_ibm_course_jni_Sample1_stringMethod

   (JNIEnv *env, jobject obj, jstring string) {

     const char *str = (*env)->GetStringUTFChars(env, string, 0);

     char cap[128];

     strcpy(cap, str);

     (*env)->ReleaseStringUTFChars(env, string, str);

     return (*env)->NewStringUTF(env, strupr(cap));

}

JNIEXPORT jint JNICALL Java_com_ibm_course_jni_Sample1_intArrayMethod

   (JNIEnv *env, jobject obj, jintArray array) {

     int i, sum = 0;

     jsize len = (*env)->GetArrayLength(env, array);

     jint *body = (*env)->GetIntArrayElements(env, array, 0);

     for (i=0; i<len; i++)

     {   sum += body[i];

     }

     (*env)->ReleaseIntArrayElements(env, array, body, 0);

     return sum;

}

void main(){}

C++ 函數實現

以下是 Sample1.cpp(C++ 實現)

#include "com_ibm_course_jni_Sample1.h"

#include <string.h>

JNIEXPORT jint JNICALL Java_Sample1_intMethod

  (JNIEnv *env, jobject obj, jint num) {

   return num * num;

}

JNIEXPORT jboolean JNICALL Java_Sample1_booleanMethod

   (JNIEnv *env, jobject obj, jboolean boolean) {

   return !boolean;

}

JNIEXPORT jstring JNICALL Java_Sample1_stringMethod

   (JNIEnv *env, jobject obj, jstring string) {

     const char *str = env->GetStringUTFChars(string, 0);

     char cap[128];

     strcpy(cap, str);

     env->ReleaseStringUTFChars(string, str);

     return env->NewStringUTF(strupr(cap));

}

JNIEXPORT jint JNICALL Java_Sample1_intArrayMethod

   (JNIEnv *env, jobject obj, jintArray array) {

     int i, sum = 0;

     jsize len = env->GetArrayLength(array);

     jint *body = env->GetIntArrayElements(array, 0);

     for (i=0; i<len; i++)

     {   sum += body[i];

     }

     env->ReleaseIntArrayElements(array, body, 0);

     return sum;

}

void main(){}

C 和 C++ 函數實現的比較

唯 一的差異在於用來訪問 JNI 函數的方法。在 C 中,JNI 函數調用由“(*env)->”作前綴,目的是爲了取出函數指針所引用的值。在 C++ 中,JNIEnv 類擁有處理函數指針查找的內聯成員函數。下面將說明這個細微的差異,其中,這兩行代碼訪問同一函數,但每種語言都有各自的語法。

C 語法: jsize len = (*env)->GetArrayLength(env,array);

C++ 語法: jsize len =env->GetArrayLength(array);

步驟 5:創建共享庫文件

接下來,我們創建包含本機代碼的共享庫文件。大多數 C 和 C++ 編譯器除了可以創建機器代碼可執行文件以外,也可以創建共享庫文件。用來創建共享庫文件的命令取決於您使用的編譯器。下面是在 Windows 和 Solaris 系統上執行的命令。

Windows: cl -Ic:\jdk\include -Ic:\jdk\include\win32 -LD Sample1.c -FeSample1.dll

Solaris: cc -G -I/usr/local/jdk/include -I/user/local/jdk/include/solaris Sample1.c -o Sample1.so

步驟 6:運行 Java 程序

最後一步是運行 Java 程序,並確保代碼正確工作。因爲必須在 Java 虛擬機中執行所有 Java 代碼,所以需要使用 Java 運行時環境。完成這一步的方法之一是使用 java,它是隨 SDK 一起提供的 Java 解釋器。所使用的命令是:

 

java com.ibm.course.jni.Sample1

當運行 Sample1.class 程序時,應該獲得下列結果:

PROMPT>java Sample1

intMethod: 25

booleanMethod: false

stringMethod: JAVA

intArrayMethod: 33

PROMPT>

故障排除

當使用 JNI 從 Java 程序訪問本機代碼時,您會遇到許多問題。您會遇到的三個最常見的錯誤是:

  • 無法找到動態鏈接 。它所產生的錯誤消息是:java.lang.UnsatisfiedLinkError。這通常指無法找到共享庫,或者無法找到共享庫內特定的本機方法。
  • 無法找到共享庫文件 。當用 System.loadLibrary(String libname) 方法(參數是文件名)裝入庫文件時,請確保文件名拼寫正確以及沒有 指定擴展名。還有,確保庫文件的位置在類路徑中,從而確保 JVM 可以訪問該庫文件。
  • 無法找到具有指定說明的方法 。確保您的 C/C++ 函數實現擁有與頭文件中的函數說明相同的說明。

從 Java 調用 C 或 C++ 本機代碼(雖然不簡單)是 Java 平臺中一種良好集成的功能。雖然 JNI 支持 C 和 C++,但 C++ 接口更清晰一些並且通常比 C 接口更可取。

正 如您已經看到的,調用 C 或 C++ 本機代碼需要賦予函數特殊的名稱,並創建共享庫文件。當利用現有代碼庫時,更改代碼通常是不可取的。要避免這一點,在 C++ 中,通常創建代理代碼或代理類,它們有專門的 JNI 所需的命名函數。然後,這些函數可以調用底層庫函數,這些庫函數的說明和實現保持不變。

 

Chap6: JNI傳遞返回值

作爲主調方的Java源程序TestJNI.java如下。

代碼清單15-4 在Linux平臺上調用C函數的例程——TestJNI.java

1.       public class TestJNI

2.       {

3.          static

4.          {

5.            System.loadLibrary("testjni");//載入靜態庫,test函數在其中實現

6.          }

7.      

8.          private native void testjni(); //聲明本地調用

9.         

10.       public void test()

11.       {

12.         testjni();

13.       }

14.   

15.       public static void main(String args[])

16.       {

17.         TestJNI haha = new TestJNI();

18.         haha.test();

19.       }

20.    }

TestJNI.java聲明從libtestjni.so(注意Linux平臺的動態鏈接庫文件的擴展名是.so)中調用函數testjni()。

在Linux平臺上,遵循JNI 規範的動態鏈接庫文件名必須以“lib”開頭。例如在上面的Java程序中指定的庫文件名爲“testjni”,則實際的庫文件應該命名爲“libtestjni.so”。

編譯TestJNI.java,併爲C程序生成頭文件:

javac TestJNI.java

javah TestJNI

提供testjni()函數的testjni.c源文件如下。

代碼清單15-5 在Linux平臺上調用C函數的例程——testjni.c

       #include <stdio.h>

      #include <TestJNI.h>  

     JNIEXPORT void JNICALL Java_TestJNI_testjni(JNIEnv *env, jobject obj){

      printf("haha---------go into c!!!\n");

     }

編寫Makefile文件如下,JDK安裝的位置請讀者自行調整:

libtestjni.so:testjni.o

     gcc -rdynamic -shared -o libtestjni.so testjni.o

testjni.o:testjni.c TestJNI.h

     gcc -c testjni.c -I./ -I/usr/java/jdk1.6.0_00/include -I/usr/java/jdk1.6.0_00/include/linux

在 Makefile文件中,我們描述了最終的 libtestjin.so依賴於目標文件testjni.o,而testjni.o則依賴於testjni.c源文件和TestJNI.h頭文件。請注 意,我們在將testjni.o連接成動態鏈接庫文件時使用了“-rdynamic”選項。

執行make命令編譯testjni.c。Linux平臺和在Windows平臺上類似,有3種方法可以讓Java程序找到並裝載動態鏈接庫文件。

— 將動態鏈接庫文件放置在當前路徑下。

— 將動態鏈接庫文件放置在LD_LIBRARY_PATH環境變量所指向的路徑下。注意這一點和Windows平臺稍有區別,Windows平臺參考PATH環境變量。

— 在啓動JVM時指定選項“-Djava.library.path”,將動態鏈接庫文件放置在該選項所指向的路徑下。

從下一節開始,我們開始接觸到在JNI 框架內Java調用C程序的一些高級話題,包括如何傳遞參數 、如何傳遞數組、如何傳遞對象等。

各種類型數據的傳遞是跨平臺、跨語言互操作的永恆話題,更復雜的操作其實都可以分解爲各種 基本數據類型的操作。只有掌握了基於各種數據類型的互操作,才能稱得上掌握了JNI 開發。從下一節開始,環境和步驟不再是闡述的重點,將不再花費專門的篇 幅,例程中的關鍵點將成爲我們關注的焦點。

15.2.2.3 傳遞字符串

到目前爲止,我們還沒有實現Java程序向C程序傳遞參數 ,或者C程序向Java程序傳遞參數 。本例程將由Java程序向C程序傳入一個字符串,C程序對該字符串轉成大寫形式後回傳給Java程序。

Java源程序如下。

代碼清單15-6 在Linux平臺上調用C函數的例程——Sample1

      public class Sample1

      {

                    public native String stringMethod(String text);

 

     public static void main(String[] args)

     {

         System.loadLibrary("Sample1");

          Sample1 sample = new Sample1();

          String text   = sample.stringMethod("Thinking In Java");

         System.out.println("stringMethod: " + text);

     }

}

Sample1.java以“Thinking In Java”爲參數 調用libSample1.so中的函數stringMethod(),在得到返回的字符串後打印輸出。

Sample1.c的源程序如下。

代碼清單15-7 在Linux平臺上調用C函數的例程——Sample1.c

      #include <Sample1.h>

      #include <string.h>   

      JNIEXPORT jstring JNICALL Java_Sample1_stringMethod(JNIEnv *env, jobject obj, jstring string)

      {

         const char *str = (*env)->GetStringUTFChars(env, string, 0);

         char cap[128];

         strcpy(cap, str);

         (*env)->ReleaseStringUTFChars(env, string, str);

       int i=0;

       for(i=0;i<strlen(cap);i++)

         *(cap+i)=(char)toupper(*(cap+i));

       return (*env)->NewStringUTF(env, cap);

    }

首先請注意函數頭部分,函數接收一個jstring類 型的輸入參數 ,並輸出一個jstring類型的參數 。jstring是jni .h中定義的數據類型,是JNI 框架內特有的字符串類型,因爲jni .h在 Sample1.h中被引入,因此在Sample1.c中無須再次引入。

程序的第4行是從JNI 調用上下文中獲取UTF編碼的輸入字符,將其放在指針str所指向 的一段內存中。第9行是釋放這段內存。第13行是將經過大寫轉換的字符串予以返回,這一句使用了NewStringUTF()函數,將C語言的字符串指針 轉換爲JNI 的jstring類型。JNIEnv也是在jni .h中定義的,代表JNI 調用的上下文,GetStringUTFChars()、 ReleaseStringUTFChars()和NewStringUTF()均是JNIEnv的函數。

15.2.2.4 傳遞整型數組

本節例程將首次嘗試在JNI 框架內啓用數組:C程序向Java程序返回一個定長的整型數組成的數組,Java程序將該數組打印輸出。

Java程序的源代碼如下。

代碼清單15-8 在Linux平臺上調用C函數的例程——Sample2

       public class Sample2

      {

        public native int[] intMethod();

 

      public static void main(String[] args)

      {

         System.loadLibrary("Sample2");

         Sample2 sample=new Sample2();

           int[] nums=sample.intMethod();

        for(int i=0;i<nums.length;i++)

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

     }

}

Sample2.java調用libSample2.so中的函數intMethod()。Sample2.c的源代碼如下。

代碼清單15-9 在Linux平臺上調用C函數的例程——Sample2.c

      #include <Sample2.h>

 

      JNIEXPORT jintArray JNICALL Java_Sample2_intMethod(JNIEnv *env, jobject obj)

    {

           int i = 1;

            jintArray  array;//定義數組對象

           array = (*env)-> NewIntArray(env, 10);

           for(; i<= 10; i++)

               (*env)->SetIntArrayRegion(env, array, i-1, 1, &i);

 

       /* 獲取數組對象的元素個數 */

      int len = (*env)->GetArrayLength(env, array);

 

       /* 獲取數組中的所有元素 */

      jint* elems = (*env)-> GetIntArrayElements(env, array, 0);

 

     for(i=0; i<len; i++)

        printf("ELEMENT %d IS %d\n", i, elems[i]);

 

return array;

    }

Sample2.c涉及了兩個jni .h定義的整型數相關的數據類型:jint和jintArray,jint是在JNI 框 架內特有的整數類型。程序的第7行開闢出一個長度爲10 的jint數組。然後依次向該數組中放入元素1-10。第11行至第16行不是程序的必須部分,純粹是爲了向讀者們演示GetArrayLength() 和GetIntArrayElements()這兩個函數的使用方法,前者是獲取數組長度,後者則是獲取數組的首地址以便於遍歷數組。

15.2.2.5 傳遞字符串數組

本節例程是對上節例程的進一步深化:雖然仍然是傳遞數組 ,但是數組的基類換成了字符串這樣一種對象數據類型。Java程序將向C程序傳入一個包含中文字符的字符串,C程序並沒有處理這個字符串,而是開闢出一個新的字符串數組返回給Java程序,其中還包含兩個漢字字符串。

Java程序的源代碼如下。

代碼清單15-10 在Linux平臺上調用C函數的例程——Sample3

      public class Sample3

      {

         public native String[] stringMethod(String text);

 

        public static void main(String[] args)

 throws java.io.UnsupportedEncodingException

        {

          System.loadLibrary("Sample3");

           Sample3 sample = new Sample3();

           String[] texts = sample.stringMethod("java編程思想");

       for(int i=0;i<texts.length;i++)

        {

            texts[i]=new String(texts[i].getBytes("ISO8859-1"),"GBK");

            System.out.print( texts[i] );

       }

         System.out.println();

      }

    }

Sample3.java調用libSample3.so中的函數stringMethod()。Sample3.c的源代碼如下:

代碼清單15-11 在Linux平臺上調用C函數的例程——Sample3.c

       #include <Sample3.h>

      #include <string.h>

      #include <stdlib.h>

 

      #define ARRAY_LENGTH 5   

     JNIEXPORT jobjectArray JNICALL Java_Sample3_stringMethod

(JNIEnv *env, jobject obj, jstring string)

       {   

         jclass objClass = (*env)->FindClass(env, "java/lang/String");

        jobjectArray texts= (*env)->NewObjectArray(env,

(jsize)ARRAY_LENGTH, objClass, 0);

         jstring jstr;

          char* sa[] = { "Hello,", "world!", "JNI ", "很", "好玩" };

         int i=0;

         for(;i<ARRAY_LENGTH;i++)

          {

            jstr = (*env)->NewStringUTF( env, sa[i] );

            (*env)->SetObjectArrayElement(env, texts, i, jstr);//必須放入jstring

         }

        return texts;

   }

第9、10行是我們需要特別關注的地方:JNI 框架並 沒有定義專門的字符串數組,而是使用jobjectArray——對象數組,對象數組的基類是jclass,jclass是JNI 框架內特有的類型,相當 於Java語言中的Class類型。在本例程中,通過FindClass()函數在JNI 上下文中獲取到java.lang.String的類型 (Class),並將其賦予jclass變量。

在例程中我們定義了一個長度爲5的對象數組texts,並在程序的第18行向其中循環放入預先定義好的sa數組中的字符串,當然前置條件是使用NewStringUTF()函數將C語言的字符串轉換爲jstring類型。

本例程的另一個關注點是C程序向Java程序傳遞的中文字符,在Java程序中能否正常顯 示的問題。在筆者的試驗環境中,Sample3.c是在Linux平臺上編輯的,其中的中文字符則是用支持GBK的輸入法輸入的,而Java程序採用 ISO8859_1字符集存放JNI 調用的返回字符,因此在“代碼清單15-10在Linux平臺上調用C函數的例程——Sample3”的第14行中將其轉碼後輸出。

15.2.2.6 傳遞對象數組

本節例程演示的是C程序向Java程序傳遞對象數組,而且對象數組中存放的不再是字符串,而是一個在Java中自定義的、含有一個topic屬性的MailInfo對象類型。

MailInfo對象定義如下。

代碼清單15-12 在Linux平臺上調用C函數的例程——MailInfo

     public class MailInfo {

         public String topic;

       public String getTopic()

       {

           return this.topic;

        }

 

public void setTopic(String topic)

   {

    this.topic=topic;

   }

   }

 

Java程序的源代碼如下。

代碼清單15-13 在Linux平臺上調用C函數的例程——Sample4

       public class Sample4

      {

      public native MailInfo[] objectMethod(String text);

 

      public static void main(String[] args)

      {

          System.loadLibrary("Sample4");

         Sample4 sample = new Sample4();

          MailInfo[] mails = sample.objectMethod("Thinking In Java");

        for(int i=0;i<mails.length;i++)

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

    }

}

Sample4.java調用libSample4.so中的objectMethod()函數。Sample4.c的源代碼如下。

代碼清單15-14 在Linux平臺上調用C函數的例程——Sample4.c

     #include <Sample4.h>

     #include <string.h>

     #include <stdlib.h>    

     #define ARRAY_LENGTH 5

 

      JNIEXPORT jobjectArray JNICALL Java_Sample4_objectMethod(

JNIEnv *env, jobject obj, jstring string)

       {  

    jclass objClass = (*env)->FindClass(env, "java/lang/Object");

   jobjectArray mails= (*env)->NewObjectArray(env,

(jsize)ARRAY_LENGTH, objClass, 0);

    jclass  objectClass = (*env)->FindClass(env, "MailInfo");

    jfieldID topicFieldId = (*env)->GetFieldID(env, objectClass,

"topic", "Ljava/lang/String;");

 

    int i=0;

    for(;i<ARRAY_LENGTH;i++)

    {

        (*env)->SetObjectField(env, obj, topicFieldId, string);

         (*env)->SetObjectArrayElement(env, mails, i, obj);

       }

 

    return mails;

     }

程序的第9、10行讀者們應該不會陌生,在上一節的例 程中已經出現過,不同之處在於這次通過FindClass()函數在JNI 上下文中獲取的是java.lang.Object的類型(Class),並將 其作爲基類開闢出一個長度爲5的對象數組,準備用來存放MailInfo對象。

程序的第12、13行的目的則是創建一個jfieldID類型的變量,在JNI 中,操作對 象屬性都是通過jfieldID進行的。第12行首先查找得到MailInfo的類型(Class),然後基於這個jclass進一步獲取其名爲 topic的屬性,並將其賦予jfieldID變量。

程 序的第18、19行的目的是循環向對象數組中放入jobject對象。 SetObjectField()函數屬於首次使用,該函數的作用是向jobject的屬性賦值,而值的內容正是Java程序傳入的jstring變量 值。請注意在向對象屬性賦值和向對象數組中放入對象的過程中,我們使用了在函數頭部分定義的jobject類型的環境參數 obj作爲中介。至此,JNI 框 架固有的兩個環境入參env和obj,我們都有涉及。

 

 

Chap7:Jni中C++和Java的參數傳遞

 

如何使用JNI的一些基本方法和過程在網上多如牛毛,如果你對Jni不甚瞭解,不知道Jni是做什麼的,如何建立一個基本的jni程序,或許可以參考下面下面這些文章:

 

<利用VC++6.0實現JNI的最簡單的例子>

<JNI入門教程之HelloWorld篇>

<SUN JNI Tutorial>

這 些資料的例子中,大多數只是輸入一些簡單的參數,獲取沒有參數。而在實際的使用過程中,往往需要對參數進行處理轉換。纔可以被C/C++程序識別。比如我 們在C++中有一個結構(Struct)DiskInfo ,需要傳遞一個類似於DiskInfo *pDiskInfo的參數,類似於在C++這樣參數如何傳遞到Java中呢?下面我們就來討論C++到Java中方法的一些常見參數的轉換:

 

1.定義Native Java類:

如果你習慣了使用JNI,你就不會覺得它難了。既然本地方法是由其他語言實現的,它們在Java中沒有函數體。但是,所有本地代碼必須用本地關鍵詞native聲明,成爲Java類的成員。假設我們在C++中有這麼一個結構,它用來描述硬盤信息:

 

//硬盤信息

struct {

    char name[256];

    int serial;

}DiskInfo;

那麼我們需要在Java中定義一個類來與之匹配,聲明可以寫成這樣:

class DiskInfo {

    //名字

    public String name;

    //序列號

    public int serial;

}

 

 

在這個類中,申明一些Native的本地方法,來測試方法參數的傳遞,分別定義了一些函數,用來傳遞結構或者結構數組,具體定義如下面代碼:

/**//****************** 定義本地方法 ********************/
    //輸入常用的數值類型(Boolean,Byte,Char,Short,Int,Float,Double)
    public native void displayParms(String showText, int i, boolean bl);

    //調用一個靜態方法
    public native int add(int a, int b);

    //輸入一個數組 
    public native void setArray(boolean[] blList);

    //返回一個字符串數組 
    public native String[] getStringArray();

    //返回一個結構
    public native DiskInfo getStruct();

//返回一個結構數組 
    public native DiskInfo[] getStructArray();

2.編譯生成C/C++頭文件

定義好了Java類之後,接下來就要寫本地代碼。本地方法符號提供一個滿足約定的頭文件,使用Java工具Javah可以很容易地創建它而不用手動去創建。你對Java的class文件使用javah命令,就會爲你生成一個對應的C/C++頭文件。

1)、在控制檯下進入工作路徑,本工程路徑爲:E:\work\java\workspace\JavaJni。

2)、運行javah 命令:javah -classpath E:\work\java\workspace\JavaJni com.sundy.jnidemo ChangeMethodFromJni

本文生成的C/C++頭文件名爲: com_sundy_jnidemo_ChangeMethodFromJni.h

 

3.在C/C++中實現本地方法

 

生 成C/C++頭文件之後,你就需要寫頭文件對應的本地方法。注意:所有的本地方法的第一個參數都是指向JNIEnv結構的。這個結構是用來調用JNI函數 的。第二個參數jclass的意義,要看方法是不是靜態的(static)或者實例(Instance)的。前者,jclass代表一個類對象的引用,而 後者是被調用的方法所屬對象的引用。

 

返回值和參數類型根據等價約定映射到本地C/C++類型,如表JNI類型映射所示。有些類型,在本地代碼中可直接使用,而其他類型只有通過JNI調用操作。

表A ※   JNI 類型映射

Java類型 本地類型 描述

boolean jboolean C/C++8位整型

byte jbyte C/C++帶符號的8位整型

char jchar C/C++無符號的16位整型

short jshort C/C++帶符號的16位整型

int jint C/C++帶符號的32位整型

long jlong C/C++帶符號的64位整型e

float jfloat C/C++32位浮點型

double jdouble C/C++64位浮點型

Object jobject 任何Java對象,或者沒有對應java類型的對象

Class jclass Class對象

String jstring 字符串對象

Object[] jobjectArray 任何對象的數組

boolean[] jbooleanArray 布爾型數組

byte[] jbyteArray 比特型數組

char[] jcharArray 字符型數組

short[] jshortArray 短整型數組

int[] jintArray 整型數組

long[] jlongArray 長整型數組

float[] jfloatArray 浮點型數組

double[] jdoubleArray 雙浮點型數組

 

 

3.1 使用數組:

JNI通過JNIEnv提供的操作Java數組的功能。它提供了兩個函數:一個是操作java的簡單型數組的,另一個是操作對象類型數組的。

因爲速度的原因,簡單類型的數組作爲指向本地類型的指針暴露給本地代碼。因此,它們能作爲常規的數組存取。這個指針是指向實際的Java數組或者Java數組的拷貝的指針。另外,數組的佈置保證匹配本地類型。

爲了存取Java簡單類型的數組,你就要要使用GetXXXArrayElements函數(見表B),XXX代表了數組的類型。這個函數把Java數組看成參數,返回一個指向對應的本地類型的數組的指針。

表B

函數                  Java數組類型 本地類型

GetBooleanArrayElements jbooleanArray jboolean

GetByteArrayElements jbyteArray jbyte

GetCharArrayElements jcharArray jchar

GetShortArrayElements jshortArray jshort

GetIntArrayElements jintArray jint

GetLongArrayElements jlongArray jlong

GetFloatArrayElements jfloatArray jfloat

GetDoubleArrayElements jdoubleArray jdouble

 

 

JNI數組存取函數

當 你對數組的存取完成後,要確保調用相應的ReleaseXXXArrayElements函數,參數是對應Java數組和 GetXXXArrayElements返回的指針。如果必要的話,這個釋放函數會複製你做的任何變化(這樣它們就反射到java數組),然後釋放所有相 關的資源。

爲了使用java對象的數組,你必須使用GetObjectArrayElement函數和SetObjectArrayElement函數,分別去get,set數組的元素。GetArrayLength函數會返回數組的長度。

 

3.2 使用對象

JNI 提供的另外一個功能是在本地代碼中使用Java對象。通過使用合適的JNI函數,你可以創建Java對象,get、set 靜態(static)和實例(instance)的域,調用靜態(static)和實例(instance)函數。JNI通過ID識別域和方法,一個域或 方法的ID是任何處理域和方法的函數的必須參數。

 

表C列出了用以得到靜態(static)和實例(instance)的域與方法的JNI函數。每個函數接受(作爲參數)域或方法的類,它們的名稱,符號和它們對應返回的jfieldID或jmethodID。

 

表C

函數 描述

GetFieldID 得到一個實例的域的ID

GetStaticFieldID 得到一個靜態的域的ID

GetMethodID 得到一個實例的方法的ID

GetStaticMethodID 得到一個靜態方法的ID

※域和方法的函數

如果你有了一個類的實例,它就可以通過方法GetObjectClass得到,或者如果你沒有這個類的實例,可以通過FindClass得到。符號是從域的類型或者方法的參數,返回值得到字符串,如表D所示。

 

表D

Java類型   符號

boolean Z

byte B

char C

short S

int I

long L

float F

double D

void V

objects對象 Lfully-qualified-class-name;L類名

Arrays數組 [array-type [數組類型

methods方法 (argument-types)return-type(參數類型)返回類型

 

※確定域和方法的符號

下面我們來看看,如果通過使用數組和對象,從C++中的獲取到Java中的DiskInfo 類對象,並返回一個DiskInfo數組:

//返回一個結構數組,返回一個硬盤信息的結構數組

JNIEXPORT jobjectArray JNICALL

Java_com_sundy_jnidemo_ChangeMethodFromJni_getStructArray

(JNIEnv *env, jobject _obj)

{

    //申明一個object數組

    jobjectArray args = 0;

    //數組大小

    jsize        len = 5;

    //獲取object所屬類,一般爲java/lang/Object就可以了

    jclass objClass = (env)->FindClass("java/lang/Object");

    //新建object數組

    args = (env)->NewObjectArray(len, objClass, 0);

    /**//* 下面爲獲取到Java中對應的實例類中的變量*/

    //獲取Java中的實例類

    jclass objectClass = (env)->FindClass("com/sundy/jnidemo/DiskInfo");

    //獲取類中每一個變量的定義

    //名字

    jfieldID str = (env)->GetFieldID(objectClass,"name","Ljava/lang/String;");

    //序列號

    jfieldID ival = (env)->GetFieldID(objectClass,"serial","I");

    //給每一個實例的變量付值,並且將實例作爲一個object,添加到objcet數組中

    for(int i=0; i < len; i++ )

    {

        //給每一個實例的變量付值

        jstring jstr = WindowsTojstring(env,"我的磁盤名字是 D:");

        //(env)->SetObjectField(_obj,str,(env)->NewStringUTF("my name is D:"));

        (env)->SetObjectField(_obj,str,jstr);

        (env)->SetShortField(_obj,ival,10);

        //添加到objcet數組中

        (env)->SetObjectArrayElement(args, i, _obj);

    }

    //返回object數組

    return args;

}

全部的C/C++方法實現代碼如下:

/**//*

*

* 一縷陽光(sundy)版權所有,保留所有權利。

*/

/**//**

*

* TODO Jni 中一個從Java到C/C++參數傳遞測試類

*

* @author 劉正偉(sundy)

* @see http://www.cnweblog.com/sundy

* @see mailto:[email protected]

* @version 1.0

* @since 2005-4-30

*

* 修改記錄:

*

* 日期              修改人                 描述

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

*

*

*

*/

// JniManage.cpp : 定義 DLL 應用程序的入口點。

//

package com.sundy.jnidemo;

#include "stdafx.h"

#include <stdio.h>

#include <math.h>

#include "jni.h"

#include "jni_md.h"

#include "./head/Base.h"

#include "head/wmi.h"

#include "head/com_sundy_jnidemo_ChangeMethodFromJni.h" //通過javah –jni javactransfer 生成

#include <stdio.h>

#include "stdlib.h"

#include "string.h"

#pragma comment (lib,"BaseInfo.lib")

#pragma comment (lib,"jvm.lib")

//硬盤信息

struct {

    char name[256];

    int serial;

}DiskInfo;

 

/**//*BOOL APIENTRY DllMain( HANDLE hModule,

                       DWORD ul_reason_for_call,

                       LPVOID lpReserved

                     )

{

    LPTSTR strName = new CHAR[256] ;

    (*GetHostName)(strName);

    printf("%s\n",strName);

    delete [] strName;

    return TRUE;

}*/

//將jstring類型轉換成windows類型

char* jstringToWindows( JNIEnv *env, jstring jstr );

//將windows類型轉換成jstring類型

jstring WindowsTojstring( JNIEnv* env, char* str );

//主函數

BOOL WINAPI DllMain(HANDLE hHandle, DWORD dwReason, LPVOID lpReserved)

{

    return TRUE;

}

//輸入常用的數值類型 Boolean,Byte,Char,Short,Int,Float,Double

JNIEXPORT void JNICALL Java_com_sundy_jnidemo_ChangeMethodFromJni_displayParms

(JNIEnv *env, jobject obj, jstring s, jint i, jboolean b)

{

    const char* szStr = (env)->GetStringUTFChars(s, 0 );

    printf( "String = [%s]\n", szStr );

    printf( "int = %d\n", i );

    printf( "boolean = %s\n", (b==JNI_TRUE ? "true" : "false") );

    (env)->ReleaseStringUTFChars(s, szStr );

}

//調用一個靜態方法,只有一個簡單類型輸出

JNIEXPORT jint JNICALL Java_com_sundy_jnidemo_ChangeMethodFromJni_add

(JNIEnv *env, jobject, jint a, jint b)

{

    int rtn = (int)(a + b);

    return (jint)rtn;

}

/**/////輸入一個數組,這裏輸入的是一個Boolean類型的數組

JNIEXPORT void JNICALL Java_com_sundy_jnidemo_ChangeMethodFromJni_setArray

(JNIEnv *env, jobject, jbooleanArray ba)

{

    jboolean* pba = (env)->GetBooleanArrayElements(ba, 0 );

    jsize len = (env)->GetArrayLength(ba);

    int i=0;

    // change even array elements

    for( i=0; i < len; i+=2 )

    {

        pba[i] = JNI_FALSE;

        printf( "boolean = %s\n", (pba[i]==JNI_TRUE ? "true" : "false") );

    }

    (env)->ReleaseBooleanArrayElements(ba, pba, 0 );

}

/**/////返回一個字符串數組

JNIEXPORT jobjectArray JNICALL Java_com_sundy_jnidemo_ChangeMethodFromJni_getStringArray

(JNIEnv *env, jobject)

{

    jstring      str;

    jobjectArray args = 0;

    jsize        len = 5;

    char*        sa[] = { "Hello,", "world!", "JNI", "is", "fun" };

    int          i=0;

    args = (env)->NewObjectArray(len,(env)->FindClass("java/lang/String"),0);

    for( i=0; i < len; i++ )

    {

        str = (env)->NewStringUTF(sa[i] );

        (env)->SetObjectArrayElement(args, i, str);

    }

    return args;

}

//返回一個結構,這裏返回一個硬盤信息的簡單結構類型

JNIEXPORT jobject JNICALL Java_com_sundy_jnidemo_ChangeMethodFromJni_getStruct

(JNIEnv *env, jobject obj)

{

    /**//* 下面爲獲取到Java中對應的實例類中的變量*/

    //獲取Java中的實例類

    jclass objectClass = (env)->FindClass("com/sundy/jnidemo/DiskInfo");

    //獲取類中每一個變量的定義

    //名字

    jfieldID str = (env)->GetFieldID(objectClass,"name","Ljava/lang/String;");

    //序列號

    jfieldID ival = (env)->GetFieldID(objectClass,"serial","I");

 

    //給每一個實例的變量付值

    (env)->SetObjectField(obj,str,(env)->NewStringUTF("my name is D:"));

    (env)->SetShortField(obj,ival,10);

 

    return obj;

}

//返回一個結構數組,返回一個硬盤信息的結構數組

JNIEXPORT jobjectArray JNICALL Java_com_sundy_jnidemo_ChangeMethodFromJni_getStructArray

(JNIEnv *env, jobject _obj)

{

    //申明一個object數組

    jobjectArray args = 0;

 

    //數組大小

    jsize        len = 5;

    //獲取object所屬類,一般爲ava/lang/Object就可以了

    jclass objClass = (env)->FindClass("java/lang/Object");

    //新建object數組

    args = (env)->NewObjectArray(len, objClass, 0);

    /**//* 下面爲獲取到Java中對應的實例類中的變量*/

    //獲取Java中的實例類

    jclass objectClass = (env)->FindClass("com/sundy/jnidemo/DiskInfo");

 

    //獲取類中每一個變量的定義

    //名字

    jfieldID str = (env)->GetFieldID(objectClass,"name","Ljava/lang/String;");

    //序列號

    jfieldID ival = (env)->GetFieldID(objectClass,"serial","I");

    //給每一個實例的變量付值,並且將實例作爲一個object,添加到objcet數組中

    for(int i=0; i < len; i++ )

    {

        //給每一個實例的變量付值

        jstring jstr = WindowsTojstring(env,"我的磁盤名字是 D:");

        //(env)->SetObjectField(_obj,str,(env)->NewStringUTF("my name is D:"));

        (env)->SetObjectField(_obj,str,jstr);

        (env)->SetShortField(_obj,ival,10);

        //添加到objcet數組中

        (env)->SetObjectArrayElement(args, i, _obj);

    }

    //返回object數組

    return args;

}

//將jstring類型轉換成windows類型

char* jstringToWindows( JNIEnv *env, jstring jstr )

{

    int length = (env)->GetStringLength(jstr );

    const jchar* jcstr = (env)->GetStringChars(jstr, 0 );

    char* rtn = (char*)malloc( length*2+1 );

    int size = 0;

    size = WideCharToMultiByte( CP_ACP, 0, (LPCWSTR)jcstr, length, rtn,(length*2+1), NULL, NULL );

    if( size <= 0 )

        return NULL;

    (env)->ReleaseStringChars(jstr, jcstr );

    rtn[size] = 0;

    return rtn;

}

//將windows類型轉換成jstring類型

jstring WindowsTojstring( JNIEnv* env, char* str )

{

    jstring rtn = 0;

    int slen = strlen(str);

    unsigned short * buffer = 0;

    if( slen == 0 )

        rtn = (env)->NewStringUTF(str );

    else

    {

        int length = MultiByteToWideChar( CP_ACP, 0, (LPCSTR)str, slen, NULL, 0 );

        buffer = (unsigned short *)malloc( length*2 + 1 );

        if( MultiByteToWideChar( CP_ACP, 0, (LPCSTR)str, slen, (LPWSTR)buffer, length ) >0 )

            rtn = (env)->NewString( (jchar*)buffer, length );

    }

    if( buffer )

        free( buffer );

    return rtn;

}

Java 測試native代碼

這沒有什麼多說的,看代碼吧

//主測試程序

    public static void main(String[] args) {

        ChangeMethodFromJni changeJni = new ChangeMethodFromJni();

        //輸入常用的數值類型(string int boolean)

        System.out

                .println("------------------輸入常用的數值類型(string int boolean)-----------");

        changeJni.displayParms("Hello World!", 100, true);

        //調用一個靜態方法

        System.out.println("------------------調用一個靜態方法-----------");

        int ret = changeJni.add(12, 20);

        System.out.println("The result is: " + String.valueOf(ret));

        //輸入一個數組

        System.out.println("------------------輸入一個數組-----------");

        boolean[] blList = new boolean[] { true, false, true };

        changeJni.setArray(blList);

        //返回一個字符串數組

        System.out.println("------------------返回一個字符串數組-----------");

        String[] strList = changeJni.getStringArray();

        for (int i = 0; i < strList.length; i++) {

            System.out.print(strList[i]);

        }

        System.out.println();

        System.out.println("------------------返回一個結構-----------");

        //返回一個結構

        DiskInfo disk = changeJni.getStruct();

        System.out.println("name:" + disk.name);

        System.out.println("Serial:" + disk.serial);

        //返回一個結構數組

        System.out.println("------------------返回一個結構數組 -----------");

        DiskInfo[] diskList = changeJni.getStructArray();

        for (int i = 0; i < diskList.length; i++) {

            System.out.println("name:" + diskList[i].name);

            System.out.println("Serial:" + diskList[i].serial);

        }

    }

注:本程序在VS2003,eclipse (jse5.0) winxp sp2編譯通過

 

posted on 2005-05-02 20:22 sundy 閱讀(4406) 評論(21) 編輯 收藏 所屬分類: Java

 

評論

# re: Jni中C++和Java的參數傳遞 2005-05-22 14:35 張磊

請問如果想返回byte[]類型該怎麼做 回覆 更多評論  

# re: Jni中C++和Java的參數傳遞 2005-05-23 08:37 sundy

因爲:

byte[] jbyteArray 比特型數組

所以你將byte[] 作爲一個jbyteArray數組傳遞就可以了

回覆 更多評論  

# re: Jni中C++和Java的參數傳遞 2005-09-21 14:46 小影

請問如果我想把在C++裏面計算好的一個二維數組傳回給java程序接受,該怎麼寫代碼呢?我找了很多這方面的書和資料,都沒有關於傳遞二維數組的介紹,請您給予指導,多謝啦^_^ 回覆 更多評論  

# re: Jni中C++和Java的參數傳遞 2005-09-21 17:47 sundy

我沒有直接傳遞過二維數組

但我想你可以把試一試二維數組轉換成爲一個Hashmap的數組傳出來。

請參考"如何在Jni中傳遞出Hashmap的數組?"的一些代碼

回覆 更多評論  

# re: Jni中C++和Java的參數傳遞 2005-12-26 16:32 wangjian

返回一個結構數組時,爲什麼每個對象的數據都是一樣的?即5個Diskinfo的成員值都相同,能不能不相同? 回覆 更多評論  

# re: Jni中C++和Java的參數傳遞 2005-12-26 16:55 wangjian

我把5個DiskInfo對象的成員serial分別設置爲1、2、3、4、5,可是傳遞到java後5個對象的serial成員值都是5,爲什麼這樣阿?盼回覆,多謝! 回覆 更多評論  

# re: Jni中C++和Java的參數傳遞 2005-12-27 21:51 sundy

//給每一個實例的變量付值,並且將實例作爲一個object,添加到objcet數組中

for(int i=0; i < len; i++ )

{

......

//添加到objcet數組中

(env)->SetObjectArrayElement(args, i, _obj);

}

你看看設置的_Obj是不是都是同一個??

回覆 更多評論  

# re: Jni中C++和Java的參數傳遞 2005-12-28 13:32 wangjian

如下所示,我就是把你程序中(env)->SetShortField(_obj,ival,10)的參數10換成i,結果每個對象都是對象的serial成員值都是4,請問怎樣實現多個不同對象的傳遞?

for(int i=0; i < len; i++ )

{

jstring jstr = WindowsTojstring(env,"我的磁盤名字是D:");

(env)->SetObjectField(_obj,str,jstr);

(env)->SetShortField(_obj,ival,i);

(env)->SetObjectArrayElement(args, i, _obj);

}

回覆 更多評論  

# re: Jni中C++和Java的參數傳遞 2005-12-28 15:15 sundy

應該沒有問題的呀,

SetObjectArrayElement的時候,_obj是不同的嗎?

要不你將for循環改爲:

jstring jstr = WindowsTojstring(env,"我的磁盤名字是C:");

(env)->SetObjectField(_obj,str,jstr);

(env)->SetShortField(_obj,ival,0);

(env)->SetObjectArrayElement(args, 0, _obj);

jstring jstr = WindowsTojstring(env,"我的磁盤名字是D:");

(env)->SetObjectField(_obj,str,jstr);

(env)->SetShortField(_obj,ival,1);

(env)->SetObjectArrayElement(args, 1, _obj);

看看對嗎?   回覆 更多評論  

# re: Jni中C++和Java的參數傳遞 2005-12-29 20:42 wangjian

這樣作不對,不過我找到正確的方法了,要用構造函數生成新的對象。 回覆 更多評論  

# re: Jni中C++和Java的參數傳遞 2006-01-17 11:07 luli

SQLRETURN SQLAllocHandle( SQLSMALLINT HandleType,

SQLHANDLE InputHandle,

SQLHANDLE * OutputHandlePtr

)

這是odbc api裏的一個函數 SQLHANDLE 是一個結構

c#裏的引用方式如下

[DllImport("ODBC32.dll")]

private static extern short SQLAllocHandle(short hType, IntPtr inputHandle, out IntPtr outputHandle);

但我不清楚 SQLHANDLE 結構具體怎麼構造的 因此我無法用java類來模擬

我是菜鳥 望解答 謝過了   回覆 更多評論  

# re: Jni中C++和Java的參數傳遞 2006-01-17 14:25 luli

忘了補充 SQLHANDLE InputHandle與SQLHANDLE * OutputHandlePtr

一個是結構 一個是結構指針 那我是否該如下模擬

class SQLHANDLE

{

}

public class test

{

SQLHANDLE a=new SQLHANDLE ();

public static void main(String args[]) {

int i=SQLAllocHandle( SQLSMALLINT HandleType, new SQLHANDLE(),a)

}

}

回覆 更多評論  

# re: Jni中C++和Java的參數傳遞 2006-03-21 17:31 Hefe

WideCharToMultiByte();

MultiByteToWideChar();

請問這兩個函數實現什麼功能,請作者給出代碼,多謝!

 

回覆 更多評論  

# re: Jni中C++和Java的參數傳遞 2006-03-22 08:47 sundy

@Hefe look here: http://www.google.com/search?hl=zh-CN&lr=lang_zh-CN&q=WideCharToMultiByte

回覆 更多評論  

# re: Jni中C++和Java的參數傳遞 2006-03-28 17:40 dijk

要在c函數中調用java類的類成員的方法,比如調用JEditorPane類型成員的setText方法,該怎麼辦? 回覆 更多評論  

# re: Jni中C++和Java的參數傳遞 2006-04-16 21:33 陳世雄

java中函數的處理中,對於對象類型(非基本類型int,long...)的輸入參數,函數中是可以修改輸入參數的內容,函數執行完畢,修改仍然是有效的。

jni中 是否也是這樣呢?

回覆 更多評論   

# re: Jni中C++和Java的參數傳遞 2006-04-18 17:50 王文波

你好:

向你請教一個問題:我想用jini來調用dll。我在jbuilder中新建的簡單的project調用jini運行正常。但是,我現在要對一個工程軟件進行二次開發,該軟件的

開發也使用jbuilder生成一個project,然後放在指定的路徑下就可以了,該軟件在運行的時候會自動讀取該project。我在這個軟件二次開發的project中使用

jini,則總是報錯:unsatisfiedlinkError get()。其中get()方法名。請問該怎麼解決這個問題?

我的郵箱:[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<WINDOWS.H>
#include<MALLOC.H>
#include<STDIO.H>
#include<jni .h>
#include "test_LinkDll.h"
#include "A.h"

JNIEXPORT jstring JNICALL Java _test_LinkDll_readDataTemp( JNIEnv *env, jobject jo, jint ji_icdev, jint ji_len )

    //*************************基本數據聲明與定義******************************
     HANDLE H_icdev = (HANDLE)ji_icdev;//設備標誌符
    __int16 i16_len = (__int16)ji_len;//讀出的數據長度,值爲3,即3個HEX形式的字符
    __int16 i16_result;//函數返回值
    __int16 i16_coverResult;//字符轉換函數的返回值
        int i_temp;//用於循環的中間變量 
      jchar jca_result[3] = { 'e', 'r', 'r' };//當讀數據錯誤時返回此字符串

    //無符號字符指針,指向的內存空間 用於存放讀出的HEX形式的數據字符串
    unsigned char* uncp_hex_passward = (unsigned char*)malloc( i16_len );

    //無符號字符指針,指向的內存空間 存放從HEX形式轉換爲ASC形式的數據字符串
    unsigned char* uncp_asc_passward = (unsigned char*)malloc( i16_len * 2 );

    //java char指針,指向的內存空間 存放從存放ASC形式數據字符串空間讀出的數據字符串
    jchar *jcp_data = (jchar*)malloc(i16_len*2+1); 

    //java String,存放從java char數組生成的String字符串,並返回給調用者
    jstring js_data = 0;

    //*********讀出3個HEX形式的數據字符到uncp_hex_data指定的內存空間 **********
    i16_result = readData( H_icdev, 6, uncp_hex_data );//這裏直接調用的是設備商提供的原型方法.

    if ( i16_result != 0 )
    {
        printf( "讀卡錯誤......\n" );
        //這個地方調用JNI 定義的方法NewString(jchar*,jint),把jchar字符串轉換爲JString類型數據,返回到java 程序中即是String
        return (env)->NewString( jca_result, 3 );
    }

    printf( "讀數據成功......\n" );

    //**************HEX形式的數據字符串轉換爲ASC形式的數據字符串**************
    i16_coverResult = hex_asc( uncp_hex_data, uncp_asc_data, 3 );
    if ( i16_coverResult != 0 )
    {
        printf( "字符轉換錯誤!\n" );
        return (env)->NewString( jca_result, 3 );
    }

    //**********ASC char形式的數據字符串轉換爲jchar形式的數據字符串***********
    for ( i_temp = 0; i_temp < i16_len; i_temp++ )  
        jcp_data[i_temp] = uncp_hex_data[i_temp];

    //******************jchar形式的數據字符串轉換爲java String****************
    js_data = (env)->NewString(jcp_data,i16_len);  

    return js_data;
}

動態調用,不需要lib文件,直接加載A.dll文件,並把其中的文件再次聲明,代碼如下:
#include<STDIO.H>
#include<WINDOWS.H>
#include "test_LinkDll.h"

//首先聲明一個臨時方法,這個方法名可以隨意定義,但參數同設備商提供的原型方法的參數保持一致.
typedef int ( *readDataTemp )( int, int, int, unsigned char * );//從指定地址 讀數據

//從指定地址 讀數據
JNIEXPORT jstring JNICALL Java _readDataTemp( JNIEnv *env, jobject jo, jint ji_icdev, jint ji_offset, jint ji_len )
{
    int i_temp;
    int i_result;
    int i_icdev = (int)ji_icdev;
    int i_offset = (int)ji_offset;
    int i_len = (int)ji_len; 
    jchar jca_result[5] = { 'e', 'r', 'r' };
    unsigned char *uncp_data = (unsigned char*)malloc(i_len); 
    jchar *jcp_data = (jchar *)malloc(i_len);
    jstring js_data = 0;

    //HINSTANCE是win32中同HANDLE類似的一種數據類型,意爲Handle to an instance,常用來標記App實例,在這個地方首先把A.dll加載到內存空間 ,以一個App的形式存放,然後取

得它的instance交給dllhandle,以備其它資源使用.
    HINSTANCE dllhandle; 
    dllhandle = LoadLibrary( "A.dll" ); 
    
    //這個地方首先定義一個已聲明過的臨時方法,此臨時方法相當於一個結構體,它和設備商提供的原型方法具有相同的參數結構,可互相轉換
    readDataTemp readData;

    //使用win32的GetProcAddress方法取得A.dll中定義的名爲readData的方法,並把這個方法轉換爲已被定義好的同結構的臨時方法,
    //然後在下面的程序中,就可以使用這個臨時方法了,使用這個臨時方法在這時等同於使用A.dll中的原型方法.
    readData = (readDataTemp) GetProcAddress( dllhandle, "readData" );

    i_result = (*readData)( i_icdev, i_offset, i_len, uncp_data );

    if ( i_result != 0 )
    {
        printf( "讀數據失敗......\n" );
        return (env)->NewString( jca_result, 3 );
    }

    for ( i_temp = 0; i_temp < i_len; i_temp++ )
    {
        jcp_data[i_temp] = uncp_data[i_temp];
    }

    js_data = (env)->NewString( jcp_data, i_len );

    return js_data; 
}

4>以上即是一個java 程序調用第三方dll文件的完整過程,當然,在整個過程的工作全部完成以後,就可以使用java 類LinkDll中的public String radData( int, int, int )方法了,效果同直接使用c/c++調用這個設備商提供的A.dll文件中的readData方法幾乎一樣.

總結:JNI 技術確實是提高了java 程序的執行效率,並且擴展了java 程序的功能,但它也確確實實破壞了java 程序的最重要的優點:平臺無關性,所以除非必須(不得不)使用JNI 技術,一般還是提倡寫100%純java 的程序.根據自己的經驗及查閱的一些資料,把可以使用JNI 技術的情況羅列如下:
    1>需要直接操作物理設備,而沒有相關的驅動程序,這時候我們可能需要用C甚至彙編語言來編寫該設備的驅動,然後通過JNI 調用; 
    2>涉及大量數學運算的部分,用java 會帶來些效率上的損失; 
    3>用java 會產生系統難以支付的開銷,如需要大量網絡鏈接的場合; 
    4>存在大量可重用的c/c++代碼,通過JNI 可以減少開發工作量,避免重複開發. 
另外,在利用JNI 技術的時候要注意以下幾點:
    1>由於Java 安全機制的限制,不要試圖通過Jar文件的方式發佈包含本地化方法的Applet到客戶端; 
    2>注意內存管理問題,雖然在本地方法返回Java 後將自動釋放局部引用,但過多的局部引用將使虛擬機在執行本地方法時耗盡內存; 
    3>JNI 技術不僅可以讓java 程序調用c/c++代碼,也可以讓c/c++代碼調用java 代碼.

注: 有一個名叫Jawin開源項目實現了直接讀取第三方dll文件,不用自己辛苦去手寫一個起傳值轉換作用的dll文件,有興趣的可以研究一下.但 是我用的時候不太順手,有很多規則限制,像自己寫程序時可以隨意定義返回值,隨意轉換類型,用這個包的話這些都是不可能的了,所以我的項目還沒開始就把它 拋棄了.

 

Chap9:如何編寫jni方法(轉載)

 

作者:crazycow 發佈時間:2008-11-16 14:44:21.0

http://blog.chinaunix.net/u1/38994/showart_1099528.html 
一、概述:

在 這篇文章中將會簡單介紹如何編制一些簡單的JNI 方法。我們都知道JNI方法可以幫助我們調用用C/c++編寫的函數,這樣如果一項工作已經用C /c++語言實現的話,我們就可以不用花很大的力氣再用JAVA語言對這一工作進行再實現,只要編制相應的JNI函數,就可以輕鬆實現JAVA語言對C /c++函數的調用,從而大大減輕程序開發人員的工作量。 

在這個項目中,我們編制了很多小實例,通過閱讀,運行這些小實例,你可以輕鬆的學會如何編制JNI方法。這篇文檔可以幫助你更好的理解及實現這些實例。 

現 在讓我們進入主題。首先,我們看一下這個項目的體系構架。該項目分爲兩部分,一部分用c語言是c語言的例子,另一部分是c++語言的例子。每部分都包含 java,src源文件目錄,以及一個Makefile文件。java目錄中是需要調用JNI函數的JAVA源程序,含有後綴名.java。src 目錄 中含有JNI函數的實現代碼,包括.c或.cpp文件和.h文件。Makefile文件是對 java 、src 目錄下的文件進行編譯組織進而生成可執 行文件的文件。當Makefile文件執行以後還會生成以下子目錄:lib , class ,bin目錄 。lib 目錄中包含項目中生成的靜態函數庫 文件libJNIExamples.so,java程序所調用的JNI方法都是通過這個庫來調用的。class 目錄中包含由java目錄下 的.java 文件生成的.class文件。bin目錄中是一個可執行的shell腳本文件。在執行該腳本的時候,項目所有程序實例的運行結果都將一併顯 示在屏幕上。 


具體執行步驟爲: 

make 

cd bin 

./run.sh 


下面來介紹一下在這個項目中所實現的實例: 

   1. 如何調用標準C/c++中的函數--例如:printf(...) 
   2. 如何調用C/c++中自定義的函數 
   3. 如何在jni函數中訪問java類中的對象實例域 
   4. 如何在jni函數中訪問java類中的靜態實例域 
   5. 如何在jni函數中調用java對象的方法 
   6. 如何在jni函數中調用java類的靜態方法 
   7. 如何在jni函數中傳遞基本數據類型參數 
   8. 如何在jni函數中傳遞對象類型參數 
   9. 如何在jni函數中處理字符串 
  10. 如何在jni函數中處理數組 
  11. 處理jni函數中的返回值情況 
  12. 在jni中實現創建java類對象 


二、基本步驟: 

在介紹這些例子之前,讓我們先來看看編寫jni方法所需要的基本步驟,這些實例都是用c來實例來講解,至於c++的實例和c的實例區別不大,只要作稍微的修改即可,在文檔的末尾我們將介紹這些內容: 

1、要想定義jni方法,首先得要在java語言中對這一方法進行聲明(自然這一聲明過程要在類中進行) 

聲明格式如下:

publicnativevoid print();   System.loadLibrary(“JNIExamples”);   }  


jni 函數用關鍵字native方法聲明。 

2、對該類的源文件進行編譯使用javac命令,生成相應的.class文件。 
3、用javah -jni爲函數生成一個在java調用和實際的c函數之間的轉換存根,該存根通過從虛擬機棧中取出參數信息,並將其傳遞給已編譯的C函數來實現轉換。 
4、建立一個特殊的共享庫,並從該共享庫到處這個存根,在上面的例子中使用了System.loadLibrary,來加載libJNIExamples共享庫。 


三、配置運行環境: 

在 編寫一個簡單的jni函數之前我們必須配置相應的運行環境。jdk的配置在這裏就不作介紹,這裏主要說的是庫的路徑。當調用 System.loadLibrary(..)時,編譯器會到我們系統設置的庫路徑中尋找該庫。修改路徑的方法和修改任何環境變量的方法基本相同,只要在 /etc/bash.bashrc目錄下增加一行LD_LIBRARY_PATH=.:./lib:$(LD_LIBRARY_PATH)即可。也可以通 過命令行export LD_LIBRARY_PATH=.:./lib:$(LD_LIBRARY_PATH) 


四、運行實例分析:

1、實例一:在jni中調用標準c中自帶的函數printf():

 

下面以實例1爲例來詳細說明編寫jni方法的詳細過程。 

(1)、定義包含jni函數的類Print.java:

{  

 /*********************************************************************** * the print() function will call the printf() funcion which is a ANSI c funciton * *************************************************************************/

Public native  void print();      

 System.loadLibrary("JNIExamples");    

 }   

}  



在 上面的實例 中,使用public native void print();語句來定義了一個Print類的jni方法。並用 Sysgem.loadLibrary(“JNIExamples”)語句來加載libJNIExamples.so庫。注意:加載的語句一定要用 static關鍵字聲明在靜態塊中,以保證引用該類時該庫始終被加載。 

(2)、對該類進行編譯:javac Print.java。生成Print.class類,然後用javah 產生一個Print.h的頭文件:javah Print。長生的Print.h文件格式如下:

/* DO NOT EDIT THIS FILE - it is machine generated *//* Header for class Print */  JNIEXPORT void JNICALL Java_Print_print     (JNIEnv *, jobject);   }   


其中的加粗字體爲要實現的JNI函數生命部分。 

(3)、編寫JNI函數的實現部分Print.c

JNIEXPORT void JNICALL Java_Print_print (JNIEnv *env, jobject obj)   

{     

printf("example1:in this example a printf() function in ANSI C is called\n");     

printf("Hello,the output is generated by printf() function in ANSI C\n");  

 }  


在這個文件中實現了一個簡單的Jni方法。該方法調用ANSI C 中的printf()函數,輸出了兩個句子。

 

(4)、將本地函數編譯到libJNIExamples.so的庫中: 
使用語句:gcc -fPIC -I/usr/jdk1.5/include -I/usr/jdk1.5/include/linux -shared -o libJNIExamples.so Print.c。 

(5)、至此Jni函數已全部實現,可以在java代碼中調用拉。 
在此我們使用一個簡單的類來對實現的jni方法進行測試,下面是PrintTest.java的源代碼部分:

publicstaticvoid main(String[] args) {       Print p = new Print();       p.print();     }   }  


(6)、對PrintTest.java進行編譯執行得到如下結果: 
example1:in this example a printf() function in ANSI C is called 
Hello,the output is generated by printf() function in ANSI C . 

下面介紹的每個實例實現的步驟也都是按着上述步驟執行的。所以介紹時只介紹實現的關鍵部分。 

2、實例二、調用c 語言用戶定義的函數

(源程序爲:java/Cfunction.java java/C_functionTest.java src/Cfunction.c src/Cfunction.h ) 
當需要在java程序中調用用c所實現的函數是,需要在需要調用該c函數的類中定義一個jni方法,在該jni方法中去調用該c函數,相當於用java方法把c函數封裝起來,以供java程序調用。 
在實例二中我們簡單定義了一個printHello()函數,該函數的功能只是輸出一句話,如果要在java程序中調用該函數,只需在jni函數中調用即可,和調用ANSI C中自帶的prinf()函數沒有任何區別。 

3、實例三、在jni函數中訪問java類中的對象實例域

(源程序爲:java/CommonField.java java/CommonFieldTest.java src/CommonField.c src/CommonField.h ) 
jni函數的實現部分是在c 語言中實現的,如果它想訪問java中定義的類對象的實例域需要作三步工作,

(1)調用GetObjectClass()函數得到該對像的類,該函數返回一個jclass類型值。

(2)調用GetFieldID()函數得到要訪問的實例域在該類中的id。 
(3)調用GetXXXField()來得到要訪問的實例域的值。其中XXX和要訪問的實例域的類型相對應。 
在jni中java 編程語言和c 語言數據類型的對應關係爲java原始數據類型前加 'j' 表示對應c語言的數據類型例如boolean 爲jboolean ,int 爲 jint,double 爲jdouble等。對象類型的對應類型爲jobject。 
在 本實例中,您可以看到我們在java/CommonField.java 中定義了類CommonField類,其中包含int a , int b 兩 個實例域,我們要在jni函數getCommonField()中對這兩個域進行訪問和修改。你可以在 src/CommonField.c中找到該函數 的實現部分。 
以下語句是對該域的訪問(以下代碼摘自:src/CommonField.c):

jclass class_Field = (*env)->GetObjectClass(env,obj);   

jfieldID fdA = (*env)->GetFieldID(env,class_Field,"a","I");  

 jfieldID fdB = (*env)->GetFieldID(env,class_Field,"b","I");   

jint valueA = (*env)->GetIntField(env,obj,fdA);  

 jint valueB = (*env)->GetIntField(env,obj,fdB);  



在jni 中對所有jni函數的調用都要用到env指針,該指針也是每一個本地方法的第一個參數,他是函數指針表的指針,所以,必須在每一個jni調用前面加上 (*env)->GetObjectClass(env,obj)函數調用返回obj對像的類型,其中obj 參數表示要你想要得到類型的類對象。 
jfieldID GetFieldID(JNIEnv *env,jclass cl, const char name[], const char sig[]) 該 函數返回一個域的標識符name 表示域名,sig表示編碼的域簽名。所謂編碼的簽名即編碼類型的簽名在上例中類中的a實例域爲int 型,用"I”來表 示,同理"B” 表示byte ,"C” 表示 char , “D”表示 double ,”F” 表示float,“J”表示long, “S” 表 示short , “V” 表示void ,”Z”表示 boolean類型。 
GetIntField(env,obj,fdA),用來訪問obj對象的fdA域,如果要訪問的域爲double類型,則要使用GetDoubleField(env,obj,fdA)來訪問,即類型對應GetXXXField中的XXX。 

以下函數用來修改域的值:

(*env)->SetIntField(env,obj,fdA,109);   (*env)->SetIntField(env,obj,fdB,145);  


這和獲得域的值類似,只是該函數多了一個要設置給該域的值參數。 
訪問對象實例域的相關函數如下: 
jfieldID GetFieldID(JNIEnv *env, jclass cl, const char name[], const char sig[]) 
該函數返回一個域的標識符。各參數含義如下: 
env JNI 接口指針;cl 類對象 ; name 域名; sig 編碼的域簽名 

XXX GetXXXField(JNIEnv *env, jobject obj, jfieldID id) 
該函數返回域的值。域類型XXX是Object, Boolean, byte, char , short, int ,long ,float, double 中類型之一。 
參數 env JNI藉口指針;obj爲域所在對象;id爲域的標識符。 
void SetXXXField(JNIEnv *env,jobject obj, jfieldID id, XXX value) 

該函數用於設置域的值。XXX的含義同上, 
參數中env, obj , id 的含義也同上,value 值爲將要設置的值。 

4、實例四:在jni函數中訪問類的靜態實例域

(java/Field.java java/FieldTest.java src/Field.c src/Field.h)  

因爲靜態實例域並不屬於某個對象,而是屬於一個類,所以在要訪問靜態實例域時,和訪問對象的實例域不同,它所調用的函數是(以實例四來說明,一下代碼摘自src/Field.c):

jclass class_Field = (*env)->FindClass(env,"Field");  

 jfieldID fdA = (*env)->GetStaticFieldID(env,class_Field,"a","I");  

 jint valueA = (*env)->GetStaticIntField(env,class_Field,fdA);  

 (*env)->SetStaticIntField(env,class_Field,fdA,111);  

由 於沒有 對象,必須使用FindClass代替GetObjectClass來獲得類引用。在FindClass()的第二個參數是類的編碼簽名,類的編碼簽名和 基本類型的編碼簽名有所不同,如果類在當前包中,就直接是類的名稱,如果類不在當前包中則要加入該類的詳細路徑:例如String類在java.lang 包中,則String的簽名要寫成( Ljava/lang/String;),其中的(L和;)是不可少的,其中(;)是表達是的終止符。其他三個函數 和訪問對象數據域基本沒什麼區別。

 

5、實例五:在jni函數中調用java對象的方法

(java/CommonMethod.java java/CommonMethodTest.java src/CommonMehod.c src/CommonMethod.h ) 

在jni函數中我們不僅要對java對象的數據域進行訪問,而且有時也需要調用java中類對象已經實現的方法,實例五就是關於這方面的實現的。在src/CommonMethod.c中我們可以找到下面的代碼:

JNIEXPORT void JNICALL Java_CommonMethod_callMethod (

JNIEnv *env, jobject obj, jint a, jstring s)   

{     

printf("example 5:in this example,a object's method will be called\n");     

jclass class_CommonMethod = (*env)->GetObjectClass(env,obj);    

   jmethodID md = (*env)->GetMethodID(env,class_CommonMethod,"print",

"(ILjava/lang/String;)V");    

 (*env)->CallVoidMethod(env,obj,md,a,s);  

 }  

 

 

該代碼部分展示瞭如何實現對java類對象函數的調用過程。從以上代碼部分我們可以看到,要實現該調用需要有三個步驟,調用三個函數

jclass class_CommonMethod = (*env)->GetObjectClass(env,obj);  

 jmethodID md = (*env)->GetMethodID(env,class_CommonMethod,"print","(ILjava/lang/String;)V");  

 (*env)->CallVoidMethod(env,obj,md,a,s);  


GetObjectClass(...)函數獲得要調用對象的類;GetMethodID(...)獲得要調用的方法相對於該類的ID號;CallXXXMethod(...)調用該方法。

在 編寫該調用過程的時候,需要注意的仍然是GetMethodID(...)函數中編碼簽名的問題,在該實例中,我們要做的是找到CommonMethod 類的print(int a, String s)方法,該方法打印整數a,和字符串s 的直。在函數的編碼簽名部分(該部分以加粗、並加有下劃 線)GetMethodID(env,class_CommonMethod,"print","(ILjava/lang /String;)V"); 從左往右可以查看,括號中的內容爲要調用方法的參數部分內容,I表示第一個參數爲int類型,“Ljava/lang /String;”表示第二個參數爲String類型,V表示返回值類型爲空void,如果返回值類型不爲空,則使用相應的類型簽名。返回值類型是和下面 將要使用的調用該方法的函數CallXXXMethod(...)相關聯的,該函數的xxx要用相應的類型來替換,在此實例中爲void,如果返回值類型 爲int類型則調用該方法的函數就爲CallIntMethod(...)。

 

6、實例六:在jni函數中調用java類的靜態方法

(java/Method.java java/MethodTest.java src/Method.h src/Method.c) 

實例五中介紹瞭如何調用類對象的方法,在此實例中我們將介紹如何調用java類的靜態方法在此實例中我們在/java/Method.java中定義了靜態方法: 

public static void print() {  

  System.out.println("this is a static method of class Method"); 
}  

 

該函數的功能就是打印字符串“ this is a static method of class Method”; 
我們在src/Method.c中實現了對該方法調用的jni函數:

JNIEXPORT void JNICALL Java_Method_callMethod (JNIEnv *env, jobject obj)   

{     

printf("example 6:in this example, the class's static method will be called\n");    

 jclass class_Method = (*env)->FindClass(env,"Method");    

 jmethodID md = (*env)->GetStaticMethodID(env,class_Method,"print","()V");    

 (*env)->CallStaticVoidMethod(env,class_Method,md);   }  



和實例五不同的是,我們要調用的三個函數變爲: 
FindClass(...)、GetStaticMethodID(...)、CallStaticVoidMethod(...)。 
其中的機制和實例五是一樣的。再次就不做過多的介紹。 

7、實例七:jni函數中傳遞基本數據類型參數

(java/Basic.java java/BasicTest.java src/Basic.c src/Basic.h) 在java/Basic.java中,我們定義了一個public native void raiseValue(int a)函數,該函數將打印使value的值增加a,並打印原來的value和新的value值。 
在src/Basic.c中給出了該jni函數的實現部分。

JNIEXPORT void JNICALL Java_Basic_raiseValue (

JNIEnv *env, jobject obj, jint a)  

 {     

printf("example 7: in this example, a integer type parament will be passed to the jni method\n");    

 jclass class_Basic = (*env)->GetObjectClass(env,obj);    

 jfieldID fd = (*env)->GetFieldID(env,class_Basic,"value","I");     

jint v = (*env)->GetIntField(env,obj,fd);     

v = v+a;    

 (*env)->SetIntField(env,obj,fd,v);   

}  


在 此函數實現中,因爲要訪問 Basic類中的value域,所以調用了 GetObjectClass(...), GetFieldID(...), GetIntField(...)函數獲取value值,下面一步 的 “ = v+a; ”說明,傳遞基本類型參數的處理方式和在c語言中的基本數據類型的處理無異。 

8、實例八:在jni函數中傳遞對象類型參數

(java/Book.java java/BookTest.java src/BookTest.c src/BookTest.h) 

  在該實例中演示了在jni函數中傳遞對象函數的過程。 

  我們在該實例中定義了一個類Book

total_page = t;    

 }

  publicint getTotalPage() {     }   

publicint getCurrentPage() {     }      

 current_page++;    

 }  

 }  


然後我們在java/BookTest.java中定義jni函數

public native void bookCurrentStatus(Book b);

該函數需要一個Book類型的參數,並返回該參數的當前狀態,包括該書一共有多少頁的total_page,以及當前頁current_page。函數的實現部分爲(src/BookTest.c)

 

JNIEXPORT void JNICALL Java_BookTest_bookCurrentStatus (JNIEnv *env, 

jobject this_obj, jobject obj)   

{    

 printf("example 8: in this example, a object parament will be passed to the jni method。\n");    

 jclass class_book = (*env)->GetObjectClass(env,obj);    

 jmethodID id_getTotal = (*env)->GetMethodID(env,

class_book,"getTotalPage","()I");  

 jmethodID id_getCurrent = (*env)->GetMethodID(env,

class_book,"getCurrentPage","()I");   

  jint total_page = (*env)->CallIntMethod(env,

obj,id_getTotal);    

jint current_page = (*env)->CallIntMethod(env,

obj,id_getCurrent);     

 

printf("the total page is:%d and the current page is :%d\n",

total_page,current_page);  

 }  

 

該 函數包含三個參數(JNIEnv *env, jobject this_obj, jobject obj) ,第二 個jobject this_obj參數表示當前的jni 函數所屬於的類對象,第三個jobject obj參數表示傳遞的參數Book類型的類對象。

對於實現部分,基本和實例五--調用java類對象的方法中的操作相同,就不作詳解。

 

9、實例九:在jni函數中處理字符串

(java/Str.java java/StrTest.java src/Str.c src/Str.h) 
在該實例中我們講解如何傳遞、處理字符串參數。 
在java/Str.java中我們定義了一個 printString(String s) 的方法,用來處理字符串參數。 
在src/Str.c中我們可以看到該函數的實現部分:

JNIEXPORT void JNICALL Java_Str_printString (JNIEnv *env, 

jobject obj, jstring s)   

{     

printf("example 9: in this example, a String object parament will be passed to the jni method.\n");    

const char* string = (char*)(*env)->GetStringUTFChars(env,s,NULL);     

printf("%s is put out in native method\n",string);  

(*env)->ReleaseStringUTFChars(env,s,(jbyte*)string);   

}  


實現過程中調用了兩個函數:GetStringUTFChars(...)、 ReleaseStringUTFChars(...)。 
GetStringUTFChars(...) 用來獲取String對象的字符串,並將其抓那還爲char*類型,這應該字符串就可以在c語言中進行處理拉。 ReleaseStringUTFChars(...)用於當該字符串使用完成後,將其進行垃圾回收。記住,當使用完字符串時一定不要忘記調用該函數。 

10、實例十:在jni函數中處理數組

(java/Arr.java java/ArrTest.java src/Arr.c src/Arr.h)

java中所有的數組類型都有相對應的c語言類型,其中jarray類型表示一個泛型數組

boolean[] --jbooleanArray 

byte[]--jbyteArray 

char[]--jcharArary

int[]---jcharArray 

short[]---jshortArray 

long[]---jlongArray 

float[]--jfloatArray

double[]—-jdoubleArray 

Object[]--- jobjectArray。

當訪問數組時,可以通過GetObjectAraryElement和SetObjectArrayElement方法訪問對象數組的元素。

而 對於一般類型數組,你可以調用GetXXXAraryElements來獲取一個只想數組起始元素的指針,而當你不在使用該數組時,要記得調用 ReleaseXXXArrayElements,這樣你所作的改變才能保證在原始數組裏得到反映。當然如果你需要得到數組的長度,可以調用 GetArrayLength函數。

在本實例中,我們在Arr.java中定義一個本地方法:print(int intArry[]),該函數的功能爲對該數組進行輸出,在src/Arr.c中我們可以看到該方法的實現過程如下:

JNIEXPORT void JNICALL Java_Arr_print (JNIEnv *env, 

jobject obj, jintArray intArray)   

{     

printf("example 10:in this example, a array parament will be passed to the jni method.\n");     

jint* arr = (*env)->GetIntArrayElements(env,intArray,NULL);   

n = (*env)->GetArrayLength(env,intArray);   

printf("the native method output the int array\n");   

for( i = 0;i<(*env)->GetArrayLength(env,intArray);i++)    

 {       

printf("%d ",arr[i]);     

}     

(*env)->ReleaseIntArrayElements(env,intArray,arr,0);   

}  

 

我們在此調用了GetIntArrayElements(...)來獲取一個指向intArray[]數組第一個元素的指針。 
用getArrayLength(..)函數來得到數組的長度,以方便數組遍歷時使用。最後應用ReleaseArrayElements(...)函數來釋放該數組指針。 

11、實例十一:在jni中的返回值問題

(java/ReturnValue.java java/ReturnValueTest.java java/BookClass.java src/ReturnValue.c src/ReturnValue.h)

在java/ReturnValue類中定義了三個jni方法: returnInt(),returnString() ,returnObject() 
三個方法,分別返回int , String , Object 類型的值。

其在src/ReturnValue.c中的實現分別爲:

JNIEXPORT jint JNICALL Java_ReturnValue_returnInt   (

JNIEnv *env, jobject obj)   

{    

 jclass  class_ReturnValue = (*env)->GetObjectClass(env,obj);     

jfieldID  fd = (*env)->GetFieldID(env,class_ReturnValue,"value","I");    

 jint  v = (*env)->GetIntField(env,obj,fd);  

return v;

 }     

 

* Signature: ()Ljava/lang/String;

JNIEXPORT jstring JNICALL Java_ReturnValue_returnString   (

JNIEnv *env, jobject obj)   

{

printf("example 11: in this example, the int and object of return value will be proceeding\n");

 jclass class_ReturnValue = (*env)->GetObjectClass(env,obj);   

  jfieldID fd = (*env)->GetFieldID(env,class_ReturnValue,"name","Ljava/lang/String;");   

  jstring jstr = (jstring)(*env)->GetObjectField(env,obj,fd);

}   

 

* * Method: returnObject

JNIEXPORT jobject JNICALL Java_ReturnValue_returnObject   (

JNIEnv *env, jobject obj)   

{   

    jclass class_ReturnValue = (*env)->GetObjectClass(env,obj);    

 jfieldID fd = (*env)->GetFieldID(env,class_ReturnValue,"myBook","LBookClass;");    

 jobject  jbook = (jstring)(*env)->GetObjectField(env,obj,fd);   

}  


在這裏分別涉及到了對java類對象的一般參數,String參數,以及Object參數的訪問。 

12、實例十二:在jni中創建java類對象:

(java/Test.java src/CreateObj.c src/CreateObj.h)


如果想要在jni函數創建java類對象則要引用java 類的構造器方法,通過調用NewObject函數來實現。

NewObject函數的調用方式爲:

jobject obj_new = (*env)->NewObject(env,class, methodid, paraments); 
在該實例中,我們在java/Test.java 中定義了Book1類,要在CreateObj類的modifyProperty() jni方法中創建該類對象。我們可以在src/CreateObj.c中看到該jni方法創建對象的過程:

jobject     book;  

jclass      class_book;   

jmethodID  md_book;   

class_book = (*env)->FindClass(env,"LBook1;");   

md_book = (*env)->GetMethodID(env,class_book,"<init>","(IILjava/lang/String;)V");  

book = (*env)->NewObject(env,class_book,md_book,100,1,"huanghe");  

 

在 創建對象的過程中可以看到,要創建一個java類對象,首先需要得到得到使用FindClass函數得到該類,然後使用GetMethodID方法得到該 類的構造器方法id,主義在此時構造器的函數名始終爲:"”,其後函數的簽名要符合函數簽名規則。在此我們的構造器有三個參 數:int , int, String.

並且其返回值類型要永久爲空,所以函數簽名爲:"(IILjava/lang/String;)V"

然後我們調用NewObject()函數來創建該類的對象,在此之後就可以使用該對象拉。

 

以上內容介紹的是jni函數c語言的實現實例。如果想要使用c++的實例,我們只需要把其中的每一個函數調用過程作稍微的修改:

例如:(*env)->NewObject(env,class_book,md_book,100,1,”huanghe”);

修改爲:(env)->NewObject(class_book,md_book,100,1,”huanghe”);

即修改(*env)爲(env)再把參數中的env去掉。然後把所有c的函數改爲c++的函數就OK拉。

具體情況可以去查看我們的c++實例代碼.

 

 

 

Chap10:在 Windows 中實現 Java 本地方法

 

級別: 初級

David Wendt WebSphere Development Research Triangle Park, NC

1999 年 5 月 01 日

本文爲在 32 位 Windows 平臺上實現 Java 本地方法提供了實用的示例、步驟和準則。這些示例包括傳遞和返回常用的數據類型。

本文中的示例使用 Sun Microsystems 公司創建的 Java DevelopmentKit (JDK) 版本 1.1.6 和 Java本地接口 (JNI ) 規範 。 用 C 語言編寫的本地代碼是用 MicrosoftVisual C++ 編譯器編譯生成的。

簡介

本文提供調用本地 C 代碼的 Java 代碼示例,包括傳遞和返回某些常用的數據類型。本地方法包含在特定於平臺的可執行文件中。就本文中的示例而言,本地方法包含在 Windows 32 位動態鏈接庫 (DLL) 中。

不過我要提醒您,對 Java 外部的調用通常不能移植到其他平臺上,在 applet 中還可能引發安全異常。實現本地代碼將使您的 Java 應用程序無法通過 100% 純 Java 測試。但是,如果必須執行本地調用,則要考慮幾個準則:

  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 <stdio.h>

#include "MyNative.h"

JNIEXPORT void JNICALL Java_MyNative_showParms0

  (JNIEnv *env, jobject obj, jstring s, jint i, jboolean b)

{

  const char* szStr = (*env)->GetStringUTFChars( env, s, 0 );

  printf( "String = [%s]\n", szStr );

  printf( "int = %d\n", i );

  printf( "boolean = %s\n", (b==JNI _TRUE ? "true" : "false") );

  (*env)->ReleaseStringUTFChars( env, s, szStr );

}

 

JNI API,GetStringUTFChars,用來根據 Java 字符串或 jstring 參數 創建 C 字符串。這是必需的,因爲在本地代碼中不能直接讀取 Java 字符串,而必須將其轉換爲 C 字符串或 Unicode。有關轉換 Java 字符串的詳細信息,請參閱標題爲 NLS Strings and JNI 的一篇論文。但是,jboolean 和 jint 值可以直接使用。

MyNative.dll 是通過編譯 C 源文件創建的。下面的編譯語句使用 Microsoft Visual C++ 編譯器:

 cl -Ic:\jdk1.1.6\include -Ic:\jdk1.1.6\include\win32 -LD MyNative.c

      -FeMyNative.dll 

 

其中 c:\jdk1.1.6 是 JDK 的安裝路徑。

MyNative.dll 已創建好,現在就可將其用於 MyNative 類了。 
可以這樣測試這個本地方法:在 MyNative 類中創建一個 main 方法來調用 showParms 方法,如下所示:

   public static void main( String[] args )

   {

     MyNative obj = new MyNative();

     obj.showParms( "Hello", 23, true );

     obj.showParms( "World", 34, false );

   }

 

當運行這個 Java 應用程序時,請確保 MyNative.dll 位於 Windows 的 PATH 環境變量所指定的路徑中或當前目錄下。當執行此 Java 程序時,如果未找到這個 DLL,您可能會看到以下的消息:

 java MyNative 

 Can't find class MyNative 

 

這是因爲 static 子句無法加載這個 DLL,所以在初始化 MyNative 類時引發異常。Java 解釋器處理這個異常,並報告一個一般錯誤,指出找不到這個類。 
如果用 -verbose 命令行選項運行解釋器,您將看到它因找不到這個 DLL 而加載 UnsatisfiedLinkError 異常。

如果此 Java 程序完成運行,就會輸出以下內容:

 java MyNative 

 String = [Hello] 

 int = 23

 boolean = true 

 String = [World] 

 int

      = 34 

 

boolean = false 示例 2 -- 返回一個值

本例將說明如何在本地方法中實現返回代碼。 
將這個方法添加到 MyNative 類中,這個類現在變爲以下形式:

public class MyNative

{

  public void showParms( String s, int i, boolean b )

  {

    showParms0( s, i , b );

  }

  public int hypotenuse( int a, int b )

  {

    return hyptenuse0( a, b );

  }

  private native void showParms0( String s, int i, boolean b );

  private native int  hypotenuse0( int a, int b );

  static

  {

    System.loadLibrary( "MyNative" );

  }

  /* 測試本地方法 */

  public static void main( String[] args )

  {

    MyNative obj = new MyNative();

    System.out.println( obj.hypotenuse(3,4) );

    System.out.println( obj.hypotenuse(9,12) );

  }

}

 

公用的 hypotenuse 方法調用本地方法 hypotenuse0 來根據傳遞的參數 計算值,並將結果作爲一個整數返回。這個新本地方法的原型是使用 javah 生成的。請注意,每次運行這個實用程序時,它將自動覆蓋當前目錄中的 MyNative.h。按以下方式執行 javah:

 javah -jni MyNative 

 

生成的 MyNative.h 現在包含 hypotenuse0 原型,如下所示:

/*

 * Class:     MyNative

 * Method:    hypotenuse0

 * Signature: (II)I

 */

JNIEXPORT jint JNICALL Java_MyNative_hypotenuse0

  (JNIEnv *, jobject, jint, jint);

 

該方法是在 MyNative.c 源文件中實現的,如下所示:

#include <stdio.h>

#include <math.h>

#include "MyNative.h"

JNIEXPORT void JNICALL Java_MyNative_showParms0

  (JNIEnv *env, jobject obj, jstring s, jint i, jboolean b)

{

  const char* szStr = (*env)->GetStringUTFChars( env, s, 0 );

  printf( "String = [%s]\n", szStr );

  printf( "int = %d\n", i );

  printf( "boolean = %s\n", (b==JNI _TRUE ? "true" : "false") );

  (*env)->ReleaseStringUTFChars( env, s, szStr );

}

JNIEXPORT jint JNICALL Java_MyNative_hypotenuse0

  (JNIEnv *env, jobject obj, jint a, jint b)

{

  int rtn = (int)sqrt( (double)( (a*a) + (b*b) ) );

  return (jint)rtn;

}

 

再次請注意,jint 和 int 值是可互換的。 
使用相同的編譯語句重新編譯這個 DLL:

 cl -Ic:\jdk1.1.6\include -Ic:\jdk1.1.6\include\win32 -LD MyNative.c

      -FeMyNative.dll 

 

現在執行 java MyNative 將輸出 5 和 15 作爲斜邊的值。

示例 3 -- 靜態方法

您可能在上面的示例中已經注意到,實例化的 MyNative 對象是沒必要的。實用方法通常不需要實際的對象,通常都將它們創建爲靜態方法。本例說明如何用一個靜態方法實現上面的示例。更改 MyNative.java 中的方法簽名,以使它們成爲靜態方法:

  public static int hypotenuse( int a, int b )

  {

    return hypotenuse0(a,b);

  }

  ...

  private static native int  hypotenuse0( int a, int b );

 

現在運行 javah 爲 hypotenuse0 創建一個新原型,生成的原型如下所示:

/*

 * Class:     MyNative

 * Method:    hypotenuse0

 * Signature: (II)I

 */

JNIEXPORT jint JNICALL Java_MyNative_hypotenuse0

  (JNIEnv *, jclass, jint, jint);

 

C 源代碼中的方法簽名變了,但代碼還保持原樣:

JNIEXPORT jint JNICALL Java_MyNative_hypotenuse0

  (JNIEnv *env, jclass cls, jint a, jint b)

{

  int rtn = (int)sqrt( (double)( (a*a) + (b*b) ) );

  return (jint)rtn;

}

 

本質上,jobject 參數 已變爲 jclass 參數 。此參數 是指向 MyNative.class 的一個句柄。main 方法可更改爲以下形式:

  public static void main( String[] args )

  {

    System.out.println( MyNative.hypotenuse( 3, 4 ) );

    System.out.println( MyNative.hypotenuse( 9, 12 ) );

  }

 

因爲方法是靜態的,所以調用它不需要實例化 MyNative 對象。本文後面的示例將使用靜態方法。

示例 4 -- 傳遞數組

本例說明如何傳遞數組 型參數 。本例使用一個基本類型,boolean,並將更改數組 元素。下一個示例將訪問 String(非基本類型)數組 。將下面的方法添加到 MyNative.java 源代碼中:

  public static void setArray( boolean[] ba )

  {

    for( int i=0; i < ba.length; i++ )

      ba[i] = true;

    setArray0( ba );

  }

  ...

  private static native void setArray0( boolean[] ba );

 

在本例中,布爾型數組 被初始化爲 true,本地方法將把特定的元素設置爲 false。同時,在 Java 源代碼中,我們可以更改 main 以使其包含測試代碼:

    boolean[] ba = new boolean[5];

    MyNative.setArray( ba );

    for( int i=0; i < ba.length; i++ )

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

 

在編譯源代碼並執行 javah 以後,MyNative.h 頭文件包含以下的原型:

/*

 * Class:     MyNative

 * Method:    setArray0

 * Signature: ([Z)V

 */

JNIEXPORT void JNICALL Java_MyNative_setArray0

  (JNIEnv *, jclass, jbooleanArray);

 

請注意,布爾型數組 是作爲單個名爲 jbooleanArray 的類型創建的。 
基本類型有它們自已的數組 類型,如 jintArray 和 jcharArray。 
非基本類型的數組 使用 jobjectArray 類型。下一個示例中包括一個 jobjectArray。這個布爾數組 的數組 元素是通過 JNI 方法 GetBooleanArrayElements 來訪問的。 
針對每種基本類型都有等價的方法。這個本地方法是如下實現的:

JNIEXPORT void JNICALL Java_MyNative_setArray0

  (JNIEnv *env, jclass cls, jbooleanArray ba)

{

  jboolean* pba = (*env)->GetBooleanArrayElements( env, ba, 0 );

  jsize len = (*env)->GetArrayLength(env, ba);

  int i=0;

  // 更改偶數數組 元素

  for( i=0; i < len; i+=2 )

    pba[i] = JNI _FALSE;

  (*env)->ReleaseBooleanArrayElements( env, ba, pba, 0 );

}

 

指向布爾型數組 的指針可以使用 GetBooleanArrayElements 獲得。 
數組 大小可以用 GetArrayLength 方法獲得。使用 ReleaseBooleanArrayElements 方法釋放數組 。現在就可以讀取和修改數組 元素的值了。jsize 聲明等價於 jint(要查看它的定義,請參閱 JDK 的 include 目錄下的 jni .h 頭文件)。

示例 5 -- 傳遞 Java String 數組

本例將通過最常用的非基本類型,Java String,說明如何訪問非基本對象的數組 。字符串數組 被傳遞給本地方法,而本地方法只是將它們顯示到控制檯上。 
MyNative 類定義中添加了以下幾個方法:

  public static void showStrings( String[] sa )

  {

    showStrings0( sa );

  }

  private static void showStrings0( String[] sa );

 

並在 main 方法中添加了兩行進行測試:

  String[] sa = new String[] { "Hello,", "world!", "JNI ", "is", "fun." };

  MyNative.showStrings( sa );

 

本地方法分別訪問每個元素,其實現如下所示。

JNIEXPORT void JNICALL Java_MyNative_showStrings0

  (JNIEnv *env, jclass cls, jobjectArray sa)

{

  int len = (*env)->GetArrayLength( env, sa );

  int i=0;

  for( i=0; i < len; i++ )

  {

    jobject obj = (*env)->GetObjectArrayElement(env, sa, i);

    jstring str = (jstring)obj;

    const char* szStr = (*env)->GetStringUTFChars( env, str, 0 );

    printf( "%s ", szStr );

    (*env)->ReleaseStringUTFChars( env, str, szStr );

  }

  printf( "\n" );

}

 

數組 元素可以通過 GetObjectArrayElement 訪問。

在 本例中,我們知道返回值是 jstring 類型,所以可以安全地將它從 jobject 類型轉換爲 jstring 類型。字符串是通過前面討論過的方法打印的。有關在 Windows 中處理 Java 字符串的信息,請參閱標題爲 NLS Strings and JNI的一篇論文。

示例 6 -- 返回 Java String 數組

最後一個示例說明如何在本地代碼中創建一個字符串數組 並將它返回給 Java 調用者。MyNative.java 中添加了以下幾個方法:

  public static String[] getStrings()

  {

    return getStrings0();

  }

  private static native String[] getStrings0();

 

更改 main 以使 showStrings 將 getStrings 的輸出顯示出來:

  MyNative.showStrings( MyNative.getStrings() );

 

實現的本地方法返回五個字符串。

JNIEXPORT jobjectArray JNICALL Java_MyNative_getStrings0

  (JNIEnv *env, jclass cls)

{

  jstring      str;

  jobjectArray args = 0;

  jsize        len = 5;

  char*        sa[] = { "Hello,", "world!", "JNI ", "is", "fun" };

  int          i=0;

  args = (*env)->NewObjectArray(env, len, (*env)->FindClass(env, "java/lang/String"), 0);

  for( i=0; i < len; i++ )

  {

    str = (*env)->NewStringUTF( env, sa[i] );

    (*env)->SetObjectArrayElement(env, args, i, str);

  }

  return args;

}

 

字符串數組 是通過調用 NewObjectArray 創建的,同時傳遞了 String 類和數組 長度兩個參數 。Java String 是使用 NewStringUTF 創建的。String 元素是使用 SetObjectArrayElement 存入數組 中的。

 

 

2.調試

現 在您已經爲您的應用程序創建了一個本地 DLL,但在調試時還要牢記以下幾點。如果使用 Java 調試器 java_g.exe,則還需要創建 DLL 的一個“調試”版本。這只是表示必須創建同名但帶有一個 _g 後綴的 DLL 版本。就 MyNative.dll 而言,使用 java_g.exe 要求在 Windows 的 PATH 環境指定的路徑中有一個 MyNative_g.dll 文件。在大多數情況下,這個 DLL 可以通過將原文件重命名或複製爲其名稱帶綴 _g 的文件。

現在,Java 調試器不允許您進入本地代碼,但您可以在 Java 環境外使用 C 調試器(如 Microsoft Visual C++)調試本地方法。首先將源文件導入一個項目中。 
將編譯設置調整爲在編譯時將 include 目錄包括在內:

 c:\jdk1.1.6\include;c:\jdk1.1.6\include\win32 

 

將配置設置爲以調試模式編譯 DLL。在 Project Settings 中的 Debug 下,將可執行文件設置爲 java.exe(或者 java_g.exe,但要確保您生成了一個 _g.dll 文件)。程序參數 包括包含 main 的類名。如果在 DLL 中設置了斷點,則當調用本地方法時,執行將在適當的地方停止。

下面是設置一個 Visual C++ 6.0 項目來調試本地方法的步驟。

  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 <jni.h>

/* Header for class HelloWorld */

#ifndef _Included_HelloWorld

#define _Included_HelloWorld

#ifdef __cplusplus

extern "C" {

#endif

/*

* Class:     HelloWorld

* Method:    print

* Signature: ()V

*/

JNIEXPORT void JNICALL Java_HelloWorld_print (JNIEnv *, jobject);

#ifdef __cplusplus

}

#endif

#endif

 

 

 

注意到在這個程序的開頭有這樣一行代碼,

#include <jni.h>

這裏的jni.h,只要你安裝了JDK就能在安裝目錄下找到它。

不要修改這個文件的內容,現在要做的是寫一個名爲HelloWorld.cpp程序,實現上面這個.h文件裏的函數,

 

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

#include "HelloWorld.h"

#include <iostream>

JNIEXPORT void JNICALL Java_HelloWorld_print (JNIEnv *, jobject) {

std::cout << "Hello World!" << std::endl;

}

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

 

這是一個最簡單的C++程序。將它編譯爲動態鏈接庫,我們得到HelloWorld.dll,將這個.dll文件拷到HelloWorld.java文件所在的目錄下。執行

> java HelloWorld

你會看到屏幕上輸出

> Hello World!

 

 

現在來總結一下,要實現JNI編程,需要以下幾個步驟:

1. 寫一個Java程序,將你希望用C語言實現的方法用native關鍵字標識出來,同時加上調用動態鏈接庫的語句。

   System.loadLibrary("HelloWorld");

2. 執行下面兩條語句,生成.h文件

 

 

   > javac HelloWorld.java

 

在class或bin目錄下(其下或其子目錄下有 javac命令生成的*.class文件)執行

   > javah -jni HelloWorld

3. 根據.h文件,寫一個.cpp程序,編譯成動態鏈接庫,並將其複製到.java文件所在的路徑下。

4. 執行java HelloWorld

這 樣,就學會了最簡單的JNI編程,網上能google到的大部分文章也就到此爲止了。但是你一定還有很多疑問,就像我剛開始一樣,最容易想到的就是,如果 本地方法要傳遞參數或者返回值怎麼辦?本地方法的定義在.java文件中,參數或者返回值的類型都是Java的類型。而它的實現是通過C程序完成的,參數 和返回值的類型只能是C的類型。諸如此類的問題,上面這個簡單的例子是回答不了的。在下一篇,我將解釋這些問題。

 

 

Chap12:JNI編程系列之中級篇(上)

 

編程技術   2008-12-06 23:41   閱讀9   評論0

字號: 大  中  小

本篇將介紹在JNI編程中如何傳遞參數和返回值。

首先要強調的是,native方法不但可以傳遞Java的基本類型做參數,還可以傳遞更復雜的類型,比如String,數組,甚至自定義的類。這一切都可以在jni.h中找到答案。

 

1. Java基本類型的傳遞

 

用 過Java的人都知道,Java中的基本類型包括boolean,byte,char,short,int,long,float,double這樣幾 種,如果你用這幾種類型做native方法的參數,當你通過javah -jni生成.h文件的時候,只要看一下生成的.h文件,就會一清二楚,這些類型分別對應的類型是 jboolean,jbyte,jchar,jshort,jint,jlong,jfloat,jdouble 。這幾種類型幾乎都可以當成對應的C++類型來用,所以沒什麼好說的。

 

2. String參數的傳遞

Java的String和C++的string是不能對等起來的,所以處理起來比較麻煩。先看一個例子,

//*****************

class Prompt {

 

// native method that prints a prompt and reads a line

private native String getLine(String prompt);

 

public static void main(String args[]) {

Prompt p = new Prompt();

String input = p.getLine("Type a line: ");

 

System.out.println("User typed: " + input);

}

 

static {

System.loadLibrary("Prompt");

}

}

 

//*****************

 

在這個例子中,我們要實現一個native方法

String getLine(String prompt);

讀入一個String參數,返回一個String值。

通過執行javah -jni得到的頭文件是這樣的

 

//*****************

#include <jni.h>

#ifndef _Included_Prompt

#define _Included_Prompt

#ifdef __cplusplus

extern "C" {

#endif

JNIEXPORT jstring JNICALL Java_Prompt_getLine(JNIEnv *env, jobject this, jstring prompt);

#ifdef __cplusplus

}

#endif

#endif

//*****************

 

jstring是JNI中對應於String的類型,但是和基本類型不同的是,jstring不能直接當作C++的string用。如果你用

cout << prompt << endl;

編譯器肯定會扔給你一個錯誤信息的。

其實要處理jstring有很多種方式,這裏只講一種我認爲最簡單的方式,看下面這個例子,

 

//*****************

#include "Prompt.h"

#include <iostream>

 

JNIEXPORT jstring JNICALL Java_Prompt_getLine(JNIEnv *env, jobject obj, jstring prompt)

{

const char* str;

str = env->GetStringUTFChars(prompt, false);

if(str == NULL) {

return NULL; /* OutOfMemoryError already thrown */

}

 

std::cout << str << std::endl;

 

env->ReleaseStringUTFChars(prompt, str);

// return a string

char* tmpstr = "return string succeeded";

jstring rtstr = env->NewStringUTF(tmpstr);

return rtstr;

}

//*****************

 

在上面的例子中,作爲參數的prompt不能直接被C++程序使用,先做了如下轉換

str = env->GetStringUTFChars(prompt, false);

將jstring類型變成一個char*類型。

返回的時候,要生成一個jstring類型的對象,也必須通過如下命令,

jstring rtstr = env->NewStringUTF(tmpstr);

這裏用到的GetStringUTFChars和NewStringUTF都是JNI提供的處理String類型的函數,還有其他的函數這裏就不一一列舉了。

 

 

 

 

/****************************************************/

JNI編程系列之中級篇(下)

編程技術   2008-12-06 23:44   閱讀7   評論0  

字號: 大  中  小

3. 數組類型的傳遞

和String一樣,JNI爲Java基本類型的數組提供了j*Array類型,比如int[]對應的就是jintArray。來看一個傳遞int數組的例子,Java程序就不寫了,

//*******************

JNIEXPORT jint JNICALL Java_IntArray_sumArray(JNIEnv *env, jobject obj, jintArray arr)

{

    jint *carr;

    carr = env->GetIntArrayElements(arr, false);

    if(carr == NULL) {

        return 0; /* exception occurred */

}

 

    jint sum = 0;

    for(int i=0; i<10; i++) {

        sum += carr[i];

}

 

    env->ReleaseIntArrayElements(arr, carr, 0);

    return sum;

}

//*******************

 

這 個例子中的GetIntArrayElements和ReleaseIntArrayElements函數就是JNI提供用於處理int數組的函數。如果 試圖用arr[i]的方式去訪問jintArray類型,毫無疑問會出錯。JNI還提供了另一對函數GetIntArrayRegion和 ReleaseIntArrayRegion訪問int數組,就不介紹了,對於其他基本類型的數組,方法類似。

 

4. 二維數組和String數組

在JNI中,二維數組和String數組都被視爲object數組,因爲數組和String被視爲object。仍然用一個例子來說明,這次是一個二維int數組,作爲返回值。

 

//*******************

JNIEXPORT jobjectArray JNICALL Java_ObjectArrayTest_initInt2DArray(JNIEnv *env, jclass cls, int size)

{

    jobjectArray result;

    jclass intArrCls = env->FindClass("[I");

    result = env->NewObjectArray(size, intArrCls, NULL);

    for (int i = 0; i < size; i++) {

        jint tmp[256]; /* make sure it is large enough! */

        jintArray iarr = env->NewIntArray(size);

        for(int j = 0; j < size; j++) {

            tmp[j] = i + j;

        }

        env->SetIntArrayRegion(iarr, 0, size, tmp);

        env->SetObjectArrayElement(result, i, iarr);

        env->DeleteLocalRef(iarr);

    }

    return result;

}

//*******************

 

上面代碼中的第三行,

jobjectArray result;

因爲要返回值,所以需要新建一個jobjectArray對象。

jclass intArrCls = env->FindClass("[I");

是 創建一個jclass的引用,因爲result的元素是一維int數組的引用,所以intArrCls必須是一維int數組的引用,這一點是如何保證的 呢?注意FindClass的參數"[I",JNI就是通過它來確定引用的類型的,I表示是int類型,[標識是數組。對於其他的類型,都有相應的表示方 法,

Z boolean

B byte

C char

S short

I int

J long

F float

D double

String是通過“Ljava/lang/String;”表示的,那相應的,String數組就應該是“[Ljava/lang/String;”。

還是回到代碼,

result = env->NewObjectArray(size, intArrCls, NULL);

的作用是爲result分配空間。

jintArray iarr = env->NewIntArray(size);

是爲一維int數組iarr分配空間。

env->SetIntArrayRegion(iarr, 0, size, tmp);

是爲iarr賦值。

env->SetObjectArrayElement(result, i, iarr);

是爲result的第i個元素賦值。

通過上面這些步驟,我們就創建了一個二維int數組,並賦值完畢,這樣就可以做爲參數返回了。

如果瞭解了上面介紹的這些內容,基本上大部分的任務都可以對付了。雖然在操作數組類型,尤其是二維數組和String數組的時候,比起在單獨的語言中編程要麻煩,但既然我們享受了跨語言編程的好處,必然要付出一定的代價。

有 一點要補充的是,本文所用到的函數調用方式都是針對C++的,如果要在C中使用,所有的env->都要被替換成(*env)->,而且後面的 函數中需要增加一個參數env,具體請看一下jni.h的代碼。另外還有些省略的內容,可以參考JNI的文檔:Java Native Interface 6.0 Specification,在JDK的文檔裏就可以找到。如果要進行更深入的JNI編程,需要仔細閱讀這個文檔。接下來的高級篇,也會討論更深入的話 題。

 

 

Chap13:JNI編程系列之高級篇

 

編程技術   2008-12-10 17:08   閱讀6   評論0

字號: 大  中  小

在本篇中,將會涉及關於JNI編程更深入的話題,包括:在native方法中訪問Java類的域和方法,將Java中自定義的類作爲參數和返回值傳遞等等。瞭解這些內容,將會對JNI編程有更深入的理解,寫出的程序也更清晰,易用性更好。

 

1. 在一般的Java類中定義native方法

在前兩篇的例子中,都是將native方法放在main方法的Java類中,實際上,完全可以在任何類中定義native方法。這樣,對於外部來說,這個類和其他的Java類沒有任何區別。

 

2. 訪問Java類的域和方法

native方法雖然是native的,但畢竟是方法,那麼就應該同其他方法一樣,能夠訪問類的私有域和方法。實際上,JNI的確可以做到這一點,我們通過幾個例子來說明,

 

public class ClassA {

String str_ = "abcde";

int number_;

public native void nativeMethod();

private void javaMethod() {

System.out.println("call java method succeeded");

}

static {

System.loadLibrary("ClassA");

}

}

在這個例子中,我們在一個沒有main方法的Java類中定義了native方法。我們將演示如何在nativeMethod()中訪問域str_,number_和方法javaMethod(),nativeMethod()的C++實現如下,

JNIEXPORT void JNICALL Java_testclass_ClassCallDLL_nativeMethod(JNIEnv *env, jobject obj)

{

// access field

jclass cls = env->GetObjectClass(obj);

jfieldID fid = env->GetFieldID(cls, "str_", "Ljava/lang/String;");

jstring jstr = (jstring)env->GetObjectField(obj, fid);

const char *str = env->GetStringUTFChars(jstr, false);

if(std::string(str) == "abcde")

std::cout << "access field succeeded" << std::endl;

jint i = 2468;

fid = env->GetFieldID(cls, "number_", "I");

env->SetIntField(obj, fid, i);

// access method

jmethodID mid = env->GetMethodID(cls, "javaMethod", "()V");

env->CallVoidMethod(obj, mid);

}

上面的代碼中,通過如下兩行代碼獲得str_的值,

jfieldID fid = env->GetFieldID(cls, "str_", "Ljava/lang/String;");

jstring jstr = (jstring)env->GetObjectField(obj, fid);

第一行代碼獲得str_的id,在GetFieldID函數的調用中需要指定str_的類型,第二行代碼通過str_的id獲得它的值,當然我們讀到的是一個jstring類型,不能直接顯示,需要轉化爲char*類型。

接下來我們看如何給Java類的域賦值,看下面兩行代碼,

fid = env->GetFieldID(cls, "number_", "I");

env->SetIntField(obj, fid, i);

第一行代碼同前面一樣,獲得number_的id,第二行我們通過SetIntField函數將i的值賦給number_,其他類似的函數可以參考JDK的文檔。

訪問javaMethod()的過程同訪問域類似,

jmethodID mid = env->GetMethodID(cls, "javaMethod", "()V");

env->CallVoidMethod(obj, mid);

需要強調的是,在GetMethodID中,我們需要指定javaMethod方法的類型,域的類型很容易理解,方法的類型如何定義呢,在上面的例子中,我們用的是()V,V表示返回值爲空,()表示參數爲空。如果是更復雜的函數類型如何表示?看一個例子,

long f (int n, String s, int[] arr);

這個函數的類型符號是(ILjava/lang/String;[I)J,I表示int類型,Ljava/lang/String;表示String類型,[I表示int數組,J表示long。這些都可以在文檔中查到。

 

3. 在native方法中使用用戶定義的類

JNI 不僅能使用Java的基礎類型,還能使用用戶定義的類,這樣靈活性就大多了。大體上使用自定義的類和使用Java的基礎類(比如String)沒有太大的 區別,關鍵的一點是,如果要使用自定義類,首先要能訪問類的構造函數,看下面這一段代碼,我們在native方法中使用了自定義的Java類 ClassB,

jclass cls = env->FindClass("Ltestclass/ClassB;");

jmethodID id = env->GetMethodID(cls, "<init>", "(D)V");

jdouble dd = 0.033;

jvalue args[1];

args[0].d = dd;

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

首 先要創建一個自定義類的引用,通過FindClass函數來完成,參數同前面介紹的創建String對象的引用類似,只不過類名稱變成自定義類的名稱。然 後通過GetMethodID函數獲得這個類的構造函數,注意這裏方法的名稱是"<init>",它表示這是一個構造函數。

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

生成了一個ClassB的對象,args是ClassB的構造函數的參數,它是一個jvalue*類型。

通 過以上介紹的三部分內容,native方法已經看起來完全像Java自己的方法了,至少主要功能上齊備了,只是實現上稍麻煩。而瞭解了這些,JNI編程的 水平也更上一層樓。下面要討論的話題也是一個重要內容,至少如果沒有它,我們的程序只能停留在演示階段,不具有實用價值。

4. 異常處理

在C++和Java的編程中,異常處理都是一個重要的內容。但是在JNI中,麻煩就來了,native方法是通過C++實現的,如果在native方法中發生了異常,如何傳導到Java呢?

JNI提供了實現這種功能的機制。我們可以通過下面這段代碼拋出一個Java可以接收的異常,

jclass errCls;

env->ExceptionDescribe();

env->ExceptionClear();

errCls = env->FindClass("java/lang/IllegalArgumentException");

env->ThrowNew(errCls, "thrown from C++ code");

如果要拋出其他類型的異常,替換掉FindClass的參數即可。這樣,在Java中就可以接收到native方法中拋出的異常。

至 此,JNI編程系列的內容就完全結束了,這些內容都是本人的原創,通過查閱文檔和網上的各種文章總結出來的,相信除了JDK的文檔外,沒有比這更全面的講 述JNI編程的文章了。當然,限於篇幅,有些地方不可能講的很細。限於水平,也可能有一些錯誤。文中所用的代碼,都親自編譯執行過。希望這些內容能爲需要 的朋友提供幫助,畢竟,分享是一種美德。

 

 

 

 

Chap14:如何在C/C++中調用Java

 

 

作者:劉冬 發文時間:2003.02.17

Java 跨平臺的特性使Java越來越受開發人員的歡迎,但也往往會聽到不少的抱怨:用Java開發的圖形用戶窗口界面每次在啓動的時候都會跳出 一個控制檯窗口,這個控制檯窗口讓本來非常棒的界面失色不少。怎麼能夠讓通過Java開發的GUI程序不彈出Java的控制檯窗口呢?其實現在很多流行的 開發環境例如JBuilder、Eclipse都是使用純Java開發的集成環境。這些集成環境啓動的時候並不會打開一個命令窗口,因爲它使用了 JNI(Java Native Interface)的技術。通過這種技術,開發人員不一定要用命令行來啓動Java程序,可以    通過編寫一個本地GUI 程序直接啓動Java程序,這樣就可避免另外打開一個命令窗口,讓開發的Java程序更加專業。

 

JNI允許運行在虛擬機的Java程序能夠與其它語言(例如C和C++)編寫的程序或者類庫進行相互間的調用。同時JNI提供的一整套的API,允許將Java虛擬機直接嵌入到本地的應用程序中。圖1是Sun站點上對JNI的基本結構的描述。

 

 

圖1 JNI基本結構描述圖

 

本 文將介紹如何在C/C++中調用Java方法,並結合可能涉及到的問題介紹整個開發的步驟及可能遇到的難題和解決方法。本文所採用的工具是 Sun公司創建的 Java Development Kit (JDK) 版本 1.3.1,以及微軟公司的Visual C++ 6開發環境。

 

1.環境搭建

 

爲了讓本文以下部分的代碼能夠正常工作,我們必須建立一個完整的開發環境。首先需要下載並安裝JDK 1.3.1,其下載地址爲“http://java.sun.com ”。假設安裝路徑爲C:\JDK。下一步就是設置集成開發環境,通過Visual C++ 6的菜單Tools→Options打開選項對話框如圖2。 

 

圖2 設置集成開發環境圖 


將目錄C:\JDK\include和C:\JDK\include\win32加入到開發環境的Include Files目錄中,

同時將C: \JDK\lib目錄添加到開發環境的Library Files目錄中。這三個目錄是JNI定義的一些常量、結構及方法的頭文件和庫文件。

 

集 成開發環境 已經設置完畢,同時爲了執行程序需要把Java虛擬機所用到的動態鏈接庫所在的目錄C:\JDK \jre\bin\classic設置到系統的Path 環境變量中。這裏需要提出的是,某些開發人員爲了方便直接將JRE所用到的DLL文件直接拷貝到系統目錄下。這樣做是不行的,將導致初始化Java虛擬機 環境失敗(返回值-1),原因是Java虛擬機是以相對路徑來尋找所用到的庫文件和其它一些相關文件的。

 

至此整個JNI 的開發環境設置完畢,爲了讓此次 JNI旅程能夠順利進行,還必須先準備一個Java類。在這個類中將用到Java中幾乎所有有代表性的屬性及方法,如靜態方法與屬性、數組、異常拋出與捕 捉等。我們定義的Java程序(Demo.java)如下,本文中所有的代碼演示都將基於該Java 程序,代碼如下: 

package jni.test;

/**

 * 該類是爲了演示JNI如何訪問各種對象屬性等

 * @author liudong

 */

public class Demo {  

 //用於演示如何訪問靜態的基本類型屬性

 public static int COUNT = 8;

 //演示對象型屬性

 public String msg; 
 private int[] counts; 
 public Demo() { 
 this("缺省構造函數"); 
 } 
 /** 
  * 演示如何訪問構造器 
  */ 
 public Demo(String msg) { 
  System.out.println("<init>:" + msg); 
  this.msg = msg; 
  this.counts = null; 
 } 
 /** 
  * 該方法演示如何訪問一個訪問以及中文字符的處理 
  */ 
 public String getMessage() { 
  return msg; 
 }

 /**

  * 演示數組對象的訪問

  */ 
 public int[] getCounts() { 
  return counts; 
 } 
 /** 
  * 演示如何構造一個數組對象 
 */ 
 public void setCounts(int[] counts) { 
  this.counts = counts; 
 } 
 /** 
  * 演示異常的捕捉 
 */ 
 public void throwExcp() throws IllegalAccessException {

  throw new IllegalAccessException("exception occur.");

 }

}

 

 

2.初始化虛擬機

本地代碼在調用Java方法之前必須先加載Java虛擬機,而後所有的Java程序都在虛擬機中執行。

爲 了初始化Java虛擬機,JNI 提供了 一系列的接口函數Invocation API。通過這些API可以很方便地將虛擬機加載到內存中。創建虛擬機可以用函 數 jint JNI_CreateJavaVM(JavaVM **pvm, void **penv, void *args)。對於這個函數有一點 需要注意的是,在JDK 1.1中第三個參數總是指向一個結構JDK1_ 1InitArgs,這個結構無法完全在所有版本的虛擬機中進行無縫移植。在 JDK 1.2中已經使用了一個標準的初始化結構JavaVMInitArgs來替代JDK1_1InitArgs。下面我們分別給出兩種不同版本的示例 代碼。 

在JDK 1.1初始化虛擬機:

#include <jni.h> 
int main() { 
 JNIEnv *env; 
 JavaVM *jvm; 
 JDK1_1InitArgs vm_args; 
 jint res; 
 /* IMPORTANT: 版本號設置一定不能漏 */ 
 vm_args.version = 0x00010001; 
 /*獲取缺省的虛擬機初始化參數*/ 
 JNI_GetDefaultJavaVMInitArgs(&vm_args); 
 /* 添加自定義的類路徑 */ 
 sprintf(classpath, "%s%c%s", 
 vm_args.classpath, PATH_SEPARATOR, USER_CLASSPATH); 
 vm_args.classpath = classpath; 
 /*設置一些其他的初始化參數*/ 
 /* 創建虛擬機 */ 
 res = JNI_CreateJavaVM(&jvm,&env,&vm_args); 
 if (res < 0) { 
  fprintf(stderr, "Can't create Java VM\n"); 
  exit(1); 
 } 
 /*釋放虛擬機資源*/ 
 (*jvm)->DestroyJavaVM(jvm); 

 

JDK 1.2初始化虛擬機:

 

/* invoke2.c */ 
#include <jni.h> 
int main() { 
 int res; 
 JavaVM *jvm; 
 JNIEnv *env; 
 JavaVMInitArgs vm_args; 
 JavaVMOption options[3]; 
 vm_args.version=JNI_VERSION_1_2;//這個字段必須設置爲該值 
 /*設置初始化參數*/ 
 options[0].optionString = "-Djava.compiler=NONE";  
 options[1].optionString = "-Djava.class.path=.";  
 options[2].optionString = "-verbose:jni"; //用於跟蹤運行時的信息 
 /*版本號設置不能漏*/ 
 vm_args.version = JNI_VERSION_1_2; 
 vm_args.nOptions = 3; 
 vm_args.options = options; 
 vm_args.ignoreUnrecognized = JNI_TRUE; 
 res = JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args); 
 if (res < 0) { 
  fprintf(stderr, "Can't create Java VM\n"); 
  exit(1); 
 }    
 (*jvm)->DestroyJavaVM(jvm); 
 fprintf(stdout, "Java VM destory.\n"); 




爲了保證JNI代碼的可移植性,建議使用JDK 1.2的方法來創建虛擬機。JNI_CreateJavaVM函數的第二個參數JNIEnv *env,就是貫穿整個JNI始末的一個參數,因爲幾乎所有的函數都要求一個參數就是JNIEnv *env。 

3.訪問類方法

初始化了Java虛擬機後,就可以開始調用Java的方法。要調用一個Java對象的方法必須經過幾個步驟:

 

3.1.獲取指定對象的類定義(jclass)

 

有兩種途徑來獲取對象的類定義:

第一種是在已知類名的情況下使用FindClass來查找對應的類。但是要注意類名並不同於平時寫的Java代碼,例如要得到類jni.test.Demo的定義必須調用如下代碼: 
jclass cls = (*env)->FindClass(env, "jni/test/Demo"); //把點號換成斜槓


然後通過對象直接得到其所對應的類定義:

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

 

 

3.2.讀取要調用方法的定義(jmethodID)

 

我們先來看看JNI中獲取方法定義的函數: 

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

 

這 兩個函數的區別在於GetStaticMethodID是用來獲取靜態方法的定義,GetMethodID則是獲取非靜態的方法定義。這兩個函 數都需要提供四個參數:env就是初始化虛擬機得到的JNI環境;第二個參數class是對象的類定義,也就是第一步得到的obj;第三個參數是方法名 稱;最重要的是第四個參數,這個參數是方法的定義。因爲我們知道Java中允許方法的多態,僅僅是通過方法名並沒有辦法定位到一個具體的方法,因此需要第 四個參數來指定方法的具體定義。但是怎麼利用一個字符串來表示方法的具體定義呢?JDK中已經準備好一個反編譯工具javap,通過這個工具就可以得到類 中每個屬性、方法的定義。下面就來看看jni.test.Demo的定義:

 

打開命令行窗口並運行 javap -s -p jni.test.Demo 得到運行結果如下: 

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

 

我們看到類中每個屬性和方法下面都有一段註釋。註釋中不包含空格的內容就是第四個參數要填的內容(關於javap具體參數請查詢JDK的使用幫助)。下面這段代碼演示如何訪問jni.test.Demo的getMessage方法:

 

/*  

假設我們已經有一個jni.test.Demo的實例obj  
*/   

jmethodID mid;

jclass cls = (*env)-> GetObjectClass (env, obj); //獲取實例的類定義 
mid=(*env)->GetMethodID( env,cls,"getMessage"," ()Ljava/lang/String; " ); 
/*如果mid爲0表示獲取方法定義失敗*/

jstring msg = (*env)-> CallObjectMethod(env, obj, mid);

/*  

如果該方法是靜態的方法那隻需要將最後一句代碼改爲以下寫法即可: 
jstring msg = (*env)-> CallStaticObjectMethod(env, cls, mid); 
*/

 

 

 

3.3.調用方法

 

爲 了調用對象的某個方法,可以使用函數Call<TYPE>Method或者 CallStatic<TYPE>Method(訪問類的靜態方法),<TYPE>根據不同的返回類型而定。這些方法都是使用可 變參數的定義,如果訪問某個方法需要參數時,只需要把所有參數按照順序填寫到方法中就可以。在講到構造函數的訪問時,將演示如何訪問帶參數的構造函數。

 

4訪問類屬性

 

訪問類的屬性與訪問類的方法大體上是一致的,只不過是把方法變成屬性而已。 

4.1.獲取指定對象的類(jclass)

 

這一步與訪問類方法的第一步完全相同,具體使用參看訪問類方法的第一步。 

4.2.讀取類屬性的定義(jfieldID)

 

在JNI中是這樣定義獲取類屬性的方法的:

 

jfieldID (JNICALL *GetFieldID)

(JNIEnv *env, jclass clazz, const char *name, const char *sig);

jfieldID (JNICALL *GetStaticFieldID)

(JNIEnv *env, jclass clazz, const char *name, const char *sig);

 

 

這兩個函數中第一個參數爲JNI環境;clazz爲類的定義;name爲屬性名稱;第四個參數同樣是爲了表達屬性的類型。前面我們使用javap工具獲取類的詳細定義的時候有這樣兩行: 

public java.lang.String msg;

/*   Ljava/lang/String;   */

 

其中第二行註釋的內容就是第四個參數要填的信息,這跟訪問類方法時是相同的。 

4.3.讀取和設置屬性值

 

有 了屬性的定義要訪問屬性值就很容易了。有幾個方法用來讀取和設置類的屬性,它們是:Get<TYPE>Field、 Set<TYPE>Field、GetStatic<TYPE>Field、 SetStatic<TYPE>Field。比如讀取Demo類的msg屬性就可以用GetObjectField,而訪問COUNT用 GetStaticIntField,相關代碼如下: 

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

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

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

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

5.訪問構造函數

 

 

很 多人剛剛接觸JNI的時候往往會在這一節遇到問題,查遍了整個jni.h看到這樣一個函數NewObject,它應該是可以用來訪問類的構造函 數。但是該函數需要提供構造函數的方法定義,其類型是jmethodID。從前面的內容我們知道要獲取方法的定義首先要知道方法的名稱,但是構造函數的名 稱怎麼來填寫呢?其實訪問構造函數與訪問一個普通的類方法大體上是一樣的,惟一不同的只是方法名稱不同及方法調用時不同而已。訪問類的構造函數時方法名必 須填寫“<init>”。下面的代碼演示如何構造一個Demo類的實例:

 

 

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

/** 

 首先通過類的名稱獲取類的定義,相當於Java中的Class.forName方法

*/

if (cls == 0)   

<error handler>

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

if(mid == 0)  

<error handler>

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

/**

 訪問構造函數必須使用NewObject的函數來調用前面獲取的構造函數的定義

 上面的代碼我們構造了一個Demo的實例並傳一個空串null

*/

 

 

 

6.數組處理

 


6.1創建一個新數組

 

要 創建一個數組,我們首先應該知道數組元素的類型及數組長度。JNI定義了一批數組的類型j<TYPE>Array及數組操作的函數 New<TYPE>Array,其中<TYPE>就是數組中元素的類型。例如,要創建一個大小爲10並且每個位置值分別爲 1-10的整數數組,編寫代碼如下:

 

int i = 1;

jintArray array; //定義數組對象

(*env)-> NewIntArray(env, 10);

for(; i<= 10; i++) 

(*env)->SetIntArrayRegion(env, array, i-1, 1, &i);

 

6.2訪問數組中的數據

 

訪問數組首先應該知道數組的長度及元素的類型。現在我們把創建的數組中的每個元素值打印出來,代碼如下:

 

int i;

/* 獲取數組對象的元素個數 */

int len = (*env)->GetArrayLength(env, array);

/* 獲取數組中的所有元素 */

jint* elems = (*env)-> GetIntArrayElements(env, array, 0);

for(i=0; i< len; i++)

printf("ELEMENT %d IS %d\n", i, elems[i]);

 

7.中文處理

 


中 文字符的處理往往是讓人比較頭疼的事情,特別是使用Java語言開發的軟件,在JNI這個問題更加突出。由於Java中所有的字符都 是 Unicode編碼,但是在本地方法中,例如用VC編寫的程序,如果沒有特殊的定義一般都沒有使用Unicode的編碼方式。爲了讓本地方法能夠訪 問 Java中定義的中文字符及Java訪問本地方法產生的中文字符串,我定義了兩個方法用來做相互轉換。 

· 方法一,將Java中文字符串轉爲本地字符串 

/**

第一個參數是虛擬機的環境指針

第二個參數爲待轉換的Java字符串定義

第三個參數是本地存儲轉換後字符串的內存塊

第三個參數是內存塊的大小

*/

int JStringToChar(JNIEnv *env, jstring str, LPTSTR desc, int desc_len)

{

 int len = 0;

 if(desc==NULL||str==NULL)

 return -1;

 //在VC中wchar_t是用來存儲寬字節字符(UNICODE)的數據類型

 wchar_t *w_buffer = new wchar_t[1024];

 ZeroMemory(w_buffer,1024*sizeof(wchar_t));

 //使用GetStringChars而不是GetStringUTFChars

 wcscpy(w_buffer,env->GetStringChars(str,0));

 env->ReleaseStringChars(str,w_buffer);

 ZeroMemory(desc,desc_len);

 //調用字符編碼轉換函數(Win32 API)將UNICODE轉爲ASCII編碼格式字符串

 //關於函數WideCharToMultiByte的使用請參考MSDN

 len = WideCharToMultiByte(CP_ACP,0,w_buffer,1024,desc,desc_len,NULL,NULL);

 //len = wcslen(w_buffer);

 if(len>0 && len<desc_len)

  desc[len]=0;

 delete[] w_buffer;

 return strlen(desc);


· 方法二,將C的字符串轉爲Java能識別的Unicode字符串


jstring NewJString(JNIEnv* env,LPCTSTR str)

{

 if(!env || !str)

  return 0;

 int slen = strlen(str);

 jchar* buffer = new jchar[slen];

 int len = MultiByteToWideChar(CP_ACP,0,str,strlen(str),buffer,slen);

 if(len>0 && len < slen)

  buffer[len]=0;

 jstring js = env->NewString(buffer,len);

 delete [] buffer;

 return js;

}

 

 

8.異常

 

 

由 於調用了Java的方法,因此難免產生操作的異常信息。這些異常沒有辦法通過C++本身的異常處理機制來捕捉到,但JNI可以通過一些函數來獲 取Java中拋出的異常信息。之前我們在Demo類中定義了一個方法throwExcp,下面將訪問該方法並捕捉其拋出來的異常信息,代碼如下:

 

 

/** 
假設我們已經構造了一個Demo的實例obj,其類定義爲cls 
*/ 
jthrowable excp = 0; /* 異常信息定義 */ 
jmethodID mid=(*env)->GetMethodID(env,cls,"throwExcp","()V"); 
/*如果mid爲0表示獲取方法定義失敗*/ 
jstring msg = (*env)-> CallVoidMethod(env, obj, mid); 
/* 在調用該方法後會有一個IllegalAccessException的異常拋出 */ 
excp = (*env)->ExceptionOccurred(env); 
if(excp){  
 (*env)->ExceptionClear(env); 
 //通過訪問excp來獲取具體異常信息 
 /*

在Java中,大部分的異常信息都是擴展類java.lang.Exception,因此可以訪問excp的toString 
或者getMessage來獲取異常信息的內容。訪問這兩個方法同前面講到的如何訪問類的方法是相同的。

 */

}

 

 

9.線程和同步訪問

 

 

有 些時候需要使用多線程的方式來訪問Java的方法。我們知道一個Java虛擬機是非常消耗系統的內存資源,差不多每個虛擬機需要內存大約在 20MB左右。爲了節省資源要求每個線程使用的是同一個虛擬機,這樣在整個的JNI程序中只需要初始化一個虛擬機就可以了。所有人都是這樣想的,但是一旦 子線程訪問主線程創建的虛擬機環境變量,系統就會出現錯誤對話框,然後整個程序終止。 

其實這裏面涉及到兩個概念,它們分別是虛擬機 (JavaVM *jvm)和虛擬機環境(JNIEnv *env)。真正消耗大量系統資源的是 jvm而不是env,jvm是允許多個線程訪問的,但是env只能被創建它本身的線程所訪問,而且每個線程必須創建自己的虛擬機環境env。這時候會有人 提出疑問,主線程在初始化虛擬機的時候就創建了虛擬機環境env。爲了讓子線程能夠創建自己的env,JNI提供了兩個函數:AttachCurrentThread 和DetachCurrentThread。下面代碼就是子線程訪問Java方法的框架:

 

DWORD WINAPI ThreadProc(PVOID dwParam)

{

 JavaVM jvm = (JavaVM*)dwParam; /* 將虛擬機通過參數傳入 */

 JNIEnv* env;

 (*jvm)-> AttachCurrentThread (jvm,  &env, NULL);

 .........

 (*jvm)-> DetachCurrentThread(jvm);


 

10.時間

 


關 於時間的話題是我在實際開發中遇到的一個問題。當要發佈使用了JNI的程序時,並不一定要求客戶要安裝一個Java運行環境,因爲可以在安裝程 序中打包這個運行環境。爲了讓打包程序利於下載,這個包要比較小,因此要去除JRE(Java運行環境)中一些不必要的文件。但是如果程序中用 到 Java中的日曆類型,例如java.util.Calendar等,那麼有個文件一定不能去掉,這個文件就是[JRE]\lib \tzmappings。它是一個時區映射文件,一旦沒有該文件就會發現時間操作上經常出現與正確時間相差幾個小時的情況。下面是打包JRE中必不可少 的文件列表(以Windows環境爲例),其中[JRE]爲運行環境的目錄,同時這些文件之間的相對路徑不能變。

 

文件名  目錄

hpi.dll       [JRE]\bin 
ioser12.dll    [JRE]\bin 
java.dll      [JRE]\bin 
net.dll       [JRE]\bin 
verify.dll     [JRE]\bin 
zip.dll       [JRE]\bin 
jvm.dll      [JRE]\bin\classic 
rt.jar        [JRE]\lib 
tzmappings   [JRE]\lib 


由於rt.jar有差不多10MB,但是其中有很大一部分文件並不需要,可以根據實際的應用情況進行刪除。例如程序如果沒有用到Java Swing,就可以把涉及到Swing的文件都刪除後重新打包。

 

Chap15:基本JNI調用技術(c/c++與java互調)

 

一.C/C++調用Java

在C/C++中調用Java的方法一般分爲五個步驟:初始化虛擬機、獲取類、創建類對象、調用方法和退出虛擬機。

1. 初始化虛擬機

代碼如下:

    JNIEnv *env;

    JavaVM *jvm;

    JavaVMInitArgs vm_args;

    JavaVMOption options[3];

    int res;

    //設置參數

options[0].optionString = "-Djava.compiler=NONE";

//classpath有多個時,UNIX下以“:”分割。

    options[1].optionString = "-Djava.class.path=.";

    options[2].optionString = "-verbose:jni";

    vm_args.version = JNI_VERSION_1_4;

    vm_args.nOptions = 3;

    vm_args.options = options;

    vm_args.ignoreUnrecognized = JNI_TRUE;

    res = JNI_CreateJavaVM (&jvm, (void**)&env, &vm_args);

      if (res >= 0)

{

     //創建虛擬機成功

}

 

一個應用程序只需要一個虛擬機,但是每個線程需要自己的虛擬機運行環境。我們從一個虛擬機獲取多個當前線程的運行環境,代碼如下:

int result=0;

result=jvm->AttachCurrentThread ( reinterpret_cast<void**>( &env ), 0 );

if(result>=0)

{

     //獲取運行環境成功

}

 

當線程退出時,需要釋放本線程使用的運行環境。

jvm->DetachCurrentThread();

 

2 獲取類

在進行方法調用之前,需要先獲取相應的類,類名稱必須包括包名,其中的“.”用“/”代替。

jclass JavaClass;

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

   if(JavaClass != 0)

   {

            //獲取成功

   }

 

3 創建類對象

如果需要調用的方法靜態方法,則可以跳過本步驟。反之,則需要構造該對象。構造對象是通過調用類的構造函數來實現的,構咱函數的方法聲明爲<init>, GetMethodID方法的參數在下一步驟詳細說明。

jobject obj;

jmethodID ctor;

ctor = env->GetMethodID(JavaClass,"<init>","()V");

if(ctor != 0)//獲取方法成功

   {

         obj = env->NewObject(JavaClass, ctor);

   }

 

4 調用方法

調用一個方法需要兩個步驟:獲取方法句柄和調用方法。

jmethodID methodID = env->GetMethodID( JavaClass, "setTest","(I)V");

if(methodID!=0)//獲取方法成功

{

env->CallVoidMethod( obj, methodID,12);

}

GetStaticMethodID是用來獲取靜態方法的定義,GetMethodID則是獲取非靜態的方法定義。他們傳入參數的參數依次爲:類定義、方法名稱和方法的定義,方法的定義可以用jdk中帶的javap工具反編譯class文件獲取,其格式如下:

public void setTest(int inTest);

Signature: (I)V

Signature後面的內容就是方法的定義。

 

CallVoidMethod 是對獲取的方法進行調用,JNI接口中提供了一系列的同 類方法,包括靜態方法的調用函數(如:CallStaticXXXMethod)和非靜態的方法(如:CallXXXMethod),其中XXX表示的不 同方法返回類型,包括int、object等等。

5 退出虛擬機

退出虛擬機調用方法如下:

jvm->DestroyJavaVM();

在JNI接口定義中,只有最後一個線程退出時,該方法纔會返回,但是我只用一個線程,調用該方法也無法返回。故此建議系統退出時執行該方法,或者整個程序退出時,讓虛擬機自己釋放。

 

[注意]:

l 在處理中文字符串時,需要注意Java的char是雙字節的,採用Unicode編碼,在和C++中的char轉換時,需要用到系統API:WideCharToMultiByte和MultiByteToWideChar。

l 注意對運行環境中對象引用時的釋放,以免引起內存泄漏。

jstring str;

wchar_t *w_buffer =(wchar_t *)env->GetStringChars(str,0);

env->ReleaseStringChars(str,(const unsigned short *)w_buffer);

6 處理異常

C/C++中調用Java時,一定要捕獲並處理Java方法拋出的異常信息,否則可能導致C/C++進程的核心轉儲(Core Dump)。

異常應在每個方法調用後檢查:

msg = (jstring)env->CallObjectMethod(obj, mid);

        if (env->ExceptionOccurred())

        {

           env->ExceptionDescribe();         

            env->ExceptionClear();

           return 0;

        }

二.Java調用C/C++

Java調用C/C++時,遵循幾個步驟:

1、 用Java native 關鍵字聲明方法爲本地方法(非Java語言實現)。

2、 編譯該聲明類,得到XXX.class文件。

3、 用“javah –jni XXX”命令從該class文件生成C語言頭文件(XXX.h)。

4、 採用C語言實現該頭文件聲明的方法,將實現類編譯成庫文件(libXXX.so)。

5、 在Java程序中使用System.loadLibrary(XXX)加載該庫文件(需要設置-Djava.library.path環境變量指向該庫文件存放路徑)。

6、 即可象調用Java方法一樣,調用native方式聲明的本地方法。

 

 

 

Chap16:JNI的c代碼中,另外一個線程獲取 JNIEnv

2009-06-19 14:36

 

JNI 中,JNIEnv * 指針變量只對當前線程有效。如果是其他的線程,需要先獲得 JVM* 指針,然後再獲得當前線程的 JNIEnv * 指針。部分示例代碼爲:

 

/** Invoker.cpp, Invoker.java */

#include <jni.h>

#include <stdio.h>

#include "Invoker.h"

#include "invoker_include.h"

JavaVM *   jvm;

JNIEnv *   static_env;

 

jobject *  jObject; // 線程間公用,必須使用 global reference

jclass     c;         // 必須使用 global reference

jmethodID m;   // 必須使用 global reference

 

/*****************************

* Class:     Invoker

* Method:    register

* Signature: ()V

*****************************/

JNIEXPORT void JNICALL Java_Invoker_register (JNIEnv *env, jobject arg)

{

jObject = arg;

 

// printf("object: %x, %x. \n", &arg, &jObject);

printf("[main] Invoker registered. \n");

 

jclass bgpClass = (*env)->GetObjectClass(env, arg);

jmethodID methodId = (*env)->GetMethodID(env, bgpClass, "invoke", "()V");

printf("[main] -class: %d, method: %d \n", bgpClass, methodId);

 

(*env)->CallVoidMethod(env, arg, methodId);

 

// Global reference

(*env)->GetJavaVM (env, &jvm);

     jObject = (*env)->NewGlobalRef(env, arg);

     c = (*env)->NewGlobalRef(env, bgpClass);

     m = (*env)->NewGlobalRef(env, methodId);

start(invoke_java_method);

(*env)->DeleteGlobalRef(env, c);             // 手動銷燬 global reference

(*env)->DeleteGlobalRef(env, m);            // 手動銷燬 global reference

(*env)->DeleteGlobalRef(env, jObject);

(*jvm)->DetachCurrentThread(jvm);         // 銷燬線程

(*jvm)->DestroyJavaVM (jvm);              // ?銷燬虛擬機

}

 

// Test method

JNIEXPORT void JNICALL Java_Invoker_println (JNIEnv *env, jobject obj, jstring string)

{

const char *str = (*env)->GetStringUTFChars(env, string, 0);

printf("[main] %s\n",str);

(*env)->ReleaseStringUTFChars(env, string, str);

}

 

// Callback method 回調函數

int invoke_java_method ()

{

(*jvm)->AttachCurrentThread(jvm, (void**)&static_env, 0);           // 獲得當前線程可以使用的 JNIEnv * 指針

    (*static_env)->CallVoidMethod(static_env, jObject, m);   // 調用 Java 方法

 

printf("[callback] java method invoked, invoker class: %x ... \n", &jObject);

}

 

 

 

 

 

chap 17:當JNI遇到多線程--java對象如何被C++中的多個線程訪問?

 

java中要訪問C++代碼時, 使用JNI是唯一選擇. 然而,在多線程的情況下, 可能出現以下問題:

問題描述:

一個java對象通過JNI調用DLL中一個send()函數向服務器發送消息,不等服務器消息到來就立即返回.同時把JNI接口的指針JNIEnv *env,和jobject obj保存在DLL中的變量裏.

一段時間後,DLL中的消息接收線程接收到服務器發來的消息,並試圖通過保存過的env和obj來調用先前的java對象的方法來處理此消息.

然而,JNI文檔上說,JNI接口的指針JNIEnv*不能在c++的線程間共享,在我的程序中,如果接收線程試圖調用java對象的方法,程序會突然退出.

不知道有沒有方法突破JNI接口的指針不能在多個c++線程中共享的限制?

解決辦法:

在 http://java.sun.com/docs/books/jni/html/pitfalls.html#29161  提到,
JNI接口指針不可爲多個線程共用,但是java虛擬機的JavaVM指針是整個jvm公用的. 於是,在DLL中可以調用:

static JavaVM* gs_jvm;

env->GetJavaVM (&gs_jvm); 來獲取JavaVM指針.獲取了這個指針後,在DLL中的另一個線程裏,可以調用:

JNIEnv *env;

gs_jvm->AttachCurrentThread((void **)&env, NULL);

來將DLL中的線程 "attached to the virtual machine"(不知如何翻譯...),同時獲得了這個線程在jvm中的  JNIEnv指針.

由 於我需要做的是在DLL中的一個線程裏改變某個java對象的值,所以,還必須獲取那個java對象的jobject指針.同 JNIEnv 指針一樣,jobject指針也不能在多個線程中共享. 就是說,不能直接在保存一個線程中的jobject指針到全局變量中,然後在另外一個線程中使用它.幸運的是,可以用

gs_object=env->NewGlobalRef(obj);

來將傳入的obj保存到gs_object中,從而其他線程可以使用這個gs_object來操縱那個java對象了.

示例代碼如下:

(1)java代碼:

//file name: Test.java

import java.io.*;

class Test  implements  Runnable

{

 public int value  = 0;

 private Thread tx=null;

 

 public Test()

 {

  tx=new Thread(this,"tx");

 }

 

 static

 {

  System.loadLibrary("Test");

 }

 

   public native void setEnev();

 

 public static void main(String args[])

 {

  Test t = new Test();

  t.setEnev();

  System.out.println("ok in java main");

  t.tx.start ();

 

  try

  {

   Thread.sleep(10000000);

  }catch(Exception e)

  {

   System.out.println("error in main");

  }

 }

 

 public void run()

 {

  try

  {

    while(true)

    {

      Thread.sleep(1000);

      System.out.println(value);

    }

  }catch(Exception e)

  {

    System.out.println("error in run");

  }

 }

}

 

(2) DLL代碼:

 

//cpp file name:  Test.cpp:

#include "test.h"

#include<windows.h>

#include<stdio.h>

 

static JavaVM *gs_jvm=NULL;

static jobject gs_object=NULL;

static int gs_i=10;

 

void WINAPI ThreadFun(PVOID argv)

{

 JNIEnv *env;

 gs_jvm->AttachCurrentThread((void **)&env, NULL);

 jclass cls = env->GetObjectClass(gs_object);

 jfieldID fieldPtr = env->GetFieldID(cls,"value","I");

 

 while(1)

 {

 Sleep(100);

 //在DLL中改變外面的java對象的value變量的值.

 env->SetIntField(gs_object,fieldPtr,(jint)gs_i++);

 }

}

 

 

JNIEXPORT void JNICALL Java_Test_setEnev(JNIEnv *env, jobject obj)

{

 printf("come into test.dll\n");

 //Returns “0” on success; returns a negative value on failure.

 int retGvm=env->GetJavaVM(&gs_jvm);

 //直接保存obj到DLL中的全局變量是不行的,應該調用以下函數:

 gs_object=env->NewGlobalRef(obj);

 HANDLE ht=CreateThread( NULL,0,

(LPTHREAD_START_ROUTINE)ThreadFun,0,

NULL,NULL);

 printf("the Handle ht is:%d\n",ht);

}

 

 

 

 

chap 18:JNI在多線程中的應用

 

引文地址:http://blog.csdn.net/hust_liuX/archive/2006/12/25/1460486.aspx

我在這裏將文章整理了一下,重新修改了部分描述和增加了一些重要的說明事項。修改文如下:

問題描述:

一個java對象通過JNI 調用DLL中一個send()函數向服務器發送消息,不等服務器消息到來就立即返回,同時把JNI 接口的指針JNIEnv *env(虛擬機環境指針),和jobject obj保存在DLL中的變量裏.

一段時間後,DLL中的消息接收線程接收到服務器發來的消息,
並試圖通過保存過的env和obj來調用先前的java對象的方法(相當於JAVA回調方法)來處理此消息.此時程序會突然退出(崩潰).

解決辦法:

    解決此問題首先要明白造成這個問題的原因。那麼崩潰的原因是什麼呢?

 

JNI 文檔上有明確表述:  The JNIEnv pointer, passed as the first argument to every native method, can only be used in the thread with which it is associated. It is wrong to cache the JNIEnv interface pointer obtained from one thread, and use that pointer in another thread.

    意思就是JNIEnv指針不能直接在多線程 中共享使用。上面描述的程序崩潰的原因就在這裏:回調時的線程和之前保存變量的線程共享了這個JNIEnv *env指針和jobject obj變量。

 

在 http://java.sun.com/docs/books/jni /html/other.html#26206 提到,JNIEnv *env指針不可爲多個線程共用,但是java虛擬機的JavaVM指針是整個jvm公用的,我們可以通過JavaVM來得到當前線程的JNIEnv指針。

 

 

於是,在第一個線程A中調用:

JavaVM* gs_jvm;

env->GetJavaVM(&gs_jvm); //來獲取JavaVM指針.獲取了這個指針後,將該JavaVM保存起來。

 

在另一個線程B裏,調用

JNIEnv *env;

gs_jvm->AttachCurrentThread((void **)&env, NULL);

 

//這裏就獲得了B這個線程在jvm中的JNIEnv指針.

 

這 裏還必須獲取那個java對象的jobject指針,因爲我們要回調JAVA方法.同 JNIEnv 指針一樣,jobject指針也不能在多個線程中共享. 就是說,不能直接在保存一個線程中的jobject指針到全局變量中,然後在另外一個線程中使用它.幸運的是,可以用   

  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<windows.h>

3.    #include<stdio.h>

4.    static JavaVM *gs_jvm=NULL;

5.    static jobject gs_object=NULL;

6.    static int gs_i=10;

7.     

8.    JNIEXPORT void JNICALL Java_Test_setEnev(JNIEnv *env, jobject obj)

9.    {

10.        env->GetJavaVM(&gs_jvm); //保存到全局變量中JVM

11.       //直接賦值obj到DLL中的全局變量是不行的,應該調用以下函數:

12.        gs_object=env->NewGlobalRef(obj);

13.    

14.   HANDLE ht=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)ThreadFun,0,NULL,NULL);

15.   }

16.    

17.   void WINAPI ThreadFun(PVOID argv)//JNI中線程回調這個方法

18.   {

19.   JNIEnv *env;

20.   gs_jvm->AttachCurrentThread((void **)&env, NULL);

21.   jclass cls = env->GetObjectClass(gs_object);

22.   jfieldID fieldPtr = env->GetFieldID(cls,"value","I");

23.    

24.   while(1)

25.   {

26.        Sleep(100);

27.      //這裏改變JAVA對象的屬性值(回調JAVA)

28.       env->SetIntField(gs_object,fieldPtr,(jint)gs_i++);

29.      }

30.   }

31.    

32.    

JNI

There are certain constraints that you must keep in mind when writing native methods that are to run in a multithreaded environment. By understanding and programming within these constraints, your native methods will execute safely no matter how many threads simultaneously execute a given native method. For example:

  • A JNIEnv pointer is only valid in the thread associated with it. You must not pass this pointer from one thread to another, or cache and use it in multiple threads. The Java virtual machine passes a native method the same JNIEnv pointer in consecutive invocations from the same thread, but passes different JNIEnv pointers when invoking that native method from different threads. Avoid the common mistake of caching the JNIEnv pointer of one thread and using the pointer in another thread.
  • Local references are valid only in the thread that created them. You must not pass local references from one thread to another. You should always convert local references to global references whenever there is a possibility that multiple threads may use the same reference.

 

 

chap 19:JNI限制(多線程)

 

JNI 限制: 
There are certain constraints that you must keep in mind when writing native methods that are to run in a multithreaded environment. By understanding and programming within these constraints, your native methods will execute safely no matter how many threads simultaneously execute a given native method. For example: 
A JNIEnv pointer is only valid in the thread associated with it. You must not pass this pointer from one thread to another, or cache and use it in multiple threads.

The Java virtual machine passes a native method the same JNIEnv pointer in consecutive invocations from the same thread, but passes different JNIEnv pointers when invoking that native method from different threads. Avoid the common mistake of caching the JNIEnv pointer of one thread and using the pointer in another thread.

 

Local references are valid only in the thread that created them. You must not pass local references from one thread to another. You should always convert local references to global references whenever there is a possibility that multiple threads may use the same reference.

                                 本文來自CSDN博客,轉載請標明出處:http://blog.csdn.net/lovingprince/archive/2008/08/19/2793504.aspx

 

 

 

 

 

chap 20:使用 Java Native Interface 的最佳實踐

 

JNI 的發展

JNI 自從 JDK 1.1 發行版以來一直是 Java 平臺的一部分,並且在 JDK 1.2 發行版中得到了擴展。JDK 1.0 發行版包含一個早期的本機方法接口,但是未明確分隔本機代碼和 Java 代碼。在這個接口中,本機代碼可以直接進入 JVM 結構,因此無法跨 JVM 實現、平臺或者甚至各種 JDK 版本進行移植。使用 JDK 1.0 模型升級含有大量本機代碼的應用程序,以及開發能支持多個 JVM 實現的本機代碼的開銷是極高的。

JDK 1.1 中引入的 JNI 支持:

  • 版本獨立性
  • 平臺獨立性
  • VM 獨立性
  • 開發第三方類庫

有一個有趣的地方值得注意,一些較年輕的語言(如 PHP)在它們的本機代碼支持方面仍然在努力克服這些問題。

、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

2009 年 7 月 27 日

Java™ 本機接口(Java Native Interface,JNI)是一個標準的 Java API,它支持將 Java 代碼與使用其他編程語言編寫的代碼相集成。如果您希望利用已有的代碼資源,那麼可以使用 JNI 作爲您工具包中的關鍵組件 —— 比如在面向服務架構(SOA)和基於雲的系統中。但是,如果在使用時未注意某些事項,則 JNI 會迅速導致應用程序性能低下且不穩定。本文將確定 10 大 JNI 編程缺陷,提供避免這些缺陷的最佳實踐,並介紹可用於實現這些實踐的工具。

Java 環境和語言對於應用程序開發來說是非常安全和高效的。但是,一些應用程序卻需要執行純 Java 程序無法完成的一些任務,比如:

  • 與舊有代碼集成,避免重新編寫。
  • 實現可用類庫中所缺少的功能。舉例來說,在 Java 語言中實現 ping 時,您可能需要 Internet Control Message Protocol (ICMP) 功能,但基本類庫並未提供它。
  • 最好與使用 C/C++ 編寫的代碼集成,以充分發掘性能或其他與環境相關的系統特性。
  • 解決需要非 Java 代碼的特殊情況。舉例來說,核心類庫的實現可能需要跨包調用或者需要繞過其他 Java 安全性檢查。

JNI 允許您完成這些任務。它明確分開了 Java 代碼與本機代碼(C/C++)的執行,定義了一個清晰的 API 在這兩者之間進行通信。從很大程度上說,它避免了本機代碼對 JVM 的直接內存引用,從而確保本機代碼只需編寫一次,並且可以跨不同的 JVM 實現或版本運行。

藉助 JNI,本機代碼可以隨意與 Java 對象交互,獲取和設計字段值,以及調用方法,而不會像 Java 代碼中的相同功能那樣受到諸多限制。這種自由是一把雙刃劍:它犧牲 Java 代碼的安全性,換取了完成上述所列任務的能力。在您的應用程序中使用 JNI 提供了強大的、對機器資源(內存、I/O 等)的低級訪問,因此您不會像普通 Java 開發人員那樣受到安全網的保護。JNI 的靈活性和強大性帶來了一些編程實踐上的風險,比如導致性能較差、出現 bug 甚至程序崩潰。您必須格外留意應用程序中的代碼,並使用良好的實踐來保障應用程序的總體完整性。

本文介紹 JNI 用戶最常遇到的 10 大編碼和設計錯誤。其目標是幫助您認識到並避免它們,以便您可以編寫安全、高效、性能出衆的 JNI 代碼。本文還將介紹一些用於在新代碼或已有代碼中查找這些問題的工具和技巧,並展示如何有效地應用它們。

JNI 編程缺陷可以分爲兩類:

  • 性能 :代碼能執行所設計的功能,但運行緩慢或者以某種形式拖慢整個程序。
  • 正確性 :代碼有時能正常運行,但不能可靠地提供所需的功能;最壞的情況是造成程序崩潰或掛起。

1.性能缺陷

程序員在使用 JNI 時的 5 大性能缺陷如下:

1.1.不緩存方法 ID、字段 ID 和類

要訪問 Java 對象的字段並調用它們的方法,本機代碼必須調用 FindClass() 、GetFieldID()GetMethodId() 和 GetStaticMethodID() 。對於 GetFieldID() 、GetMethodID() 和GetStaticMethodID() , 爲特定類返回的 ID 不會在 JVM 進程的生存期內發生變化。但是,獲取字段或方法的調用有時會需要在 JVM 中完成大量工作,因爲字段和方法可能是從超類中繼承而來的,這會讓 JVM 向上遍歷類層次結構來找到它們。由於 ID 對於特定類是相同的,因此您只需要查找一次,然後便可重複使用。同樣,查找類對象的開銷也很大,因此也應該緩存它們。

舉例來說,清單 1 展示了調用靜態方法所需的 JNI 代碼:


清單 1. 使用 JNI 調用靜態方法

                               
int val=1;
jmethodID method;
jclass cls;
 
cls = (*env)->FindClass(env, "com/ibm/example/TestClass");
if ((*env)->ExceptionCheck(env)) {
   return ERR_FIND_CLASS_FAILED;
}
method = (*env)->GetStaticMethodID(env, cls, "setInfo", "(I)V");
if ((*env)->ExceptionCheck(env)) {
   return ERR_GET_STATIC_METHOD_FAILED;
}
(*env)->CallStaticVoidMethod(env, cls, method,val);
if ((*env)->ExceptionCheck(env)) {
   return ERR_CALL_STATIC_METHOD_FAILED;
}

 

當我們每次希望調用方法時查找類和方法 ID 都會產生六個本機調用,而不是第一次緩存類和方法 ID 時需要的兩個調用。

緩存會對您應用程序的運行時造成顯著的影響。考慮下面兩個版本的方法,它們的作用是相同的。清單 2 使用了緩存的字段 ID:


清單 2. 使用緩存的字段 ID

                               
int sumValues2(JNIEnv* env, jobject obj, jobject allValues){
 
   jint avalue = (*env)->GetIntField(env, allValues, a);
   jint bvalue = (*env)->GetIntField(env, allValues, b);
   jint cvalue = (*env)->GetIntField(env, allValues, c);
   jint dvalue = (*env)->GetIntField(env, allValues, d);
   jint evalue = (*env)->GetIntField(env, allValues, e);
   jint fvalue = (*env)->GetIntField(env, allValues, f);
 
   return avalue + bvalue + cvalue + dvalue + evalue + fvalue;
}

 

性能技巧 #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 正確性缺陷包括:

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

您可以在開發週期的早期確定許多常見缺陷,方法如下:

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 回調

                               
<JNI GetStringCritical: buffer=0x100BD010>
<JNI ReleaseStringCritical: buffer=100BD010>
<JNI GetStringChars: buffer=0x03019C88>
<JNI ReleaseStringChars: buffer=03019C88>
<JNI FindClass: java/lang/String>
<JNI FindClass: java/io/WinNTFileSystem>
<JNI GetMethodID: java/io/WinNTFileSystem.<init> ()V>
<JNI GetStaticMethodID: com/ibm/j9/offload/tests/HelloWorld.main ([Ljava/lang/String;)V>
<JNI GetMethodID: java/lang/reflect/Method.getModifiers ()I>
<JNI FindClass: java/lang/String>

 

對於 Sun JVM 而言,開啓 -verbose:jni 選項不會提供關於當前調用的信息,但它會提供關於所使用的本機方法的額外信息。清單 15 顯示了一個示例:


清單 15. 使用 Sun JVM 的 -verbose:jni

                               
[Dynamic-linking native method java.util.zip.ZipFile.getMethod ... JNI]
[Dynamic-linking native method java.util.zip.Inflater.initIDs ... JNI]
[Dynamic-linking native method java.util.zip.Inflater.init ... JNI]
[Dynamic-linking native method java.util.zip.Inflater.inflateBytes ... JNI]
[Dynamic-linking native method java.util.zip.ZipFile.read ... JNI]
[Dynamic-linking native method java.lang.Package.getSystemPackage0 ... JNI]
[Dynamic-linking native method java.util.zip.Inflater.reset ... JNI]

 

開啓此選項還會讓 JVM 針對使用過多本地引用而未通知 JVM 的情況發起警告。舉例來說,IBM JVM 生成了這樣一個消息:

JVMJNCK065W JNI warning in FindClass: Automatically grew local reference frame capacity 
from 16 to 48. 17 references are in use. 
Use EnsureLocalCapacity or PushLocalFrame to explicitly grow the frame. 

 

雖然 -verbose:jni 和 -Xcheck:jni:trace 選項可幫助您方便地獲取所需的信息,但手動審查此信息是一項艱鉅的任務。一個不錯的提議是,創建一些腳本或實用工具來處理由 JVM 生成的跟蹤文件,並查看 警告 。

3.4生成轉儲

運行中的 Java 進程生成的轉儲包含大量關於 JVM 狀態的信息。對於許多 JVM 來說,它們包括關於全局引用的信息。舉例來說,最新的 Sun JVM 在轉儲信息中包括這樣一行:

JNI global references: 73

 

通過生成前後轉儲,您可以確定是否創建了任何未正常釋放的全局引用。

您可以在 UNIX® 環境中通過對 java 進程發起 kill -3 或 kill -QUIT 來請求轉儲。在 Windows® 上,使用 Ctrl+Break 組合鍵。

對於 IBM JVM,使用以下步驟獲取關於全局引用的信息:

  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條目

                               
<rootobject type="Thread" id="0x10089990" reachability="strong" />
<rootobject type="Thread" id="0x10089fd0" reachability="strong" />
<rootobject type="JNIGlobalReference" id="0x100100c0" reachability="strong" />
<rootobject type="JNIGlobalReference" id="0x10011250" reachability="strong" />
<rootobject type="JNIGlobalReference" id="0x10011840" reachability="strong" />
<rootobject type="JNIGlobalReference" id="0x10011880" reachability="strong" />
<rootobject type="JNIGlobalReference" id="0x10010af8" reachability="strong" />
<rootobject type="JNIGlobalReference" id="0x10010360" reachability="strong" />
<rootobject type="JNIGlobalReference" id="0x10081f48" reachability="strong" />
<rootobject type="StringTable" id="0x10010be0" reachability="weak" />
<rootobject type="StringTable" id="0x10010c70" reachability="weak" />
<rootobject type="StringTable" id="0x10010d00" reachability="weak" />
<rootobject type="StringTable" id="0x10011018" reachability="weak" />

 

通過查看後續 Java 轉儲中報告的數值,您可以確定全局引用是否出現的泄漏。

參見 參考資料 獲取關於使用轉儲文件以及 IBM JVM 的 jextract 的更多信息。

3.5執行代碼審查

代 碼審查經常可用於確定常見缺陷,並且可以在各種級別上完成。繼承新代碼時,快速掃描可以發現各種問題,從而避免稍後花費更多時間進行調試。在某些情況下, 審查是確定缺陷實例(比如未檢查返回值)的唯一方法。舉例來說,此代碼的問題可能可以通過代碼審查輕鬆確定,但卻很難通過調試來發現:

int calledALot(JNIEnv* env, jobject obj, jobject allValues){
   jclass cls = (*env)->GetObjectClass(env,allValues); 
   jfieldID a = (*env)->GetFieldID(env, cls, "a", "I");
   jfieldID b = (*env)->GetFieldID(env, cls, "b", "I");
   jfieldID c = (*env)->GetFieldID(env, cls, "c", "I");
   jfieldID d = (*env)->GetFieldID(env, cls, "d", "I");
   jfieldID e = (*env)->GetFieldID(env, cls, "e", "I");
   jfieldID f = (*env)->GetFieldID(env, cls, "f", "I");
 
}
 
jclass getObjectClassHelper(jobject object){ 
   /* use globally cached JNIEnv */
   return cls = (*globalEnvStatic)->GetObjectClass(globalEnvStatic,allValues); 
}

 

代碼審查可能會發現第一個方法未正確緩存字段 ID,儘管重複使用了相同的 ID,並且第二個方法所使用的JNIEnv 並不在應該在的線程上。

 

 

 

回頁首

 

4.結束語

現在,您已經瞭解了 10 大 JNI 編程缺陷,以及一些用於在已有或新代碼中確定它們的良好實踐。堅持應用這些實踐有助於提高 JNI 代碼的正確率,並且您的應用程序可以實現所需的性能水平。

有 效集成已有代碼資源的能力對於面向對象架構(SOA)和基於雲的計算這兩種技術的成功至關重要。JNI 是一項非常重要的技術,用於將非 Java 舊有代碼和組件集成到基於 Java 的平臺中,充當 SOA 或基於雲的系統的基本元素。正確使用 JNI 可以加速將這些組件轉變爲服務的過程,並允許您從現有投資中獲得最大優勢。

 

參考資料

學習

 

 

Chap21:JNI設計實踐之路

一、       前言

本 文爲在 32 位 Windows 平臺上實現 Java 本地方法提供了實用的示例、步驟和準則。本文中的示例使用 Sun公司的 Java Development Kit (JDK) 版本 1.4.2。用 C ++語言編寫的本地代碼是用 Microsoft Visual C++  6.0編譯器編譯生成。規定在Java程序中function/method稱爲方法,在C++程序中稱爲函數。

本 文將圍繞求圓面積逐步展開,探討java程序如何調用現有的DLL?如何在C++程序中創建,檢查及更新Java對象?如何在C++和Java程序中互拋 異常,並進行異常處理?最後將探討Eclipse及JBuilder工具可執行文件爲什麼不到100K大小以及所採用的技術方案?

二、       JNI基礎知識簡介

Java 語言及其標準API應付應用程序的編寫已綽綽有餘。但在某些情況下,還是必須使用非Java代碼,例如:打印、圖像轉換、訪問硬件、訪問現有的非Java 代碼等。與非Java代碼的溝通要求獲得編譯器和JVM的專門支持,並需附加的工具將Java代碼映射成非Java代碼。目前,不同的開發商爲我們提供了 不同的方案,主要有以下方法:

1.         JNI(Java Native Interface)

2.         JRI(Java Runtime Interface)

3.         J/Direct

4.         RNI(Raw Native Interface)

5.         Java/COM集成方案

6.         CORBA(Common Object Request Broker Architecture)

其中方案1是JDK自帶的一部分,方案2由網景公司所提供,方案3 、 4 、 5是微軟所提供的方案,方案6是一家非盈利組織開發的一種集成技術,利用它可以在由不同語言實現的對象之間進行“相互操作”的能力。

在開發過程中,我們一般採用第1種方案――JNI技術。因爲只用當程序用Microsoft Java編譯器編譯,而且只有在Microsoft Java虛擬機(JVM)上運行的時候,才採用方案3 、 4 、 5。而方案6一般應用在大型的分佈式應用中。

       JNI是一種包容極廣的編程接口,允許我們從Java應用程序裏調用本地化方法。也就是說,JNI允許運行在虛擬機上的Java程序能夠與其它語言(例如 C/ C++/彙編語言)編寫的程序或者類庫進行相互間的調用。同時JNI也提供了一整套的API,允許將Java虛擬機直接嵌入到本地的應用程序中。其中 JNI所扮演的角色可用圖一描述:

 

 

圖一 JNI基本結構描述圖

目前JNI只能與用C和C++編寫的本地化方法打交道。利用JNI,我們本地化方法可以:

1.         創建、檢查及更新Java對象

2.         調用Java和非Java程序所編寫的方法(函數),以及win32 API等.

3.         捕獲和拋出“異常”

4.         裝載類並獲取類信息

5.         進行運行期類型檢查

所以,原來在Java程序中能對類及對象所做的幾乎所有事情都可以在本地化方法中實現。

下圖表示了通過JNI,Java程序和非Java程序相互調用原理。

圖二   Java程序和非Java程序通過JNI相互調用原理

 

通過JNI,編寫Java程序調用非Java程序一般步驟:

1.)    編寫對本地化方法及自變量進行聲明的Java代碼

2.)    利用頭文件生成器javah生成本地化方法對應的頭文件

3.)    利用C和C++實現本地化方法(可調用非Java程序),並編譯、鏈接生成DLL文件

4.)    Java程序通過生成的DLL調用非Java程序

 

同時我們也可以通過JNI,將Java虛擬機直接嵌入到本地的應用程序中,步驟很簡單,只需要在C/C++程序中以JNI API函數爲媒介調用Java程序。

以上步驟雖簡單,但有很多地方值得注意。如果一招不慎,可能造成滿盤皆輸。

三、       Java程序調用非Java程序

3.1 本地化方法聲明及頭文件生成

任務:現有一求圓面積的Circle.dll(用MFC編寫,參數:圓半徑;返回值:圓面積)文件,在Java程序中調用該Dll。

在本地化聲明中,可分無包和有包兩種情況。我們主要對有包的情況展開討論。

實例1:

package com.testJni;

public class Circle

{

   public native void cAreas(int radius) ;

   static

   {

        //System.out.println(System.getProperty("java.library.path"));

        System.loadLibrary("CCircle");

    }

}

在Java程序中,需要在類中聲明所調用的庫名稱System.loadLibrary( String libname );

該 函數是將一個Dll/so庫載入內存,並建立同它的鏈接。定位庫的操作依賴於具體的操作系統。在windows下,首先從當前目錄查找,然後再搜 尋”PATH”環境變量列出的目錄。如果找不到該庫,則會拋出異常UnsatisfiedLinkError。庫的擴展名可以不用寫出來,究竟是Dll還 是so,由系統自己判斷。這裏加載的是3.2中生成的DLL,而不是其他應用程序生成的Dll。還需要對將要調用的方法做本地聲明,關鍵字爲native 。表明此方法在本地方法中實現,而不是在Java程序中,有點類似於關鍵字abstract。

    我們寫一個Circle.bat批處理文件編譯Circle.java文件,內容如下(可以用其他工具編譯):

javac -d . Circle.java

javah com.testJni.Circle

pause

 

對於有包的情況一定要注意這一點 , 就是在用javah時有所不同。開始時我的程序始終運行都不成功,問題就出在這裏。本類名稱的前面均是包名。這樣生成的頭文件就 是:com_testJni_Circle.h。開始時,在包含包的情況下我用javah Circle生成的頭文件始終是Circle.h。在網上查資料時,看見別人的頭文件名砸那長,我的那短。但不知道爲什麼,現在大家和我一樣知道爲什麼了 吧。:)。

如果是無包的情況,則將批處理文件換成如下內容:

javac Circle.java

javah Circle

pause

3.2 本地化方法實現

剛纔生成的com_testJni_Circle.h頭文件內容如下:

/* DO NOT EDIT THIS FILE - it is machine generated */

#include <jni.h>

/* Header for class com_testJni_Circle */

#ifndef _Included_com_testJni_Circle

#define _Included_com_testJni_Circle

#ifdef __cplusplus

extern "C" {

#endif

/*

 * Class:     com_testJni_Circle

 * Method:    cAreas

 * Signature :  (I)V

 */

JNIEXPORT void JNICALL Java_com_testJni_Circle_cAreas

 (JNIEnv *, jobject, jint);

#ifdef __cplusplus

}

#endif

#endif

如果在本地化方法聲明中,方法cAreas ()聲明爲static類型,則與之相對應的Java_com_testJni_Circle_cAreas()函數中的第二個參數類型爲jclass。也就是

JNIEXPORT void JNICALL Java_com_testJni_Circle_cAreas(JNIEnv *env, jclass newCircle ,jint radius)。

這裏JNIEXPORT 和JNICALL 都是JNI的關鍵字,其實是一些宏(具體參看jni_md.h文件)。

從以上頭文件中,可以看出函數名生成規則爲:Java[ _包名]_類名_方法名[ _函數簽名] (其中[ ]是可選項),均以字符下劃線( _ )分割。如果是無包的情況,則不包含[ _包名]選項。如果本地化方法中有方法重載,則在該函數名最後面追加函數簽名,也就是Signature對應的值,函數簽名參見表一。

 

 

 

 

 

 

 

 

 

函數簽名

Java類型

V

void

Z

boolean

B

byte

C

char

S

short

I

int

J

long

F

float

D

double

L fully-qualified-class ;

fully-qualified-class

[ type

type[]

( arg-types ) ret-type

method type

表一函數簽名與Java類型的映射

在具體實現的時候,我們只關心函數原型: 

JNIEXPORT void JNICALL Java_com_testJni_Circle_cAreas(JNIEnv *, jobject, jint);

現 在就讓我們開始激動人心的一步吧 : ) 。啓動VC集成開發環境,新建一工程,在project裏選擇win32 Dynamic-link Library,輸入工程名,然後點擊ok,接下去步驟均取默認(圖三)。如果不取默認,生成的工程將會有DllMain ()函數,反之將無這個函數。我在這裏取的是空。

圖三   新建DLL工程

然 後選擇菜單File->new->Files->C++ Source File,生成一個空*.cpp文件,取名爲CCircle。與3.1中System.loadLibrary("CCircle");參 數保持一致。將JNIEXPORT void JNICALL Java_com_testJni_Circle_cAreas(JNIEnv *, jobject, jint);拷貝到CPP文件中,幷包含其頭文件。

對應的CCircle.cpp內容如下:

#include<iostream.h>

#include"com_testJni_Circle.h"

#include"windows.h"

 

JNIEXPORT void JNICALL Java_com_testJni_Circle_cAreas(JNIEnv *env, jobject newCircle,jint radius)

{

     //調用求圓面積的Circle.dll

     typedef void (*PCircle)(int radius);

     HINSTANCE hDLL;

     PCircle Circle;

     hDLL=LoadLibrary("Circle.dll");//加載動態鏈接庫Circle.dll文件

     Circle=(PCircle)GetProcAddress(hDLL,"Circle");

     Circle(8);

     FreeLibrary(hDLL);//卸載Circle.dll文件;

}

 

在編譯前一定要注意下列情況。

注意: 一定要把SDK目錄下include文件夾及其下面的win32文件夾中的頭文件拷貝到VC目錄的include文件夾下。或者在VC的tools\options\directories中設置,如圖四所示。

圖四   頭文件設置

我們知道dll文件有兩種指明導出函數的方法,一種是在.def文件中定義,另一種是在定義函數時使用關鍵字__declspec(dllexport)。而關鍵字JNIEXPORT實際在jni_md.h中如下定義,

#define JNIEXPORT __declspec(dllexport),可見JNI默認的導出函數使用第二種。使用第二種方式產生的導出函數名會根據編譯器發生變化,在有的情況下 會發生找不到導出函數的問題(我們在java控制檯程序中調用很正常,但把它移植到JSP頁面時,就發生了該問題,JVM開始崩潰,百思不得其解,後來加 入一個.def文件才解決問題)。其實在《windows 核心編程》一書中,第19.3.2節就明確指出創建用於非Visual C++工具的DLL時,建議加入一個def文件,告訴Microsoft編譯器輸出沒有經過改變的函數名。因此最好採用第一種方法,定義一個.def文件 來指明導出函數。本例中可以新建一個CCircle.def文件,內容如下:

; CCircle.def : Declares the module parameters for the DLL.

 

LIBRARY      "CCircle"

DESCRIPTION 'CCircle Windows Dynamic Link Library'

 

EXPORTS

    ; Explicit exports can go here

    Java_com_testJni_Circle_cAreas

現在開始對所寫的程序進行編譯。選擇build->rebuild all對所寫的程序進行編譯。點擊build->build CCirclee.DLL生成DLL文件。

也可以用命令行cl來編譯。語法格式參見JDK文檔JNI部分。

再次強調(曾經爲這個東西大傷腦筋) :DLL放置地方

1)        當前目錄。

2)        Windows的系統目錄及Windows目錄

3)        放在path所指的路徑中

4)        自己在path環境變量中設置一個路徑,要注意所指引的路徑應該到.dll文件的上一級,如果指到.dll,則會報錯。

下面就開始測試我們的所寫的DLL吧(假設DLL已放置正確)。

import com.testJni.Circle;

public class test

{

   public static void main(String argvs[])

   {

      Circle myCircle;

      myCircle = new Circle();

      myCircle.cAreas(2);

   }     

}

編譯,運行程序,將會彈出如下界面:

圖五 運行結果

以上是我們通過JNI方法調用的一個簡單程序。實際情況要比這複雜的多。

現在開始來討論JNI中參數的情況,我們來看一個程序片斷。

實例二:

JNIEXPORT jstring JNICALL Java_MyNative_cToJava

(JNIEnv *env, jclass obj)

{

              jstring jstr;

              char str[]="Hello,word!\n";

              jstr=env->NewStringUTF(str);

              return jstr;

}

在 C和Java編程語言之間傳送值時,需要理解這些值類型在這兩種語言間的對應關係。這些都在頭文件jni.h中定義,用typedef語句聲明瞭這些類在 目標平臺上的代價類。頭文件也定義了常量如:JNI_FALSE=0 和JNI_TRUE=1;表二和表三說明了Java類型和C類型之間的映射關係。

Java語言

C/C++語言

bit位數

boolean

jboolean

8 unsigned

byte

jbyte

8

char

jchar

16 unsigned

short

jshort

16

int

jint

32

long

jlong

64

float

jfloat

32

double

jdouble

64

void

void

0

表二   Java基本類型到本地類型的映射

表三 Java中的類到本地類的映射

JNI 函數NewStringUTF()是從一個包含UTF格式編碼字符的char類型數組中創建一個新的jstring對象。jstring是以JNI爲中介 使Java的String類型與本地的string溝通的一種類型,我們可以視而不見 (具體對應見表二和表三)。如果你使用的函數是GetStringUTFChars()(將jstring轉換爲UTF-8字符串),必須同時使用 ReleaseStringUTFChars()函數,通過它來通知虛擬機去回收UTF-8串佔用的內存,否則將會造成內存泄漏,最終導致系統崩潰。因爲 JVM在調用本地方法時,是在虛擬機中開闢了一塊本地方法棧供本地方法使用,當本地方法使用完UTF-8串後,得釋放所佔用的內存。其中程序片斷 jstr=env->NewStringUTF(str);是C++中的寫法,不必使用env指針。因爲JNIEnv函數的C++版本包含有直接插 入成員函數,他們負責查找函數指針。而對於C的寫法,應改爲:jstr=(*env)-> NewStringUTF(env ,str);因爲所有JNI函數的調用都使用env指針,它是任意一個本地方法的第一個參數。env指針是指向一個函數指針表的指針。因此在每個JNI函數訪問前加前綴(*env)->,以確保間接引用函數指針。

C/C++和Java互傳參數需要自己在編程過程中仔細摸索與體味。

四、       C/C++訪問Java成員變量和成員方法

我們修改3.1中的Java程序聲明,加入如下代碼:

   private int circleRadius;

   public Circle()

   {

       circleRadius=0;

   }

   public void setCircleRadius(int radius)

   {

       circleRadius=radius;

   }

   public void javaAreas()

   {

       float PI = 3.14f;

       if(circleRadius<=0)

       {

           System.out.println (“error!”);

       }

       else

       {

          System.out.println (PI*circleRadius*circleRadius);

       }      

    }

在C++程序中訪問Circle類中的private私有成員變量circleRadius,並設置它的值,同時調用Java方法javaAreas()。在函數Java_com_testJni_Circle_cAreas()中加入如下代碼:

jclass circle;

       jmethodID AreasID;

       jfieldID radiusID;

jint newRadius=5;

       circle = env->GetObjectClass(newCircle);//get current class

       radiusID=env->GetFieldID(circle,"circleRadius","I");//get field ID

       env->SetIntField(newCircle,radiusID,newRadius);//set field value

       AreasID=env->GetMethodID(circle,"javaAreas","()V");//get method ID

       env->CallVoidMethod(newCircle,AreasID,NULL);//invoking method

在C++代碼中,創建、檢查及更新Java對象,首先要得到該類,然後再根據類得到其成員的ID,最後根據該類的對象,ID號調用成員變量或者成員方法。

得 到類,有兩個API函數,分別爲FindClass()和GetObjectClass();後者顧名思義用於已經明確知道其對象,然後根據對象找類。前 者用於得到沒有實例對象的類。這裏也可以改成circle = env-> FidnClass("com/testJni/Circle");其中包的分隔符用字符" /"代替。如果已知一個類,也可以在C++代碼中創建該類對象,其JNI函數爲NewObject();示例代碼如下:

jclass      circle =env->FindClass("com/testJni/ Circle ");

jmethodID circleID=env->GetMethodID(circle,"<init>","()V");//得到構造函數的ID

jobject newException=env->NewObject(circle, circleID,NULL);

得 到成員變量的ID,根據其在Java代碼中聲明的類型不同而不同。具體分爲兩大類:非static型和static型,分別對應GetFieldID() 和GetStaticFieldID()。同時也可以獲得和設置成員變量的值,根據其聲明的type而變化,獲得其值的API函數爲:Get typeField ()和GetStatic typeField ();與之相對應的設置其值的函數爲Set typeField ()和SetStatic typeField ();在本例中,成員變量circleRadius聲明成int型,則對應的函數分別爲GetIntField()和SetIntField();

其實JNI API函數名是很有規律的,從上面已窺全貌。獲得成員方法的ID也是同樣的分類方法。具體爲GetMethodID()和GetStaticMethodID()。調用成員方法跟獲得成員變量的值相類似,也根據其方法返回值的type不同而不同,分別爲Call typeMethod ()和CallStatic typeMethod ()。對於返回值爲void的類型,其相應JNI函數爲CallVoidMethod();

以上獲得成員ID函數的形參均一致。第一個參數爲jclass,第二個參數爲成員變量或方法,第三個參數爲該成員的簽名(簽名可參見表一)。但調用或設置成員變量或方法時,第一個參數爲實例對象(即jobject),其餘形參與上面相同。

特別要注意的是得到構造方法的ID時,第二個參數不遵循上面的原則,爲jmethodID constructorID = env->GetMethodID(jclass, "<init>" ," 函數簽名");

從上面代碼中可以看出,在C++中可以訪問java程序private類型的變量,嚴重破壞了類的封裝原則。從而可以看出其不安全性。

五、       異常處理

本地化方法穩定性非常差,調用任何一個JNI函數都會出錯,爲了程序的健壯性 ,非常有必要在本地化方法中加入異常處理。我們繼續修改上面的類。

我們聲明一個異常類,其代碼如下:

package com.testJni;

import com.testJni.*;

public class RadiusIllegal extends Exception

{

    protected String MSG="error!";

    public RadiusIllegal(String message)

    {

        MSG=message;

    }

    public void print()

    {

       System.out.println(MSG);

    }

}

同時也修改Circle.java中的方法,加入異常處理。

    public void javaAreas() throws RadiusIllegal    //修改javaAreas(),加入異常處理

   {

       float PI = 3.14f;

       if(circleRadius<=0)

       {

           throw new RadiusIllegal("warning:radius is illegal!");

       }

       else

       {

            System.out.println (PI*circleRadius*circleRadius);

       }      

   }

   public native void cAreas(int radius) throws RadiusIllegal;    //修改cAreas (),加入異常處理

   修改C++代碼中的函數,加入異常處理,實現Java和C++互拋異常,並進行異常處理。

   JNIEXPORT void JNICALL Java_com_testJni_Circle_cAreas(JNIEnv *env, jobject newCircle,jint radius)

{

       //此處省略部分代碼

       radiusIllegal=env->FindClass("com/testJni/RadiusIllegal");//get the exception class

       if((exception=env->ExceptionOccurred())!=NULL)

       {

              cout<<"errors in com_testJni_RadiusIllegal"<<endl;

              env->ExceptionClear();

       }    

       //此處省略部分代碼

       env->CallVoidMethod(newCircle,AreasID,NULL);//invoking

       if((exception=env->ExceptionOccurred())!=NULL)

       {

              if(env->IsInstanceOf(exception,radiusIllegal)==JNI_TRUE)

              {                  

                     cout<<"errors in java method"<<endl;

                     env->ExceptionClear();

              }

              else

              {

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

                     env->ExceptionClear();

              }

        }   

        if(radius<=0)

        {

              env->ThrowNew(radiusIllegal,"errors in C function!");//throw exception

         return ;

        }

        else

        {

              //此處爲調用計算圓面積的DLL

        }

}

在本地化方法(C++)中,可以自己處理異常,也可以重新拋出異常,讓Java程序來捕獲該異常,進行相關處理。

如 果調用JNI函數發生異常,不及時進行處理,再次調用其他JNI函數時,可能會使JVM崩潰(crash),大多數JNI函數都具有此特性。可以調用函數 ExceptionOccurred()來判斷是否發生了異常。該函數返回jthrowable的實例對象,如本例 if((exception=env->ExceptionOccurred())!=NULL)就用來判斷是否發生了異常。當要判斷具體是哪個異 常發生時,可以用IsInstanceOf()來進行測試,此函數非彼IsInstanceOf(Java語言中的IsInstanceOf)。在上面的 代碼中,我們在本地化方法中給circleRadius設置了一非法值,然後調用方法javaAreas(),此時java代碼會拋出異常,在本地化方法 中進行捕獲,然後用IsInstanceOf()來進行測試是否發生了RadiusIllegal類型的異常,以便進行相關處理。在調用其他JNI函數之 前,應當首先清除異常,其函數爲ExceptionClear()。

如果是C++的程序發生異常,則可以用 JNI API函數ThrowNew()拋出該異常。但此時本地化方法並不返回退出,直到該程序執行完畢。所以當在本地化方法中發生異常時,應該人爲的退出,及時 進行處理,避免程序崩潰。函數ThrowNew()中第一個參數爲jclass的類,第二個參數爲附加信息,用來描述異常信息。

如果要知道異常發生的詳細信息,或者對程序進行調試時,可以用函數ExceptionDescribe()來顯示異常棧裏面的內容。

 

六、       MFC程序中嵌入Java虛擬機

可能大家天天都在用Eclipse和Jbulider這兩款優秀的IDE進行程序開發,可能還不知道他們的可執行文件不到100KB大小,甚則連一副圖片都可能比他們大。其實隱藏在他們背後的技術是JNI,可執行文件只是去啓動Java程序,所以也只有那麼小。

我 們只需要在MFC程序中創建一個JVM,然後基於這個JVM調用Java的方法,啓動Java程序,就可以模擬出Eclipse和Jbulider的那種 效果,使java程序更專業。其實要實現這種效果,用上面的方法足有夠有。創建JVM,只需包含相應的類庫,設置相關的屬性。

首先進行環境設置,在VC環境的tools-->options-->Directories下的Library files選項中包含其創建JVM的庫文件jvm.lib,該庫文件位於JDK \ lib目錄下,如圖6所示:

 

圖六庫文件路徑設置

然後,在環境變量path中設置jvm.dll的路徑。該Dll      位於jdk\jre\bin\server目錄或jdk\jre\bin\client目錄下。注意:一定不要將jvm.dll和jvm.lib拷貝到你應用程序路徑下 ,這樣會引起JVM初始化失敗。因爲Java虛擬機是以相對路徑來尋找和調用用到的庫文件和其他相關文件。

接下來,我們在MFC程序(該程序請到《程序員》雜誌頻道下載)中進行創建JVM初始化工作。示例代碼如下:

       JNIEnv *env;

       JavaVM *jvm;

       jint res;

       jclass cls;

       jmethodID mid;      

       JavaVMInitArgs vm_args;

       JavaVMOption options[3];

       memset(&vm_args, 0, sizeof(vm_args));    

     //進行初始化工作      

       options[0].optionString = "-Djava.compiler=NONE";

       options[1].optionString = "-Djava.class.path=.";

       options[2].optionString = "-verbose:jni";      

       vm_args.version=JNI_VERSION_1_4;       //版本號設置

       vm_args.nOptions = 3;

       vm_args.options = options;

       vm_args.ignoreUnrecognized = JNI_TRUE;

 

       res = JNI_CreateJavaVM(&jvm,(void**)&env,&vm_args); //創建JVM

       if (res < 0)

       {

              MessageBox( "Can't create Java VM","Error",MB_OK|MB_ICONERROR);

              exit(1);

       }

       cls = env->FindClass("prog");

       if(env->ExceptionOccurred()!=NULL)

       {

              MessageBox( "Can't find Prog class!","Error",MB_OK|MB_ICONERROR);

              exit(1);

       }

       mid = env->GetStaticMethodID(cls, "main", "([Ljava/lang/String;)V");

       if(env->ExceptionOccurred()!=NULL)

       {

              MessageBox("Can't find Prog.main!","Error",MB_OK|MB_ICONERROR);

              exit(1);

       }

       env->CallStaticVoidMethod( cls, mid, NULL); //調用Java程序main()方法,啓動Java程序

       if(env->ExceptionOccurred()!=NULL)

       {

              MessageBox( "Fatal Error!","Error",MB_OK|MB_ICONERROR);

              exit(1);

       }

       jvm->DestroyJavaVM();//釋放JVM資源

程序首先進行JVM初始化設置。我們觀察jni.h 文件關於JavaVMOption和JavaVMInitArgs的定義

typedef struct JavaVMOption {

    char *optionString;

    void *extraInfo;

} JavaVMOption;

 

typedef struct JavaVMInitArgs {

    jint version;

    jint nOptions;

    JavaVMOption *options;

    jboolean ignoreUnrecognized;

} JavaVMInitArgs;

結 構體JavaVMInitArgs中有四個參數,我們在程序中都得必須設置。其中版本號一定要設置正確,不同的版本有不同的設置方法,關於版本1.1和 1.2的設置方法參看sun公司的文檔,這裏只給出版本1.4的設置方法。第二個參數表示JavaVMOption結構體變量的維數,這裏設置爲三維,其 中options[0].optionString = "-Djava.compiler=NONE";表示disable JIT;options[1].optionString = "-Djava.class.path=.";表示你所調用Java程序的Class文件的路徑,這裏設置爲該exe應用程序的根路徑(最後一個字 符"."表示根路徑);options[2].optionString = "-verbose:jni";用於跟蹤運行時的信息。第三個參數是一個JavaVMOption的指針變量。第四個參數意思我們可以參看幫助文檔的解釋 If ignoreUnrecognized is JNI_FALSE, JNI_CreateJavaVM returns JNI_ERR as soon as it encounters any unrecognized option strings。

初始化完畢後,就可以調用創建JVM的函數jint JNICALL JNI_CreateJavaVM(JavaVM **pvm, void **penv, void *args);如果返回值小於0表示創建JVM失敗。最可能的原因就是jvm.dll和jvm.lib 設置錯誤。

如果在運行的過程中找不到java程序的類,那麼就是-Djava.class.path設置錯誤。只要JVM創建成功,就可以根據上面的方法調用java程序。最後當程序結束後,調用函數DestroyJavaVM()摧毀JVM,釋放資源。

七、       附錄

利 用JNI函數,我們可以從本地化方法的內部與JVM打交道。正如在前面的例子中所看到的那樣,每個本地化方法中都會接收一個特殊的自變量作爲自己的第一個 參數:JNIEnv――它是指向類型爲JNIEnv_的一個特殊JNI數據結構的指針。JNI數據結構的一個元素是指向由JVM生成的一個指針的數組;該 數組的每個元素都是指向一個JNI函數的指針。可以從本地化方法的內部對JNI函數的調用。第二個參數會根據Java類中本地方法的定義不同而不同,如果 是定義爲static方法,類型會是jclass,表示對特定Class對象的引用,如果是非static方法,類型是jobject,表示當前對象的引 用,相當於” this”。可以說這兩個變量是本地化方法返回JAVA的大門。

注意: 在本地化方法中生成的Dll不具備到處運行的特性,而具有”牽一髮而動全身”的特點。只要包名一改變,那麼你所有的工作就得重新做一遍。原因就是當用javah生成頭文件時,函數名的生成規則爲Java[ _包名]_類名_方法名[ _函數簽名] ;當你的包名改變時,生成的函數名也跟着改變了,那麼你再次調用以前編寫的Dll時,會拋出異常。

 

八、       參考文獻

1.         《Java 編程思想》Bruce Eckel 機械工業出版社

2.         《Java2 核心技術卷2》(第6版)Cay S.Horstmann,Gary Cornell機械工業出版社

3.         《高級Java2 大學教程》(英文版) Harvey M.Deitel ,Paul J.Deitel,Sean E.Santry 電子工業出版社

4.         《windows 核心編程》Jeffrey Richter 機械工業出版社

5.           http://www.jguru.com

6.                    sun公司文檔

 

如對本文有任何疑問和異議,歡迎與作者探討:[email protected]

 

注:本文最初發表在2004年《開發高手》第12期上。

 

 

 

=====================================================================

利用jawin完成調用window中dll的調用 
http://hi.baidu.com/linjk03/blog/item/2c506a349988d6bfd0a2d34c.html

最近由於項目的特殊需求,我們必須在程序調用window的dll。
開始我們用jni,後來由於調用的dll太多,而且很煩瑣。所以,我們決定用開源的jawin調用。
jawin 可以對dll中的方法進行調用,也可以調用com中的方法.內部還提供了一個工具,直接對 com組件導出成 java的類,個人認爲很方便。

下面是我們作的一個測試,很順利便通過了。
1、下載jawin:http://jawinproject.sourceforge.net/ 。
2、配置:
    》將jawin.jar放於%JAVA_HOME%\jre\lib\ext下 。
    》將jawin.dll放於c:\winnt\system32下。否則將出現錯誤:COMException : no jawin in java.library.path; 
    也可將jawin.dll放於每個項目目錄下。

   》至此在Editplus中調試Jawin/NJawin的例子,可以通過。 而在Eclipse中有時還會出上面的錯誤:COMException : no jawin in java.library.path。 
   》在Eclipse中,菜單->window->preference->Java->installed JREs 將原來的remove,重新建一個指到你的java sdk目錄。 
   》 ok了。
3、程序測試:

     》調用 dll,dll 的方式不需要導出了,直接調用就可以了,下面是下載的包中提供的一個例子:
     》我在win2000下,測試通過。
/*
* Created on Dec 22, 2005
*
*/
import org.jawin.FuncPtr;

import org.jawin.ReturnFlags;

/**
* @author gf mail to [email protected] 
*
* TODO To change the template for this generated type comment go to
* Window - Preferences - Java - Code Style - Code Templates
*/
public class GfJawinTest {

       public static void main(String[] args) {

              try {

                     FuncPtr msgBox = new FuncPtr("USER32.DLL", "MessageBoxW");

 

                     msgBox.invoke_I(0, "Hello From a DLL", "From Jawin", 0, ReturnFlags.CHECK_NONE);

              } catch (Exception e) {

                     e.printStackTrace();

              }

       }

}

 

 

 

 

 

static int32_t readInputStreamData(int32_t handle, uint8_t* buf, int32_t bufLen)
{
    JNIEnv* env;
    jobject* pInputStream;
    int32_t len;
    DrmData* p;
    jclass cls;
    jmethodID mid;
    jbyteArray tmp;
    int tmpLen;
    jbyte* pNativeBuf;
 
    p = (DrmData *)handle;
 
    if (NULL == p || NULL == buf || bufLen <- 0)
        return 0;
 
    env = p->env;
    pInputStream = p->pInData;
    len = p->len;
 
    if (NULL == env || p->len <= 0 || NULL == pInputStream)
        return 0;
 
    cls = (*env)->GetObjectClass(env, *pInputStream);
    mid = (*env)->GetMethodID(env, cls, "read", "([BII)I");
    tmp = (*env)->NewByteArray(env, bufLen);
    bufLen = (*env)->CallIntMethod(env, *pInputStream, mid, tmp, 0, bufLen);
 
    (*env)->DeleteLocalRef(env, cls);
 
    if (-1 == bufLen)
        return -1;
 
    pNativeBuf = (*env)->GetByteArrayElements(env, tmp, NULL);
    memcpy(buf, pNativeBuf, bufLen);
    (*env)->ReleaseByteArrayElements(env, tmp, pNativeBuf, 0);
    (*env)->DeleteLocalRef(env, tmp);
 
    return bufLen;
}
 
 

Ch22:用Java代碼處理本地對象的事件

當您需要使用以其他語言編寫的對象時,本地事件源和 Java? 偵聽器之間的通信可能需要一些小技巧 —— 尤其是在多線程環境中。本文通過使用一種透明處理從本地代碼到 JVM 的事件通信的設計模式,幫助您有效地處理傳統的本地庫。 

   在面向對象系統中,對象可以觸發一組事件。Java 編程語言爲定義基於觀察者設計模式(Observer design pattern)的事件偵聽器提供了支持,但當您需要使用以其他語言編寫的對象時,這還不夠。使用 Java Native Interface (JNI) 在本地事件源和 Java 偵聽器之間進行通信,可能需要一些技巧,尤其是有多線程環境中。在本文中,我們描述了一種透明處理從本地代碼到 JVM 的事件通信的設計模式。您可以使用這種設計來提供到遺留本地應用程序的 Java 接口,或者構建帶 Java 偵聽器的本地應用程序。

1.觀察者設計模式

觀察者設計模式定義了事件偵聽器與事件創建者之間的多對一依賴關係。當事件創建者觸發一個事件時,其所有偵聽器接收到該事件的通知。由於事件創建者和偵 聽器是無關的,您可以單獨使用或修改它們。這種設計模式是事件驅動編程的核心,被廣泛用於 GUI 框架,比如 Swing 和 SWT。
 
如果整個應用程序都使用 Java 編程語言編寫,實現觀察者設計模式相當簡單。圖 1 中的類圖給出了一個例子: 

圖 1. 觀察者設計模式

 

Java 本地應用程序偵聽器
 
不過,在某些情況下,我們想要讓本地應用程序支持 Java 偵聽器。大量應用程序(包括應用程序入口點)可能以本地代碼編寫,而應用程序以與其用戶界面交互的方式生成事件。在這種情形下,支持基於Java 用戶界面的最佳方式是讓 Java 類將自身註冊爲應用程序生成的各種事件的偵聽器。簡而言之,通過支持以 Java 語言編寫的偵聽器,可以獲得支持 Java 的
 本地應用程序。
 
圖 2 所示的類圖給出了一個示例場景。CEventSource 類用 C++ 語言編寫。它使用 addMouseDownListener() 和 removeMouseDownListener() 讓偵聽器註冊和取消註冊其 “鼠標按下” 事件。它想要所有偵聽器實現 IMouseDownListener 接口。

圖 2. 示例場景的類圖

 

   注意,IMouseDownListener 是一個 C++ 抽象類。那麼,Java 類如何註冊事件纔不會引入 Java 偵聽器和 CEventSource 類之間的編譯時綁定呢?在其註冊事件後,CEventSource 類如何調用 Java 類中的方法呢?這正是 Java Invocation API 的用武之地。

2.Java Invocation API

 Invocation API 讓您可以將 JVM 加載到一個本地應用程序中,而不必顯式地鏈接 JVM 源。 通過在 jvm.dll 中調用一個函數,可以創建一個 JVM,jvm.dll 還將當前本地線程連接到 JVM。然後,您可以在 JVM 中從本地線程調用所有 Java 方法。

然而,Invocation API 無法徹底解決問題。您不希望 CEventSource 類具有與 Java 偵聽器的編譯時依賴關係。另外,Java 偵聽器不應該承擔使用 JNI 來註冊帶 CEventSource 的偵聽器的責任。
 
 

3.代理設計模式

 
通過使用代理設計模式(Proxy design pattern),可以避免這一弊端。通常來說,代理是另一個對象的佔位符。客戶端對象可以處理代理對象,而代理封裝了所有使用本地方法的細節。代理模式的細節顯示在圖 3 中:
 
圖 3. 代理設計模式示例

 
 
   EventSourceProxy 是 CEventSource 類的代理。要將其自身註冊爲一個偵聽器,客戶端應該實現 IJMouseDownListener 接口。該接口類似於 IMouseDownListener,但它是用 Java 代碼編寫的。當第一個客戶端調用其 addMouseDownListener() 方法時,它使用 registerListener() 本地方法來將其自身註冊爲一個帶 CEventSource 的偵聽器。registerListener() 方法用 C++ 語言來實現。它創建一個 JMouseDownListener 對象,並將其註冊爲一個帶 CEventSource 的偵聽器。當 JMouseDownListener 的 onMouseDown 事件被觸發時,JMouseDownListener 中的 onMouseDownListener() 方法使用 Invocation API 來通知 EventSourceProxy。

   EventSourceProxy 還維持一組使用它來註冊的偵聽器。無論其 onMouseDown 何時被觸發,它將通知該組中的所有偵聽器。注意,即使針對該事件存在多個 Java 偵聽器,只有代理的一個實例被註冊爲具有 CEventSource。代理將 onMouseDown 事件委託給它的所有偵聽器。這防止了本地代碼和 Java 代碼之間不必要的上下文切換。

4.多線程問題

 

  本地方法接收 JNI 接口指針作爲一個參數。但是,一個想要將事件委託回其關聯 Java 代理的本地偵聽器沒有現成的 JNI 接口指針。一旦獲得 JNI 接口指針,應該將其保存起來以便後續使用。 

  JNI 接口指針只在當前線程中有效。實現 JNI 的 JVM 可以在 JNI 接口指針指向的區域中分配和存儲本地線程數據。這意味着您也需要以本地線程數據保存 JNI 接口指針。

  JNI 接口指針可以兩種方式獲得:

  • 一旦線程使用 JNI_CreateJavaVM 創建了 JVM,JNI 將接口指針值放在由第二個參數指定的位置。然後該值可以保存在本地線程區域中。
  • 如果 JVM 已由進程中某個其他線程創建,當前線程可以調用 AttachCurrentThread。JNI 將接口指針值放在由第一個參數指定的位置。
  但是這還沒有完。需要記住的是,程序是以 C/C++ 語言編寫的,因此,無法使用自動垃圾回收,因爲程序不是使用 Java 語言編寫的。一旦線程完成 JNI 調用,它需要通過調用 DetachCurrentThread
 來釋放接口指針。如果未做此調用並且線程存在,進程將無法正常終止。相反,它將一直等待現在已不存在的線程以 DestroyJavaVM 調用的方式從 JVM 中離開。

5.環境設置

 

  公共接口

   IMouseDownListener 和 IEventSource 接口定義在 common.h 中。IMouseDownListener 只有一個方法:onMouseDown()。該方法接收鼠標單擊的屏幕位置。IEventSource 接口包含了 addMouseDownListener() 和 removeMouseDownListener() 方法,用於註冊和取消註冊偵聽器。

  Java Invocation API 的幫助例程

  有 7 個必需的常用工具方法可用於簡化 Java Invocation API 的使用,它們定義在 Jvm.h 中,在 Jvm.cpp 中實現:

  • CreateJavaObject() 創建一個 Java 對象,給出其類名和針對本地 IEventSource 的句柄。這個創建好的 Java 對象將用於偵聽來自該本地句柄的事件。該句柄通過其構造方法傳遞給對象。
  • ReleaseJObject() 調用 Java 對象的 release() 方法。該方法用於從 EventSourceProxy 取消註冊對象的偵聽器。
  • DetachThread() 從 JVM 分離當前線程(如果當前線程連接到 JVM)。當線程正被連接時,該調用對於釋放特定於線程的、已分配給 JNI 的資源是很必要的。

  其餘方法都自己附帶有解釋:

  • CreateJVM()
  • DestroyJVM()
  • GetJVM()
  • GetJNIEnv()
CJvm 類還在特定於線程的位置保存一個 JNI 環境指針。這是很有必要的,因爲 JNI 環境指針是線程相關的。

  本地事件源

  在 EventSource.h 和 EventSource.cpp 中,CEventSource 是一個簡單而直觀的 IEventSource 接口的實現。 

  本地事件偵聽器

  在 MouseDownListener.h 和 MouseDownListener.cpp 中,CMouseDownListener 是 IMouseDownListener 接口的實現。該本地偵聽器僅出於解釋目的而編寫。

  入口點

  main.cpp 包含 main() 和 ThreadMain()。 main() 創建一個本地 EventSource、一個本地偵聽器和一個 Java 偵聽器。然後創建線程,並在睡眠幾秒後讓它執行。最後,它釋放 Java 偵聽器並銷燬 JVM。

  ThreadMain() 簡單地觸發一個事件,然後將自身從 JVM 分離出來。
 

Java 模塊

  IJMouseDownListener.java 中的 IJMouseDownListener 只是本地接口針對 Java 平臺的一個克隆。

   MouseDownListener 是 Java 中的一個示例偵聽器,在 MouseDownListener.java 中實現。它在其構造方法中接收本地 EventSource 句柄。它定義了一個 release() 方法,該方法取消註冊帶 EventSourceProxy 的偵聽器。

  EventSourceProxy 是一個用於來自本地模塊的 EventSource 的佔位符或代理項。它在 EventSourceProxy.java 中實現。它維持一個靜態哈希表,以將一個代理映射到實際 EventSource。

   addMouseDownListener() 和 removeMouseDownListener() 允許您維持一個 Java 偵聽器集合。單個本地 EventSource 可以有多個 Java 偵聽器,但只有在必要時代理才註冊/取消註冊本地 EventSource。

  當從本地 EventSource 轉發事件時,EventSourceProxy 的本地實現調用 fireMouseDownEvent()。該方法迭代 Java 偵聽器的哈希集合,並通知它們。

  EventSourceProxy 的本地部分還維持一個到自身的全局引用。這對於稍後調用 fireMouseDownEvent() 是很必要的。

  構建並執行示例代碼

  示例代碼中的所有 Java 類都使用普通過程構建,無需特殊步驟。對於 EventSourceProxy 的本地實現,您需要使用 javah 生成頭文件:

javah -classpath .\java\bin events.EventSourceProxy


   爲了構建針對 Win32 平臺的 C++ 模塊,我們提供了 Microsoft Developer Studio 項目文件和 cpp.dsw 工作區。您可以打開工作區,簡單地構建 main 項目。工作區中的所有項目都以適當的依賴關係相關聯。確保您的 Developer Studio 可以找到 JNI 頭和編譯時 JNI 庫。可以通過選擇 Tools > Options > Directories 菜單項完成這一工作。

  構建成功之後,在可以執行示例程序之前,還需要完成幾個步驟。

  首先,因爲用於構建 Java 類幷包含 JNI 頭和庫的 JDK 可能有針對 Java Invocation API 的運行時組件,例如 jvm.dll,您必需設置它。最簡單的方法是更新 PATH 變量。

  其次,main 程序帶有命令行參數,這些參數是簡單的 JVM 參數。您需要至少傳遞兩個參數給 JVM:

 

main.exe "-Djava.class.path=.\\java\\bin" 

"-Djava.library.path=.\\cpp\\listener\\Debug"


  得到的控制檯輸出如下:

 

In CMouseDownListener::onMouseDown

        X = 50

        Y = 100

In MouseDownListener.onMouseDown

        X = 50

        Y = 100


  正如您從控制檯輸出所看到的,Java 偵聽器產生與出於解釋目的而構建的本地偵聽器相同的結果。 

  結束語 

   本文展示瞭如何爲本地應用程序生成的事件註冊一個 Java 類作爲偵聽器。通過使用觀察者設計模式,您已經減少了事件源與偵聽器之間的耦合。您還通過使用代理設計模式隱藏了來自 Java 偵聽器的事件源的實現細節。您可以使用該設計模式組合來將一個 Java UI 添加到現有的本地應用程序。

 
 
 

Ch23: JNI and Thread

The Java virtual machine supports multiple threads of control concurrently executing in the same address space. This concurrency introduces a degree of complexity that you do not have in a single-threaded environment. Multiple threads may access the same objects, the same file descriptors--in short, the same shared resources--at the same time.

To get the most out of this section, you should be familiar with the concepts of multithreaded programming. You should know how to write Java applications that utilize multiple threads and how to synchronize access of shared resources. A good reference on multithreaded programming in the Java programming language isConcurrent Programming in Java TM, Design Principles and Patterns , by Doug Lea (Addison-Wesley, 1997).

1.1 Constraints

There are certain constraints that you must keep in mind when writing native methods that are to run in a multithreaded environment. By understanding and programming within these constraints, your native methods will execute safely no matter how many threads simultaneously execute a given native method. For example:

  • 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 differentJNIEnv 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.

1.2 Monitor Entry and Exit

Monitors are the primitive synchronization mechanism on the Java platform. Each object can be dynamically associated with a monitor. The JNI allows you to synchronize using these monitors, thus implementing the functionality equivalent to a synchronized block in the Java programming language:

 synchronized (obj) {
     ...                   // synchronized block
 }

The Java virtual machine guarantees that a thread acquires the monitor associated with the object obj before it executes any statements in the block. This ensures that there can be at most one thread that holds the monitor and executes inside the synchronized block at any given time. A thread blocks when it waits for another thread to exit a monitor.

Native code can use JNI functions to perform equivalent synchronization on JNI references. You can use theMonitorEnter function to enter the monitor and the MonitorExit function to exit the monitor:

 if ((*env)->MonitorEnter(env, obj) != JNI_OK) {
     ... /* error handling */
 }
 ...     /* synchronized block */
 if ((*env)->MonitorExit(env, obj) != JNI_OK) {
     ... /* error handling */
 };

Executing the code above, a thread must first enter the monitor associated with obj before executing any code inside the synchronized block. The Monitor-Enter operation takes a jobject as an argument and blocks if another thread has already entered the monitor associated with the jobject . Calling MonitorExitwhen the current thread does not own the monitor results in an error and causes anIllegal-MonitorStateException to be raised. The above code contains a matched pair of MonitorEnterand MonitorExit calls, yet we still need to check for possible errors. Monitor operations may fail if, for example, the underlying thread implementation cannot allocate the resources necessary to perform the monitor operation.

MonitorEnter and MonitorExit work on jclass , jstring , and jarray types, which are special kinds ofjobject references.

Remember to match a MonitorEnter call with the appropriate number of MonitorExit calls, especially in code that handles errors and exceptions:

 if ((*env)->MonitorEnter(env, obj) != JNI_OK) ...;
 ...
 if ((*env)->ExceptionOccurred(env)) {
     ... /* exception handling */
     /* remember to call MonitorExit here */
     if ((*env)->MonitorExit(env, obj) != JNI_OK) ...;
 }
 ... /* Normal execution path.
 if ((*env)->MonitorExit(env, obj) != JNI_OK) ...;

Failure to call MonitorExit will most likely lead to deadlocks. By comparing the above C code segment with the code segment at the beginning of this section, you can appreciate how much easier it is to program with the Java programming language than with the JNI. Thus, it is preferable to express synchronization constructs in the Java programming language. If, for example, a static native method needs to enter the monitor associated with its defining class, you should define a static synchronized native method as opposed to performing JNI-level monitor synchronization in native code.

1.3 Monitor Wait and Notify

The Java API contains several other methods that are useful for thread synchronization. They areObject.wait , Object.notify , and Object.notifyAll . No JNI functions are supplied that correspond directly to these methods because monitor wait and notify operations are not as performance critical as monitor enter and exit operations. Native code may instead use the JNI method call mechanism to invoke the corresponding methods in the Java API:

 /* precomputed method IDs */
 static jmethodID MID_Object_wait;
 static jmethodID MID_Object_notify;
 static jmethodID MID_Object_notifyAll;
 
 void
 JNU_MonitorWait(JNIEnv *env, jobject object, jlong timeout)
 {
     (*env)->CallVoidMethod(env, object, MID_Object_wait,
                            timeout);
 }
 
 void
 JNU_MonitorNotify(JNIEnv *env, jobject object)
 {
     (*env)->CallVoidMethod(env, object, MID_Object_notify);
 }
 
 void
 JNU_MonitorNotifyAll(JNIEnv *env, jobject object)
 {
     (*env)->CallVoidMethod(env, object, MID_Object_notifyAll);
 }

We assume that the method IDs for Object.wait , Object.notify , and Object.notifyAll have been calculated elsewhere and are cached in the global variables. Like in the Java programming language, you can call the above monitor-related functions only when holding the monitor associated with the jobject argument.

1.4 Obtaining a JNIEnv Pointer in Arbitrary Contexts

We explained earlier that a JNIEnv pointer is only valid in its associated thread. This is generally not a problem for native methods because they receive the JNIEnv pointer from the virtual machine as the first argument. Occasionally, however, it may be necessary for a piece of native code not called directly from the virtual machine to obtain the JNIEnv interface pointer that belongs to the current thread. For example, the piece of native code may belong to a "callback" function called by the operating system, in which case the JNIEnvpointer will probably not be available as an argument.

You can obtain the JNIEnv pointer for the current thread by calling the AttachCurrentThread function of the invocation interface:

 JavaVM *jvm
; /* already set */
 
 f()
 {
     JNIEnv *env;
     (*jvm
)->AttachCurrentThread(jvm
, (void **)&env, NULL);
     ... /* use env */
 }

When the current thread is already attached to the virtual machine, Attach-Current-Thread returns theJNIEnv interface pointer that belongs to the current thread.

There are many ways to obtain the JavaVM pointer: by recording it when the virtual machine is created, by querying for the created virtual machines using JNI_GetCreatedJavaVMs , by calling the JNI functionGetJavaVM inside a regular native method, or by defining a JNI_OnLoad handler. Unlike the JNIEnv pointer, the JavaVM pointer remains valid across multiple threads so it can be cached in a global variable.

Java 2 SDK release 1.2 provides a new invocation interface function GetEnvso that you can check whether the current thread is attached to the virtual machine, and, if so, to return theJNIEnv pointer that belongs to the current thread. GetEnvand AttachCurrentThread are functionally equivalent if the current thread is already attached to the virtual machine.

1.5 Matching the Thread Models

Suppose that native code to be run in multiple threads accesses a global resource. Should the native code use JNI functions MonitorEnter and MonitorExit , or use the native thread synchronization primitives in the host environment (such as mutex_lock on Solaris)? Similarly, if the native code needs to create a new thread, should it create a java.lang.Thread object and perform a callback of Thread.start through the JNI, or should it use the native thread creation primitive in the host environment (such as thr_create on Solaris)?

The answer is that all of these approaches work if the Java virtual machine implementation supports a thread model that matches that used by the native code. The thread model dictates how the system implements essential thread operations such as scheduling, context switching, synchronization, and blocking in system calls. In a native thread model the operating system manages all the essential thread operations. In a user thread model, on the other hand, the application code implements the thread operations. For example, the "Green thread" model shipped with JDK and Java 2 SDK releases on Solaris uses the ANSI C functionssetjmp and longjmp to implement context switches.

Many modern operating systems (such as Solaris and Win32) support a native thread model. Unfortunately, some operating systems still lack native thread support. Instead, there may be one or many user thread packages on these operating systems.

If you write application strictly in the Java programming language, you need not worry about the underlying thread model of the virtual machine implementation. The Java platform can be ported to any host environment that supports the required set of thread primitives. Most native and user thread packages provide the necessary thread primitives for implementing a Java virtual machine.

JNI programmers, on the other hand, must pay attention to thread models. The application using native code may not function properly if the Java virtual implementation and the native code have a different notion of threading and synchronization. For example, a native method could be blocked in a synchronization operation in its own thread model, but the Java virtual machine, running in a different thread model, may not be aware that the thread executing the native method is blocked. The application deadlocks because no other threads will be scheduled.

The thread models match if the native code uses the same thread model as the Java virtual machine implementation. If the Java virtual machine implementation uses native thread support, the native code can freely invoke thread-related primitives in the host environment. If the Java virtual machine implementation is based on a user thread package, the native code should either link with the same user thread package or rely on no thread operations at all. The latter may be harder to achieve than you think: most C library calls (such as I/O and memory allocation functions) perform thread synchronization underneath. Unless the native code performs pure computation and makes no library calls, it is likely to use thread primitives indirectly.

Most virtual machine implementations support only a particular thread model for JNI native code. Implementations that support native threads are the most flexible, hence native threads, when available, are typically preferred on a given host environment. Virtual machine implementations that rely on a particular user thread package may be severely limited as to the type of native code with which they can operate.

Some virtual machine implementations may support a number of different thread models. A more flexible type of virtual machine implementation may even allow you to provide a custom thread model implementation for virtual machine's internal use, thus ensuring that the virtual machine implementation can work with your native code. Before embarking on a project likely to require native code, you should consult the documentation that comes with your virtual machine implementation for thread model limitations.

 

8.4 Load and Unload Handlers

Load and unload handlers allow the native library to export two functions: one to be called whenSystem.loadLibrary loads the native library, the other to be called when the virtual machine unloads the native library. This feature was added in Java 2 SDK release 1.2.

8.4.1 The JNI_OnLoad Handler

When System.loadLibrary loads a native library, the virtual machine searches for the following exported entry in the native library:

 JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm
, void *reserved);

You can invoke any JNI functions in an implementation of JNI_Onload . A typical use of the JNI_OnLoadhandler is caching the JavaVM pointer, class references, or method and field IDs, as shown in the following example:

 JavaVM *cached_jvm
;
 jclass Class_C;
 jmethodID MID_C_g;
 JNIEXPORT jint JNICALL
 JNI_OnLoad(JavaVM *jvm
, void *reserved)
 {
     JNIEnv *env;
     jclass cls;
     cached_jvm
 = jvm
;  /* cache the JavaVM pointer */
 
     if ((*jvm
)->GetEnv
(jvm
, (void **)&env, JNI_VERSION_1_2)) {
         return JNI_ERR; /* JNI version not supported */
     }
     cls = (*env)->FindClass(env, "C");
     if (cls == NULL) {
         return JNI_ERR;
     }
     /* Use weak global ref to allow C class to be unloaded */
     Class_C = (*env)->NewWeakGlobalRef(env, cls);
     if (Class_C == NULL) {
         return JNI_ERR;
     }
     /* Compute and cache the method ID */
     MID_C_g = (*env)->GetMethodID(env, cls, "g", "()V");
     if (MID_C_g == NULL) {
         return JNI_ERR;
     }
     return JNI_VERSION_1_2;
 }

The JNI_OnLoad function first caches the JavaVM pointer in the global variable cached_jvm. It then obtains the JNIEnv pointer by calling GetEnv. It finally loads the C class, caches the class reference, and computes the method ID for C.g . TheJNI_OnLoad function returns JNI_ERR (§12.4 ) on error and otherwise returns the JNIEnv versionJNI_VERSION_1_2 needed by the native library.

We will explain in the next section why we cache the C class in a weak global reference instead of a global reference.

Given a cached JavaVM interface pointer it is trivial to implement a utility function that allows the native code to obtain the JNIEnv interface pointer for the current thread (§8.1.4 ) :

 JNIEnv *JNU_GetEnv
()
 {
     JNIEnv *env;
     (*cached_jvm
)->GetEnv
(cached_jvm
,
                           (void **)&env,
                           JNI_VERSION_1_2);
     return env;
 }

8.4.2 The JNI_OnUnload Handler

Intuitively, the virtual machine calls the JNI_OnUnload handler when it unloads a JNI native library. This is not precise enough, however. When does the virtual machine determine that it can unload a native library? Which thread runs the JNI_OnUnload handler?

The rules of unloading native libraries are as follows:

  • The virtual machine associates each native library with the class loader L of the class C that issues theSystem.loadLibrary call.
  • The virtual machine calls the JNI_OnUnload handler and unloads the native library after it determines that the class loader L is no longer a live object. Because a class loader refers to all the classes it defines, this implies that C can be unloaded as well.
  • The JNI_OnUnload handler runs in a finalizer, and is either invoked synchroniously byjava.lang.System.runFinalization or invoked asynchronously by the virtual machine.

Here is the definition of a JNI_OnUnload handler that cleans up the resources allocated by the JNI_OnLoadhandler in the last section:

 JNIEXPORT void JNICALL 
 JNI_OnUnload(JavaVM *jvm
, void *reserved)
 {
     JNIEnv *env;
     if ((*jvm
)->GetEnv
(jvm
, (void **)&env, JNI_VERSION_1_2)) {
         return;
     }
     (*env)->DeleteWeakGlobalRef(env, Class_C);
     return;
 }

The JNI_OnUnload function deletes the weak global reference to the C class created in the JNI_OnLoadhandler. We need not delete the method ID MID_C_g because the virtual machine automatically reclaims the resources needed to represent C 's method IDs when unloading its defining class C .

We are now ready to explain why we cache the C class in a weak global reference instead of a global reference. A global reference would keep C alive, which in turn would keep C 's class loader alive. Given that the native library is associated with C 's class loader L , the native library would not be unloaded andJNI_OnUnload would not be called.

The JNI_OnUnload handler runs in a finalizer. In contrast, the JNI_OnLoad handler runs in the thread that initiates the System.loadLibrary call. Because JNI_OnUnload runs in an unknown thread context, to avoid possible deadlocks, you should avoid complex synchronization and locking operations in JNI_OnUnload . TheJNI_OnUnload handler typically carries out simple tasks such as releasing the resources allocated by the native library.

The JNI_OnUnload handler runs when the class loader that loaded the library and all classes defined by that class loader are no longer alive. The JNI_OnUnload handler must not use these classes in any way. In the above JNI_OnUnload definition, you must not perform any operations that assume Class_C still refers to a valid class. The DeleteWeakGlobalRef call in the example frees the memory for the weak global reference itself, but does not manipulate the referred class C in any way.

In summary, you should be careful when writing JNI_OnUnload handlers. Avoid complex locking operations that may introduce deadlocks. Keep in mind that classes have been unloaded when the JNI_OnUnloadhandler is invoked.

8.5 Reflection Support

Reflection generally refers to manipulating language-level constructs at runtime. For example, reflection allows you to discover at run time the name of arbitrary class objects and the set of fields and methods defined in the class. Reflection support is provided at the Java programming language level through thejava.lang.reflect package as well as some methods in the java.lang.Object and java.lang.Classclasses. Although you can always call the corresponding Java API to carry out reflective operations, the JNI provides the following functions to make the frequent reflective operations from native code more efficient and convenient:

  • GetSuperclass returns the superclass of a given class reference.
  • IsAssignableFrom checks whether instances of one class can be used when instances of another class are expected.
  • GetObjectClass returns the class of a given jobject reference.
  • IsInstanceOf checks whether a jobject reference is an instance of a given class.
  • FromReflectedField and ToReflectedField allow the native code to convert between field IDs andjava.lang.reflect.Field objects. They are new additions in Java 2 SDK release 1.2.
  • FromReflectedMethod and ToReflectedMethod allow the native code to convert between method IDs,java.lang.reflect.Method objects and java.lang.reflect.Constructor  objects. They are new additions in Java 2 SDK release 1.2.

Ch24:測量Java應用程序的CPU和內存佔用率

   測量CPU和內存的佔用率常常是檢查Java 應用程序是否達到特定性能的一個重要環節。儘管Java 提 供了一些重要的方法用於測量其堆棧大小,但是使用標準的API是無法測量本機Java進程的大小和 CPU當前的使用率的。這種測量的結果對於開發人員來說非常重要,它會提供應用程序的實時性能和效率信息。不幸的是,這樣的信息只能從操作系統直接獲取, 而這已經超出了Java標準的可移植能力。

    一個主要的解決方案是使用操作系統自帶的本機系統調用,將數據通過JNI(Java Native Interface,Java本機接口)傳輸給Java.與調用各個平臺專用的外部命令(比如ps)並分析輸出結果不同,這種方案始終是一種很可靠的方 式。以前碰到這樣的問題時,我嘗試過使用Vladimir Roubtsov自己編寫的一個很小的庫,它只能在Win32系統下測量進程的CPU佔用率。但是,這個庫的能力十分有限,所以我需要某種方式能夠同時在Windows 和Solaris平臺上測量CPU和內存的佔用率。

    我擴展了這個庫的能力,在Windows 和Solaris 8平臺上實現了所有功能。新的庫能夠測量純CPU使用時間、CPU使用的百分比、本機剩餘內存和已經使用的內存、Java進程的本機內存大小、系統信息 (比如操作系統的名字、補丁程序、硬件信息等)。它由三部分實現: Java通用的部分、Windows實現,以及Solaris實現。依靠操作系統的部分用純C語言實現。

    編輯提示:本文可以下載 ,所有的源代碼都以單獨的文本方式列出來了。

    所以,我們將創建一個簡單的JNI庫,用於同C層裏的操作系統進行溝通,並把生成的數據提供給Java應用程序。首先,我們要創建一個 SystemInformation類(列表A),爲測量和記錄CPU的使用率和其他與系統相關的信息提供一個簡單的API.這個類是抽象的,因爲它公開 的是一個完全靜態的API.

 

列表A

<PRE>package com.vladium.utils;

public abstract class SystemInformation
{
// public: ................................................................

/**
* A simple class to represent data snapshots taken by {@link #makeCPUUsageSnapshot}.
*/
public static final class CPUUsageSnapshot

public final long m_time, m_CPUTime;

// constructor is private to ensure that makeCPUUsageSnapshot()
// is used as the factory method for this class:
private CPUUsageSnapshot (final long time, final long CPUTime)
{
m_time = time;
m_CPUTime = CPUTime;
}

} // end of nested class

// Custom exception class for throwing
public static final class NegativeCPUTime extends Exception {
}

/**
* Minimum time difference [in milliseconds] enforced for the inputs into
* {@link #getProcessCPUUsage(SystemInformation.CPUUsageSnapshot,SystemInformation.CPUUsageSnapshot)}.
* The motivation for this restriction is the fact that <CODE>System.currentTimeMillis()</CODE>
* on some systems has a low resolution (e.g., 10ms on win32). The current value
* is 100 ms.
*/
public static final int MIN_ELAPSED_TIME = 100;


/**
* Creates a CPU usage data snapshot by associating CPU time used with system
* time. The resulting data can be fed into
* {@link #getProcessCPUUsage(SystemInformation.CPUUsageSnapshot,SystemInformation.CPUUsageSnapshot)}.
*/
public static CPUUsageSnapshot makeCPUUsageSnapshot() throws SystemInformation.NegativeCPUTime
{
long prCPUTime = getProcessCPUTime ();
if (prCPUTime&lt;0) throw new NegativeCPUTime();
return new CPUUsageSnapshot (System.currentTimeMillis (), getProcessCPUTime ());
}

/**
* Computes CPU usage (fraction of 1.0) between <CODE>start.m_CPUTime</CODE> and
* <CODE>end.m_CPUTime</CODE> time points [1.0 corresponds to 100% utilization of
* all processors].
*
* @throws IllegalArgumentException if start and end time points are less than
* {@link #MIN_ELAPSED_TIME} ms apart.
* @throws IllegalArgumentException if either argument is null;
*/
public static double getProcessCPUUsage (final CPUUsageSnapshot start, final CPUUsageSnapshot end)
{
if (start == null) throw new IllegalArgumentException ("null input: start");
if (end == null) throw new IllegalArgumentException ("null input: end");
if (end.m_time &lt; start.m_time + MIN_ELAPSED_TIME)
throw new IllegalArgumentException ("end time must be at least " + MIN_ELAPSED_TIME + " ms later than start time");

return ((double)(end.m_CPUTime - start.m_CPUTime)) / (end.m_time - start.m_time);
}

/**
* Returns the PID of the current process. The result is useful when you need
* to integrate a Java app with external tools.
*/
public static native int getProcessID ();

/**
* Returns the number of processors on machine
*/
public static native int getCPUs ();

/**
* Returns CPU (kernel + user) time used by the current process [in milliseconds].
* The returned value is adjusted for the number of processors in the system.
*/
public static native long getProcessCPUTime ();

/**
* Returns CPU (kernel + user) time used by the current process [in perecents].
* The returned value is either CPU percentage, or zero if this is not supported by OS.
* Currently it is supported by Solaris8, and not supported by Windows XP
*/
public static native double getProcessCPUPercentage();

/**
* Returns maximum memory available in the system.
*/
public static native long getMaxMem ();

/**
* Returns current free memory in the system.
*/
public static native long getFreeMem ();

/**
* Returns system name info like "uname" command output
*/
public static native String getSysInfo ();

/**
* Returns CPU usage (fraction of 1.0) so far by the current process. This is a total
* for all processors since the process creation time.
*/
public static native double getProcessCPUUsage ();

/**
* Returns current space allocated for the process, in Kbytes. Those pages may or may not be in memory.
*/
public static native long getMemoryUsage();

/**
* Returns current process space being resident in memory, in Kbytes.
*/
public static native long getMemoryResident();

/**
* Sets the system native process PID for which all measurements will be done.
* If this method is not called then the current JVM pid will act as a default.
* Returns the native-dependent error code, or 0 in case of success.
*/
public static native int setPid(int pid);

/**
* Closes native-dependent process handle, if necessary.
*/
public static native int detachProcess();

// protected: .............................................................

// package: ...............................................................

// private: ...............................................................


private SystemInformation () {} // prevent subclassing

private static final String SILIB = "silib";

static
{
// loading a native lib in a static initializer ensures that it is
// available done before any method in this class is called:
try
{
System.loadLibrary (SILIB);
}
catch (UnsatisfiedLinkError e)
{
System.out.println ("native lib '" + SILIB + "' not found in 'java.library.path': " + System.getProperty ("java.library.path"));

throw e; // re-throw
}
}

} // end of class
</PRE>

 

   最重要的方法是getProcessCPUTime(),它會返回當前進程的CPU時間(內核和用戶)的毫秒數,或者是PID在初始化期間被傳送給本機庫 的進程所消耗的CPU時間。返回的值應該根據系統的處理器的個數進行調整;下面我們來看看這是如何在本機代碼裏做到的。我們用修改符native來聲明這 個方法,這意味着它必須在JNI代碼裏實現。getProcessCPUTime()方法用來給CPU的使用率數據進行快照,方式是將經過測量的CPU時 間與當前的系統時間進行關聯。這樣的數據庫快照在makeCPUUsageSnapshot()方法裏進行,並輸出一個CPUUsageSnapshot 容器對象。這樣,測量CPU使用率的原理就很容易理解了:我們按照給定的時間間隔進行兩次CPU快照,按1.0的分數來計算兩個時間點之間的CPU使用 率,方式是兩點所在的CPU時間差除以兩點所在系統時間差。下面就是getProcessCPUUsage()方法的工作原理:

public static double getProcessCPUUsage (final CPUUsageSnapshot start, final CPUUsageSnapshot end)
    {
        if (start == null) throw new IllegalArgumentException ("null input: start");
        if (end == null) throw new IllegalArgumentException ("null input: end");
        if (end.m_time < start.m_time + MIN_ELAPSED_TIME)
            throw new IllegalArgumentException ("end time must be at least " + MIN_ELAPSED_TIME + " ms later than start time");
        
        return ((double)(end.m_CPUTime - start.m_CPUTime)) / (end.m_time - start.m_time);
    }

    只要我們知道分母裏的快照之間的時間間隔,以及分子裏進程花在活動狀態上的CPU時間,我們就會得到所測量的時間間隔過程中進程的CPU使用率;1.0就代表所有處理器100%的使用率。

    事實上這種方式可以用在所有版本的UNIX的ps工具和Windows的任務管理器上,這兩個都是用於監視特定進程的CPU使用率的程序。很顯然,時間間 隔越長,我們得到的結果就越平均、越不準確。但是最小時差應該被強制輸入getProcessCPUUsage()。這種限制的原因是某些系統上的 System.currentTimeMillis()的解析度很低。Solaris 8操作系統提供了一個系統調用,用於從內核表裏直接獲得CPU使用率。出於這個目的,我們擁有的getProcessCPUPercentage()方法 會以百分比的形式返回進程所使用的CPU時間。如果這個特性不被操作系統支持(比如在Windows下),那麼JNI庫就會根據我們應用程序的設計返回一 個負值。

    還有其他一些本機聲明要在本機部分實現:

    getCPUs()用來返回機器上處理器的個數
    getMaxMem()用來返回系統上可用的最大物理內存
    getFreeMem()用來返回系統上當前可用內存
    getSysInfo()用來返回系統信息,包括一些硬件和操作系統的詳細信息
    getMemoryUsage()用來返回分配給進程的空間,以KB爲單位(這些頁面文件可能在內存裏,也有可能不在內存裏)
    getMemoryResident()用來返回當前進程駐留在內存裏的空間,以KB爲單位。

    所有這些方法對於不同的Java開發人員來說常常都是非常有用的。爲了確保本機JNI庫被調入內存並在調用任何本機方法之前被初始化,這個庫被加載到一個靜態初始值裏:

static
    {
        try
        {
            System.loadLibrary (SILIB);
        }
        catch (UnsatisfiedLinkError e)
        {
            System.out.println ("native lib '" + SILIB + "' not found in 'Java.library.path': " + System.getProperty ("Java.library.path"));
            
            throw e; // re-throw
        }
    }

    在初始化一個.dll(或者.so)庫之後,我們可以直接使用本機聲明的方法:

final SystemInformation.CPUUsageSnapshot m_prevSnapshot =
SystemInformation.makeCPUUsageSnapshot ();
Thread.sleep(1000);
final SystemInformation.CPUUsageSnapshot event = 
SystemInformation.makeCPUUsageSnapshot ();
final long memorySize = SystemInformation.getMemoryUsage();
final long residentSize = SystemInformation.getMemoryResident();
long freemem = SystemInformation.getFreeMem()/1024;
long maxmem = SystemInformation.getMaxMem()/1024;
double receivedCPUUsage = 100.0 * SystemInformation.getProcessCPUUsage (m_prevSnapshot, event);
System.out.println("Current CPU usage is "+receivedCPUUsage+"%”);

    現在讓我們來分別看看針對Windows和Solaris的JNI本機實現。C頭文件silib.h(列表B)能夠用JDK裏的Javah工具生成,或者手動編寫。

    列表B

/* DO NOT EDIT THIS FILE - it is machine generated */
#include
/* Header for class com_vladium_utils_SystemInformation */

#ifndef _Included_com_vladium_utils_SystemInformation
#define _Included_com_vladium_utils_SystemInformation
#ifdef __cplusplus
extern "C" {
#endif

/*
 * Class:     com_vladium_utils_SystemInformation
 * Method:    getProcessID
 * Signature: ()I
 */
JNIEXPORT jint
JNICALL Java_com_vladium_utils_SystemInformation_getProcessID (JNIEnv *, jclass);

/*
 * Class:     com_vladium_utils_SystemInformation
 * Method:    getCPUs
 * Signature: ()I
 */
JNIEXPORT jint
JNICALL Java_com_vladium_utils_SystemInformation_getCPUs (JNIEnv *, jclass);

/*
 * Class:     com_vladium_utils_SystemInformation
 * Method:    getProcessCPUTime
 * Signature: ()J
 */
JNIEXPORT jlong
JNICALL Java_com_vladium_utils_SystemInformation_getProcessCPUTime (JNIEnv *, jclass);

/*
 * Class:     com_vladium_utils_SystemInformation
 * Method:    getProcessCPUUsage
 * Signature: ()D
 */
JNIEXPORT jdouble
JNICALL Java_com_vladium_utils_SystemInformation_getProcessCPUUsage (JNIEnv *, jclass);

/*
 * Class:     com_vladium_utils_SystemInformation
 * Method:    getPagefileUsage
 * Signature: ()J
 */
JNIEXPORT jlong
JNICALL Java_com_vladium_utils_SystemInformation_getPagefileUsage (JNIEnv *, jclass);

#ifdef __cplusplus
}
#endif
#endif

 

 

   Windows

    首先我們來看看Windows的實現(列表C)。

    列表C

/* ------------------------------------------------------------------------- */
/*
 * An implementation of JNI methods in com.vladium.utils.SystemInformation
 * class. The author compiled it using Microsoft Visual C++ and GCC for Win32 but the code
 * should be easy to use with any compiler for win32 platform.
 *
 * For simplicity, this implementaion assumes JNI 1.2+ and omits error handling.
 *
 * Enhanced by Peter V. Mikhalenko (C) 2004, Deutsche Bank [[email protected]]
 * Original source (C) 2002, Vladimir Roubtsov [[email protected]]
 */
/* ------------------------------------------------------------------------- */

#include
#include
#include
#include
#include
#include

#include "com_vladium_utils_SystemInformation.h"

static jint s_PID;
static HANDLE s_currentProcess;
static int alreadyDetached;
static int s_numberOfProcessors;
static SYSTEM_INFO systemInfo;
static WORD processorArchitecture;
static DWORD pageSize;
static DWORD processorType;
static WORD processorLevel;
static WORD processorRevision;

#define INFO_BUFFER_SIZE 32768
#define BUFSIZE 2048


/* ------------------------------------------------------------------------- */

/*
 * A helper function for converting FILETIME to a LONGLONG [safe from memory
 * alignment point of view].
 */
static LONGLONG
fileTimeToInt64 (const FILETIME * time)
{
    ULARGE_INTEGER _time;

    _time.LowPart = time->dwLowDateTime;
    _time.HighPart = time->dwHighDateTime;

    return _time.QuadPart;
}
/* ......................................................................... */

/*
 * This method was added in JNI 1.2. It is executed once before any other
 * methods are called and is ostensibly for negotiating JNI spec versions, but
 * can also be conveniently used for initializing variables that will not
 * change throughout the lifetime of this process.
 */
JNIEXPORT jint JNICALL
JNI_OnLoad (JavaVM * vm, void * reserved)
{

    s_PID = _getpid ();
    s_currentProcess = GetCurrentProcess ();
    externalCPUmon = 0;
    alreadyDetached = 0;

    GetSystemInfo (& systemInfo);
    s_numberOfProcessors = systemInfo.dwNumberOfProcessors;
    processorArchitecture = systemInfo.wProcessorArchitecture;
    pageSize = systemInfo.dwPageSize;
    processorType = systemInfo.dwProcessorType;
    processorLevel = systemInfo.wProcessorLevel;
    processorRevision = systemInfo.wProcessorRevision;

    return JNI_VERSION_1_2;
}
/* ......................................................................... */

JNIEXPORT void JNICALL
JNI_OnUnload (JavaVM * vm, void * reserved)
{

    if (!alreadyDetached && s_currentProcess!=NULL) {
     CloseHandle(s_currentProcess);
     printf("[JNI Unload] Detached from native process.
");
     fflush(stdout);
    }

}
/* ......................................................................... */

/*
 * Class:     com_vladium_utils_SystemInformation
 * Method:    getCPUs
 * Signature: ()I
 */
JNIEXPORT jint JNICALL
Java_com_vladium_utils_SystemInformation_getCPUs (JNIEnv * env, jclass cls)
{
    return (jint)s_numberOfProcessors;
}
/* ......................................................................... */

/*
 * Class:     com_vladium_utils_SystemInformation
 * Method:    getSysInfo
 * Signature: ()S
 */
JNIEXPORT jstring JNICALL
Java_com_vladium_utils_SystemInformation_getSysInfo (JNIEnv * env, jclass cls)
{
    char buf[2048];
    char buf2[512];
    *buf=0;
    OSVERSIONINFOEX osvi;
    BOOL bOsVersionInfoEx;
    TCHAR  infoBuf[INFO_BUFFER_SIZE];
    DWORD  bufCharCount = INFO_BUFFER_SIZE;


    // Try calling GetVersionEx using the OSVERSIONINFOEX structure.
    // If that fails, try using the OSVERSIONINFO structure.
    ZeroMemory(&osvi, sizeof(OSVERSIONINFOEX));
    osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX);
    if( !(bOsVersionInfoEx = GetVersionEx ((OSVERSIONINFO *) &osvi)) )
    {
       osvi.dwOSVersionInfoSize = sizeof (OSVERSIONINFO);
       if (! GetVersionEx ( (OSVERSIONINFO *) &osvi) ) {
     // Return empty string in case of problems
   goto next_label;
 }
    }
   switch (osvi.dwPlatformId)
   {
      // Test for the Windows NT product family.
      case VER_PLATFORM_WIN32_NT:

         // Test for the specific product.
         if ( osvi.dwMajorVersion == 5 && osvi.dwMinorVersion == 2 )
            strcat(buf,"WinServer2003, ");

         if ( osvi.dwMajorVersion == 5 && osvi.dwMinorVersion == 1 )
            strcat(buf,"WinXP ");

         if ( osvi.dwMajorVersion == 5 && osvi.dwMinorVersion == 0 )
            strcat(buf,"Win2K ");

         if ( osvi.dwMajorVersion <= 4 )
            strcat(buf,"WinNT ");

         // Test for specific product on Windows NT 4.0 SP6 and later.
         if( bOsVersionInfoEx )
         {
            // Test for the workstation type.
            if ( osvi.wProductType == VER_NT_WORKSTATION )
            {
               if( osvi.dwMajorVersion == 4 )
                  strcat(buf,"Workstation 4.0 " );
               else if( osvi.wSuiteMask & VER_SUITE_PERSONAL )
                  strcat(buf,"Home Edition " );
               else
                  strcat(buf,"Professional " );
            }

            // Test for the server type.
            else if ( osvi.wProductType == VER_NT_SERVER ||
                      osvi.wProductType == VER_NT_DOMAIN_CONTROLLER )
            {
               if( osvi.dwMajorVersion == 5 && osvi.dwMinorVersion == 2 )
               {
                  if( osvi.wSuiteMask & VER_SUITE_DATACENTER )
                     strcat(buf,"Datacenter Edition " );
                  else if( osvi.wSuiteMask & VER_SUITE_ENTERPRISE )
                     strcat(buf,"Enterprise Edition " );
                  else if ( osvi.wSuiteMask == VER_SUITE_BLADE )
                     strcat(buf,"Web Edition " );
                  else
                     strcat(buf,"Standard Edition " );
               }

               else if( osvi.dwMajorVersion == 5 && osvi.dwMinorVersion == 0 )
               {
                  if( osvi.wSuiteMask & VER_SUITE_DATACENTER )
                     strcat(buf,"Datacenter Server " );
                  else if( osvi.wSuiteMask & VER_SUITE_ENTERPRISE )
                     strcat(buf,"Advanced Server " );
                  else
                     strcat(buf,"Server " );
               }

               else  // Windows NT 4.0
               {
                  if( osvi.wSuiteMask & VER_SUITE_ENTERPRISE )
                     strcat(buf,"Server 4.0, Enterprise Edition " );
                  else
                     strcat(buf,"Server 4.0 " );
               }
            }
         }
         else  // Test for specific product on Windows NT 4.0 SP5 and earlier
         {
            HKEY hKey;
            char szProductType[BUFSIZE];
            DWORD dwBufLen=BUFSIZE;
            LONG lRet;

            lRet = RegOpenKeyEx( HKEY_LOCAL_MACHINE,
               "SYSTEMCurrentControlSetControlProductOptions",
               0, KEY_QUERY_VALUE, &hKey );
            if( lRet != ERROR_SUCCESS ) {
      goto next_label;
  }

            lRet = RegQueryValueEx( hKey, "ProductType", NULL, NULL,
               (LPBYTE) szProductType, &dwBufLen);
            if( (lRet != ERROR_SUCCESS) || (dwBufLen > BUFSIZE) ) {
  goto next_label;
     }

            RegCloseKey( hKey );

            if ( lstrcmpi( "WINNT", szProductType) == 0 )
               strcat(buf,"Workstation " );
            if ( lstrcmpi( "LANMANNT", szProductType) == 0 )
               strcat(buf,"Server " );
            if ( lstrcmpi( "SERVERNT", szProductType) == 0 )
               strcat(buf,"Advanced Server " );

            sprintf(buf2, "%d.%d ", (int)osvi.dwMajorVersion, (int)osvi.dwMinorVersion );
     strcat(buf,buf2);
         }

      // Display service pack (if any) and build number.

         if( osvi.dwMajorVersion == 4 &&
             lstrcmpi( osvi.szCSDVersion, "Service Pack 6" ) == 0 )
         {
            HKEY hKey;
            LONG lRet;

            // Test for SP6 versus SP6a.
            lRet = RegOpenKeyEx( HKEY_LOCAL_MACHINE,
               "SOFTWAREMicrosoftWindows NTCurrentVersionHotfixQ246009",
               0, KEY_QUERY_VALUE, &hKey );
            if( lRet == ERROR_SUCCESS ) {
               sprintf(buf2, "SP 6a (Build %d), ", (int)(osvi.dwBuildNumber & 0xFFFF) );
        strcat(buf,buf2);
     }
            else // Windows NT 4.0 prior to SP6a
            {
               sprintf(buf2, "%s (Build %d), ",
                  osvi.szCSDVersion,
                  (int)(osvi.dwBuildNumber & 0xFFFF));
        strcat(buf,buf2);
            }

            RegCloseKey( hKey );
         }
         else // not Windows NT 4.0
         {
            sprintf(buf2, "%s (Build %d), ",
               osvi.szCSDVersion,
               (int)(osvi.dwBuildNumber & 0xFFFF));
     strcat(buf,buf2);
         }


         break;

      // Test for the Windows Me/98/95.
      case VER_PLATFORM_WIN32_WINDOWS:

         if (osvi.dwMajorVersion == 4 && osvi.dwMinorVersion == 0)
         {
             strcat(buf,"Win95 ");
             if ( osvi.szCSDVersion[1] == 'C' || osvi.szCSDVersion[1] == 'B' )
                strcat(buf,"OSR2 " );
         }

         if (osvi.dwMajorVersion == 4 && osvi.dwMinorVersion == 10)
         {
             strcat(buf,"Win98 ");
             if ( osvi.szCSDVersion[1] == 'A' )
                strcat(buf,"SE " );
         }

         if (osvi.dwMajorVersion == 4 && osvi.dwMinorVersion == 90)
         {
             strcat(buf,"WinME ");
         }
         break;

      case VER_PLATFORM_WIN32s:

         strcat(buf,"Win32s ");
         break;
   }

next_label:

   strcat(buf,"
         on ");
   // Get and display the name of the computer.
   bufCharCount = INFO_BUFFER_SIZE;
   if( !GetComputerName( infoBuf, &bufCharCount ) )
     goto next_label_2;
   strcat(buf, infoBuf );

next_label_2:
    strcat(buf," (");
    if (!(osvi.dwPlatformId==VER_PLATFORM_WIN32_WINDOWS && osvi.dwMajorVersion == 4 && osvi.dwMinorVersion == 0)) {
     // Win95 does not keep CPU info in registry
     LONG lRet;
     HKEY hKey;
            char szOrigCPUType[BUFSIZE];
     int i=0;
            DWORD dwBufLen=BUFSIZE;
            lRet = RegOpenKeyEx( HKEY_LOCAL_MACHINE,
               "HARDWAREDESCRIPTIONSystemCentralProcessor",
               0, KEY_QUERY_VALUE, &hKey );
            if( lRet != ERROR_SUCCESS ) {
      goto next_label_3;
  }

            lRet = RegQueryValueEx( hKey, "ProcessorNameString", NULL, NULL,
               (LPBYTE) szOrigCPUType, &dwBufLen);
            if( (lRet != ERROR_SUCCESS) || (dwBufLen > BUFSIZE) ) {
  goto next_label_3;
     }
            RegCloseKey( hKey );
     if (strlen(szOrigCPUType)>0) {
  while(szOrigCPUType[i]==' ' && szOrigCPUType[i]!=0) i++;
  strcat(buf,szOrigCPUType+i);
     } else goto next_label_3;
    } else {
next_label_3:
     if (processorArchitecture==PROCESSOR_ARCHITECTURE_UNKNOWN) strcat(buf,"unknown_arch");
     else if (processorArchitecture==PROCESSOR_ARCHITECTURE_INTEL) {
      strcat(buf,"Intel ");
      sprintf(buf2,"level %d ",processorLevel);
      strcat(buf,buf2);
     } else if (processorArchitecture==PROCESSOR_ARCHITECTURE_IA64) strcat(buf,"IA64 ");
     else if (processorArchitecture==PROCESSOR_ARCHITECTURE_MIPS) strcat(buf,"MIPS ");
     else if (processorArchitecture==PROCESSOR_ARCHITECTURE_ALPHA) strcat(buf,"Alpha ");
     else if (processorArchitecture==PROCESSOR_ARCHITECTURE_PPC) strcat(buf,"PowerPC ");
     else if (processorArchitecture==PROCESSOR_ARCHITECTURE_SHX) strcat(buf,"SHX ");
     else if (processorArchitecture==PROCESSOR_ARCHITECTURE_ALPHA64) strcat(buf,"Alpha64 ");
     else strcat(buf,"unknown_arch ");
    }

    strcat(buf,")");

    jstring retval = (*env)->NewStringUTF(env,buf);
    return retval;
}
/* ......................................................................... */


/*
 * Class:     com_vladium_utils_SystemInformation
 * Method:    getProcessID
 * Signature: ()I
 */
JNIEXPORT jint JNICALL
Java_com_vladium_utils_SystemInformation_getProcessID (JNIEnv * env, jclass cls)
{
    return s_PID;
}
/* ......................................................................... */

/*
 * Class:     com_vladium_utils_SystemInformation
 * Method:    setPid
 * Signature: ()I
 */
JNIEXPORT jint JNICALL
Java_com_vladium_utils_SystemInformation_setPid (JNIEnv * env, jclass cls, jint pid)
{
    DWORD errCode;
    LPVOID lpMsgBuf;
    s_PID = pid;
    s_currentProcess = OpenProcess(PROCESS_ALL_ACCESS,FALSE,pid);
    if (s_currentProcess==NULL) {
     errCode = GetLastError();
     FormatMessage(
              FORMAT_MESSAGE_ALLOCATE_BUFFER |
              FORMAT_MESSAGE_FROM_SYSTEM,
              NULL,
              errCode,
              MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
              (LPTSTR) &lpMsgBuf,
              0, NULL );

     printf("[CPUmon] Could not attach to native process.
  Error code: %ld
  Error description: %s
",errCode,lpMsgBuf);
     fflush(stdout);
     LocalFree(lpMsgBuf);
     return errCode;
    }
    printf("[CPUmon] Attached to native process.
");
    fflush(stdout);
    return 0;
}
/* ......................................................................... */

/*
 * Class:     com_vladium_utils_SystemInformation
 * Method:    detachProcess
 * Signature: ()I
 */
JNIEXPORT jint JNICALL
Java_com_vladium_utils_SystemInformation_detachProcess (JNIEnv * env, jclass cls)
{
    if (!alreadyDetached && s_currentProcess!=NULL) {
     CloseHandle(s_currentProcess);
     alreadyDetached = 1;
     printf("[CPUmon] Detached from native process.
");
     fflush(stdout);
    }
    return 0;
}
/* ......................................................................... */

/*
 * Class:     com_vladium_utils_SystemInformation
 * Method:    getProcessCPUTime
 * Signature: ()J
 */
JNIEXPORT jlong JNICALL
Java_com_vladium_utils_SystemInformation_getProcessCPUTime (JNIEnv * env, jclass cls)
{
    FILETIME creationTime, exitTime, kernelTime, userTime;
    DWORD errCode;
    LPVOID lpMsgBuf;

    BOOL resultSuccessful = GetProcessTimes (s_currentProcess, & creationTime, & exitTime, & kernelTime, & userTime);
    if (!resultSuccessful) {
     errCode = GetLastError();
     FormatMessage(
              FORMAT_MESSAGE_ALLOCATE_BUFFER |
              FORMAT_MESSAGE_FROM_SYSTEM,
              NULL,
              errCode,
              MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
              (LPTSTR) &lpMsgBuf,
              0, NULL );

     printf("[CPUmon] An error occured while trying to get CPU time.
  Error code: %ld
  Error description: %s
",errCode,lpMsgBuf);

     fflush(stdout);
     LocalFree(lpMsgBuf);
     return -1;
    }

    return (jlong) ((fileTimeToInt64 (& kernelTime) + fileTimeToInt64 (& userTime)) /
        (s_numberOfProcessors * 10000));
}
/* ......................................................................... */

/*
 * Class:     com_vladium_utils_SystemInformation
 * Method:    getMaxMem
 * Signature: ()J
 */
JNIEXPORT jlong JNICALL
Java_com_vladium_utils_SystemInformation_getMaxMem (JNIEnv * env, jclass cls)
{
 MEMORYSTATUS stat;
 GlobalMemoryStatus (&stat);
        return (jlong)(stat.dwTotalPhys/1024);
}
/* ......................................................................... */

/*
 * Class:     com_vladium_utils_SystemInformation
 * Method:    getFreeMem
 * Signature: ()J
 */
JNIEXPORT jlong JNICALL
Java_com_vladium_utils_SystemInformation_getFreeMem (JNIEnv * env, jclass cls)
{
        MEMORYSTATUS stat;
 GlobalMemoryStatus (&stat);
        return (jlong)(stat.dwAvailPhys/1024);
}
/* ......................................................................... */


/* define min elapsed time (in units of 10E-7 sec): */
#define MIN_ELAPSED_TIME (10000)

/*
 * Class:     com_vladium_utils_SystemInformation
 * Method:    getProcessCPUUsage
 * Signature: ()D
 */
JNIEXPORT jdouble JNICALL
Java_com_vladium_utils_SystemInformation_getProcessCPUUsage (JNIEnv * env, jclass cls)
{
    FILETIME creationTime, exitTime, kernelTime, userTime, nowTime;
    LONGLONG elapsedTime;
    DWORD errCode;
    LPVOID lpMsgBuf;

    BOOL resultSuccessful = GetProcessTimes (s_currentProcess, & creationTime, & exitTime, & kernelTime, & userTime);
    if (!resultSuccessful) {
     errCode = GetLastError();
     FormatMessage(
              FORMAT_MESSAGE_ALLOCATE_BUFFER |
              FORMAT_MESSAGE_FROM_SYSTEM,
              NULL,
              errCode,
              MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
              (LPTSTR) &lpMsgBuf,
              0, NULL );

     printf("[CPUmon] An error occured while trying to get CPU time.
  Error code: %ld
  Error description: %s
",errCode,lpMsgBuf);
     fflush(stdout);
     LocalFree(lpMsgBuf);
     return -1.0;
    }
    GetSystemTimeAsFileTime (& nowTime);

    /*
        NOTE: win32 system time is not very precise [~10ms resolution], use
        sufficiently long sampling intervals if you make use of this method.
    */

    elapsedTime = fileTimeToInt64 (& nowTime) - fileTimeToInt64 (& creationTime);

    if (elapsedTime < MIN_ELAPSED_TIME)
        return 0.0;
    else
        return ((jdouble) (fileTimeToInt64 (& kernelTime) + fileTimeToInt64 (& userTime))) /
            (s_numberOfProcessors * elapsedTime);
}
/* ......................................................................... */

/*
 * Class:     com_vladium_utils_SystemInformation
 * Method:    getProcessCPUPercentage
 * Signature: ()D
 */
JNIEXPORT jdouble JNICALL
Java_com_vladium_utils_SystemInformation_getProcessCPUPercentage (JNIEnv * env, jclass cls)
{
 // Not implemented on Windows
        return (jdouble)(-1.0);
}
/* ......................................................................... */

/*
 * Class:     com_vladium_utils_SystemInformation
 * Method:    getMemoryUsage
 * Signature: ()J
 */
JNIEXPORT jlong JNICALL
Java_com_vladium_utils_SystemInformation_getMemoryUsage (JNIEnv * env, jclass cls)
{
    PROCESS_MEMORY_COUNTERS pmc;

    if ( GetProcessMemoryInfo( s_currentProcess, &pmc, sizeof(pmc)) )
    {
 return (jlong)(pmc.PagefileUsage/1024);
    } else {
 return (jlong)(0);
    }
}
/* ......................................................................... */

/*
 * Class:     com_vladium_utils_SystemInformation
 * Method:    getMemoryResident
 * Signature: ()J
 */
JNIEXPORT jlong JNICALL
Java_com_vladium_utils_SystemInformation_getMemoryResident (JNIEnv * env, jclass cls)
{
    PROCESS_MEMORY_COUNTERS pmc;

    if ( GetProcessMemoryInfo( s_currentProcess, &pmc, sizeof(pmc)) )
    {
 return (jlong)(pmc.WorkingSetSize/1024);
    } else {
 return (jlong)(0);
    }
}


#undef MIN_ELAPSED_TIME

/* ------------------------------------------------------------------------- */
/* end of file */

 

    JNI裏有兩個特殊的函數——JNI_OnLoad和JNI_OnUnload,它們分別在加載和卸載庫的時候被調用。JNI_OnLoad在調用其他任 何方法之前被執行,而且能夠很方便地用於初始化在這一進程的生命週期中沒有發生變化的變量,並用於協調JNI規範的版本。在默認情況下,庫會測量它自己的 進程的參數,但是通過調用systemInformation.setPid()方法它可以從Java應用程序被重載。s_PID C變量用來保存PID,而s_currentProcess用來保存進程句柄(用於Windows的是HANDLE類型,而用於Solaris的是int 類型)。爲了讀取的一些參數,應該首先打開進程句柄,我們需要在庫關閉使用的時候停止同一個進程句柄(通常它在JVM因爲相同的原因而關閉的時候發生)。 這就是JNI_OnUnload()函數起作用的地方。但是,JVM的一些實現事實上沒有調用JNI_OnUnload(),還有發生句柄會永遠打開的危 險。爲了降低這種可能性,我們應該在Java應用程序里加入一個明確調用detachProcess() C函數的關閉掛鉤。下面就是我們加入關閉掛鉤的方法:

if (pid!=-1) {
            int result = SystemInformation.setPid(pid);
            if (result!=0) {
                return;
            }
            hasPid = true;
            // Create shutdown hook for proper process detach
            Runtime.getRuntime().addShutdownHook(new Thread() {
                public void run() {
                    SystemInformation.detachProcess();
                }
            });
        }

    通過調用WinAPI裏的GetSystemInfo(),我們還可以獲得關於中央處理器的一些信息。只要它是CPU的佔用率根據這個值來調節,測量進程 最重要的值就是處理器的個數(s_numberOfProcessors)。SystemInformation.getSysInfo()的Win32 實現相當麻煩,因爲在各個版本的Windows裏,關於操作系統的版本、補丁、服務包以及相關硬件等信息被以不同的方式保存。所以需要讀者來分析相關的源 代碼和代碼中的註釋。下面就是Windows XP上代碼輸出的示例:

System.out.println("SysInfo: ”+SystemInformation.getSysInfo()):

SysInfo: WinXP Professional Service Pack 1 (Build 2600),
         on DBMOSWS2132 (Intel(R) Xeon(TM) CPU 1.70GHz)

And the same code on Solaris will give:

SysInfo: SunOS 5.8 sxav-dev Generic_108528-29 sun4u sparc 
           SUNW,Ultra-Enterprise Sun_Microsystems

    爲了獲得CPU上進程所佔用的總時間,我們使用了WinAPI的函數GetProcessTimes.其他的函數實現都非常簡單,直接調用WinAPI, 所以沒有什麼必要討論它們。列表D裏是用於Windows版本的GCC的make.bat文件,用來幫助讀者創建相關的。dll庫。

列表D

    gcc -D_JNI_IMPLEMENTATION_ -Wl,——kill-at -IC:/jdk1.3.1_12/include -IC:/jdk1.3.1_12/include/win32 -shared C:/cpu_profile/src/native/com_vladium_utils_SystemInformation.c -o C:/cpu_profile/dll/silib.dll C:/MinGW/lib/libpsapi.a  這個庫的Solaris實現見列表E和列表F.這兩個都是C語言文件,應該被編譯到一個共享庫(.so)裏。用於編譯共享庫的幫助器make.sh見列表 G.所有基於Solaris系統的調用被移到列表F裏,這使得列表E就是一個JNI的簡單包裝程序。Solaris實現要比Win32實現更加複雜,要求 更多的臨時數據結構、內核和進程表。列出的代碼裏有更多的註釋。

 

 

   列表E

/* ------------------------------------------------------------------------- */
/*
 * An implementation of JNI methods in com.vladium.utils.SystemInformation
 * class.
 * This is a ported version from Win32 to Solaris.
 *
 * For simplicity, this implementaion assumes JNI 1.2+ and omits error handling.
 *
 * Port from Win32 by Peter V. Mikhalenko (C) 2004, Deutsche Bank [[email protected]]
 * Original source (C) 2002, Vladimir Roubtsov [[email protected]]
 */
/* ------------------------------------------------------------------------- */

#include

#include "com_vladium_utils_SystemInformation.h"

// Helper Solaris8-dependent external routines
extern int sol_getCPUs();
extern int sol_getFreeMem();
extern int sol_getMaxMem();
extern long int sol_getProcessCPUTime(int pid,int nproc);
extern double sol_getProcessCPUPercentage(int pid);
extern long sol_getMemoryUsage(int pid);
extern long sol_getMemoryResident(int pid);
extern char* sol_getSysInfo();
extern void initKVM();

static jint s_PID;
static int s_numberOfProcessors;
static int externalCPUmon;
static int alreadyDetached;

/* ------------------------------------------------------------------------- */

/* ......................................................................... */

/*
 * This method was added in JNI 1.2. It is executed once before any other
 * methods are called and is ostensibly for negotiating JNI spec versions, but
 * can also be conveniently used for initializing variables that will not
 * change throughout the lifetime of this process.
 */
JNIEXPORT jint JNICALL
JNI_OnLoad (JavaVM * vm, void * reserved)
{
    s_PID = _getpid ();
    externalCPUmon = 0;
    alreadyDetached = 0;

    /* use kstat to update all processor information */
    s_numberOfProcessors = sol_getCPUs();
    initKVM();

    return JNI_VERSION_1_2;
}
/* ......................................................................... */

/*
 * Class:     com_vladium_utils_SystemInformation
 * Method:    getCPUs
 * Signature: ()I
 */
JNIEXPORT jint JNICALL
Java_com_vladium_utils_SystemInformation_getCPUs (JNIEnv * env, jclass cls)
{
    return (jint)s_numberOfProcessors;
}
/* ......................................................................... */

/*
 * Class:     com_vladium_utils_SystemInformation
 * Method:    getSysInfo
 * Signature: ()S
 */
JNIEXPORT jstring JNICALL
Java_com_vladium_utils_SystemInformation_getSysInfo (JNIEnv * env, jclass cls)
{
    char * buf = sol_getSysInfo();
    jstring retval = (*env)->NewStringUTF(env,buf);
    free(buf);
    return retval;
}
/* ......................................................................... */


/*
 * Class:     com_vladium_utils_SystemInformation
 * Method:    getProcessID
 * Signature: ()I
 */
JNIEXPORT jint JNICALL
Java_com_vladium_utils_SystemInformation_getProcessID (JNIEnv * env, jclass cls)
{
    return s_PID;
}
/* ......................................................................... */

/*
 * Class:     com_vladium_utils_SystemInformation
 * Method:    setPid
 * Signature: ()I
 */
JNIEXPORT jint JNICALL
Java_com_vladium_utils_SystemInformation_setPid (JNIEnv * env, jclass cls, jint pid)
{
    s_PID = pid;
    externalCPUmon = 1;
    printf("[CPUmon] Attached to process.
");
    fflush(stdout);
    return 0;
}
/* ......................................................................... */

/*

 * Class:     com_vladium_utils_SystemInformation

 * Method:    detachProcess

 * Signature: ()I

 */

JNIEXPORT jint JNICALL

Java_com_vladium_utils_SystemInformation_detachProcess (JNIEnv * env, jclass cls)

{

if (externalCPUmon && !alreadyDetached) {

alreadyDetached = 1;
     printf("[CPUmon] Detached from process. ");
     fflush(stdout);
    }
    return 0;
}
/* ......................................................................... */

/*
 * Class:     com_vladium_utils_SystemInformation
 * Method:    getProcessCPUTime
 * Signature: ()J
 */
JNIEXPORT jlong JNICALL
Java_com_vladium_utils_SystemInformation_getProcessCPUTime (JNIEnv * env, jclass cls)
{
 return (jlong)sol_getProcessCPUTime((int)s_PID,s_numberOfProcessors);
}
/* ......................................................................... */

/*
 * Class:     com_vladium_utils_SystemInformation
 * Method:    getMaxMem
 * Signature: ()J
 */
JNIEXPORT jlong JNICALL
Java_com_vladium_utils_SystemInformation_getMaxMem (JNIEnv * env, jclass cls)
{
 return (jlong)sol_getMaxMem();
}
/* ......................................................................... */

/*
 * Class:     com_vladium_utils_SystemInformation
 * Method:    getFreeMem
 * Signature: ()J
 */
JNIEXPORT jlong JNICALL
Java_com_vladium_utils_SystemInformation_getFreeMem (JNIEnv * env, jclass cls)
{
 return (jlong)sol_getFreeMem();
}
/* ......................................................................... */


/* define min elapsed time (in units of 10E-7 sec): */
#define MIN_ELAPSED_TIME (10000)

/*
 * Class:     com_vladium_utils_SystemInformation
 * Method:    getProcessCPUUsage
 * Signature: ()D
 */
JNIEXPORT jdouble JNICALL
Java_com_vladium_utils_SystemInformation_getProcessCPUUsage (JNIEnv * env, jclass cls)
{
 return 0.0;
}
/* ......................................................................... */

/*
 * Class:     com_vladium_utils_SystemInformation
 * Method:    getProcessCPUPercentage
 * Signature: ()D
 */
JNIEXPORT jdouble JNICALL
Java_com_vladium_utils_SystemInformation_getProcessCPUPercentage (JNIEnv * env, jclass cls)
{
        return (jdouble)sol_getProcessCPUPercentage((int)s_PID);
}
/* ......................................................................... */


/*
 * Class:     com_vladium_utils_SystemInformation
 * Method:    getMemoryUsage
 * Signature: ()J
 */
JNIEXPORT jlong JNICALL
Java_com_vladium_utils_SystemInformation_getMemoryUsage (JNIEnv * env, jclass cls)
{
 return (jlong)sol_getMemoryUsage((int)s_PID);
}
/* ......................................................................... */

/*
 * Class:     com_vladium_utils_SystemInformation
 * Method:    getMemoryResident
 * Signature: ()J
 */
JNIEXPORT jlong JNICALL
Java_com_vladium_utils_SystemInformation_getMemoryResident (JNIEnv * env, jclass cls)
{
 return (jlong)sol_getMemoryResident((int)s_PID);
}

 

#undef MIN_ELAPSED_TIME

/* ------------------------------------------------------------------------- */
/* end of file */
列表F/* ------------------------------------------------------------------------- */
/*
 * Solaris-dependent system routines and kernel calls
 * Used for getting memory and CPU consumption statistics
 *
 * Author: Peter V. Mikhalenko (C) 2004, Deutsche Bank [[email protected]]
 */
/* ------------------------------------------------------------------------- */

#include
# ifndef KSTAT_DATA_UINT32
#  define ui32 ul
# endif
#include
#include
#include
#include
#include
#include
#include
#include

#define _STRUCTURED_PROC 1
#include
#define prpsinfo psinfo
#define pr_fill pr_nlwp
/* These require an ANSI C compiler "Reisser cpp" doesn't like this */
#define pr_state pr_lwp.pr_state
#define pr_oldpri pr_lwp.pr_oldpri
#define pr_nice pr_lwp.pr_nice
#define pr_pri pr_lwp.pr_pri
#define pr_onpro pr_lwp.pr_onpro
#define ZOMBIE(p) ((p)->pr_nlwp == 0)
#define SIZE_K(p) ((p)->pr_size)
#define RSS_K(p) ((p)->pr_rssize)
#define PROCFS "/proc"


/* definitions for indices in the nlist array */
#define X_V    0
#define X_MPID    1
#define X_ANONINFO   2
#define X_MAXMEM   3
#define X_SWAPFS_MINFREE  4
#define X_FREEMEM   5
#define X_AVAILRMEM   6
#define X_AVENRUN   7
#define X_CPU    8
#define X_NPROC    9
#define X_NCPUS      10

static struct nlist nlst[] =
{
  ,   /* 0 */ /* replaced by dynamic allocation */
  ,   /* 1 */
#if OSREV >= 56
  /* this structure really has some extra fields, but the first three match */
  ,  /* 2 */
#else
  ,   /* 2 */
#endif
  ,   /* 3 */ /* use sysconf */
  ,  /* 4 */ /* used only w/ USE_ANONINFO */
  ,   /* 5 */ /* available from kstat >= 2.5 */
  ,  /* 6 */ /* available from kstat >= 2.5 */
  ,   /* 7 */ /* available from kstat */
  ,   /* 8 */ /* available from kstat */
  ,   /* 9 */ /* available from kstat */
  ,   /* 10 */ /* available from kstat */

};

static kstat_ctl_t *kc = NULL;
static kstat_t **cpu_ks;
static cpu_stat_t *cpu_stat;
static int ncpus;
kvm_t *kd;
static unsigned long freemem_offset;
static unsigned long maxmem_offset;
static unsigned long freemem = -1L;
static unsigned long maxmem = -1L;

/* pagetok function is really a pointer to an appropriate function */
static int pageshift;
static int (*p_pagetok) ();
#define pagetok(size) ((*p_pagetok)(size))

int pagetok_none(int size) {
    return(size);
}

int pagetok_left(int size) {
    return(size << pageshift);
}

int pagetok_right(int size) {
    return(size >> pageshift);
}

#define UPDKCID(nk,ok)
if (nk == -1) {
  perror("kstat_read ");
  exit(1);
}
if (nk != ok)
  goto kcid_changed;

void initKVM() {
    int i;

    /* perform the kvm_open - suppress error here */
    kd = kvm_open (NULL, NULL, NULL, O_RDONLY, NULL);

    /* calculate pageshift value */
    i = sysconf(_SC_PAGESIZE);
    pageshift = 0;
    while ((i >>= 1) > 0)
    {
 pageshift++;
    }

    /* calculate an amount to shift to K values */
    /* remember that log base 2 of 1024 is 10 (i.e.: 2^10 = 1024) */
    pageshift -= 10;

    /* now determine which pageshift function is appropriate for the
       result (have to because x << y is undefined for y < 0) */
    if (pageshift > 0)
    {
 /* this is the most likely */
 p_pagetok = pagetok_left;
    }
    else if (pageshift == 0)
    {
 p_pagetok = pagetok_none;
    }
    else
    {
 p_pagetok = pagetok_right;
 pageshift = -pageshift;
    }
}

#define SI_LEN 512
#define BUFSIZE 256

char * sol_getSysInfo() {
 char * retbuf = (char*)malloc(SI_LEN);
 int curend = 0;
 int maxl = SI_LEN;
 *retbuf=0;
 char * buf = (char*)malloc(BUFSIZE);
 long res = sysinfo(SI_SYSNAME,buf,BUFSIZE);
 if (res>0 && res<=maxl) {
  strcat(retbuf,buf);
  curend=res-1;
  maxl=SI_LEN-curend;
 }
 if (curend0 && res<=maxl) {
  strcat(retbuf,buf);
  curend+=res-1;
  maxl=SI_LEN-curend;
 }
 if (curend0 && res<=maxl) {
  strcat(retbuf,buf);
  curend+=res-1;
  maxl=SI_LEN-curend;
 }
 if (curend0 && res<=maxl) {
                strcat(retbuf,buf);
                curend+=res-1;
                maxl=SI_LEN-curend;
        }
        if (curend0 && res<=maxl) {
                strcat(retbuf,buf);
                curend+=res-1;
                maxl=SI_LEN-curend;
        }
        if (curend0 && res<=maxl) {
                strcat(retbuf,buf);
                curend+=res-1;
                maxl=SI_LEN-curend;
        }
        if (curend0 && res<=maxl) {
                strcat(retbuf,buf);
                curend+=res-1;
                maxl=SI_LEN-curend;
        }
        if (curend0 && res<=maxl) {
                strcat(retbuf,buf);
                curend+=res-1;
                maxl=SI_LEN-curend;
        }
        if (curendvalue.ui32 > ncpus) {
     ncpus = kn->value.ui32;
     cpu_ks = (kstat_t **) realloc (cpu_ks, ncpus * sizeof (kstat_t *));
     cpu_stat = (cpu_stat_t *) realloc (cpu_stat,
   ncpus * sizeof (cpu_stat_t));
 }

 for (ks = kc->kc_chain; ks;
      ks = ks->ks_next)
 {
     if (strncmp(ks->ks_name, "cpu_stat", 8) == 0)
     {
  nkcid = kstat_read(kc, ks, NULL);
  /* if kcid changed, pointer might be invalid */
  UPDKCID(nkcid, kcid);

  cpu_ks[ncpu] = ks;
  ncpu++;
  if (ncpu > ncpus)
  {
      fprintf(stderr, "kstat finds too many cpus: should be %d
",
       ncpus);
      exit(1);
  }
     }
 }
 /* note that ncpu could be less than ncpus, but that's okay */
 changed = 0;
    }

    /* return the number of cpus found */
    ncpus=ncpu;
    return ncpu;
}

unsigned long sol_getMaxMem() {
 maxmem = pagetok(sysconf(_SC_PHYS_PAGES));
 return maxmem;
}

unsigned long sol_getFreeMem() {
 kstat_t *ks;
 kstat_named_t *kn;
 ks = kstat_lookup(kc, "unix", 0, "system_pages");
 if (kstat_read(kc, ks, 0) == -1) {
     perror("kstat_read");
     exit(1);
 }
 if (kd != NULL) {        /* always get freemem from kvm if we can*/
          (void) getkval (freemem_offset, (int *) (&freemem), sizeof (freemem), "freemem");
 } else {
   kn = kstat_data_lookup(ks, "freemem");
   if (kn)
     freemem = kn->value.ul;
 }
        return (unsigned long)pagetok(freemem);
}

// Returns the number of milliseconds (not nanoseconds and seconds) elapsed on processor
// since process start. The returned value is adjusted for the number of processors in the system.
long int sol_getProcessCPUTime(int pid,int nproc) {
 struct prpsinfo currproc;
 int fd;
       char buf[30];
 long int retval=0;
 snprintf(buf, sizeof(buf), "%s/%d/psinfo", PROCFS, pid);
 if ((fd = open (buf, O_RDONLY)) < 0) {
  return 0L;
 }
 if (read(fd, &currproc, sizeof(psinfo_t)) != sizeof(psinfo_t)) {
    (void)close(fd);
  return 0L;
 }
 (void)close(fd);
 retval = (currproc.pr_time.tv_sec * 1000 + currproc.pr_time.tv_nsec / 1000000) / nproc;
 return retval;
}

// Returns percentage CPU by pid
// In Solaris 8 it is contained in procfs
double sol_getProcessCPUPercentage(int pid) {
 struct prpsinfo currproc;
        int fd;
        char buf[30];
        double retval=0.0;
        snprintf(buf, sizeof(buf), "%s/%d/psinfo", PROCFS, pid);
        if ((fd = open (buf, O_RDONLY)) < 0) {
                return 0;
        }
        if (read(fd, &currproc, sizeof(psinfo_t)) != sizeof(psinfo_t)) {
                (void)close(fd);
                return 0;
        }
        (void)close(fd);
        retval = ((double)currproc.pr_pctcpu)/0x8000*100;
        return retval;
}

// Returns current space allocated for the process, in bytes. Those pages may or may not be in memory.
long sol_getMemoryUsage(int pid) {
 struct prpsinfo currproc;
        int fd;
        char buf[30];
        double retval=0.0;
        snprintf(buf, sizeof(buf), "%s/%d/psinfo", PROCFS, pid);
        if ((fd = open (buf, O_RDONLY)) < 0) {
                return 0;
        }
        if (read(fd, &currproc, sizeof(psinfo_t)) != sizeof(psinfo_t)) {
                (void)close(fd);
                return 0;
        }
        (void)close(fd);
        retval = currproc.pr_size;
        return retval;
}

// Returns current process space being resident in memory.
long sol_getMemoryResident(int pid) {
 struct prpsinfo currproc;
        int fd;
        char buf[30];
        double retval=0.0;
        snprintf(buf, sizeof(buf), "%s/%d/psinfo", PROCFS, pid);
        if ((fd = open (buf, O_RDONLY)) < 0) {
                return 0;
        }
        if (read(fd, &currproc, sizeof(psinfo_t)) != sizeof(psinfo_t)) {
                (void)close(fd);
                return 0;
        }
        (void)close(fd);
        retval = currproc.pr_rssize;
        return retval;
}

/*
 *  getkval(offset, ptr, size, refstr) - get a value out of the kernel.
 * "offset" is the byte offset into the kernel for the desired value,
 *   "ptr" points to a buffer into which the value is retrieved,
 *   "size" is the size of the buffer (and the object to retrieve),
 *   "refstr" is a reference string used when printing error meessages,
 *     if "refstr" starts with a '!', then a failure on read will not
 *       be fatal (this may seem like a silly way to do things, but I
 *       really didn't want the overhead of another argument).
 *
 */
int
getkval (unsigned long offset,
  int *ptr,
  int size,
  char *refstr)
{
  if (kvm_read (kd, offset, (char *) ptr, size) != size)
    {
      if (*refstr == '!')
 {
   return (0);
 }
      else
 {
   fprintf (stderr, "top: kvm_read for %s: %s
", refstr, strerror(errno));
   exit(23);
 }
    }
  return (1);
}

    列表G

#!/bin/sh
gcc -G -lkvm -lkstat com_vladium_utils_SystemInformation.c -o libsilib.so solaris-extern.c
# com_vladium_utils_SystemInformation.c is in Listing-E
# solaris-extern.c is in Listing-F

    在本文裏,我已告訴你如何編程測量Java進程的內存和CPU佔用率。當然用戶可以通過查看Windows任務管理器或者ps的輸出來達到相同的目的,但是重要的一點是,用戶現在能夠進行任何長期運行和/或自動的軟件性能測試 ,這對於開發一個分佈式或者可伸縮的,或者實時的性能關鍵的應用程序十分重要。加入獲取系統軟件和硬件信息的能力對於開發人員同樣十分有用。這個庫可以在Win32和Solaris平臺上實現。但是,把它移植到其他UNIX平臺上應該也不是問題,比如Linux.

 

 

 

Ch25:C++調用JAVA方法詳解

 

 

本文主要參考http://tech.ccidnet.com/art/1081/20050413/237901_1.html 上的文章。

?


C++ 調用JAVA主要用到了SUN公司的JNI技術, JNI是Java Native Interface的 縮寫。從Java 1.1開始,Java Native Interface (JNI)標準成爲java平臺的一部分,它允許Java代碼和其他語言寫的代碼進行交互。相關資料見http://java.sun.com/j2se/1.5.0/docs/guide/jni/spec/jniTOC.html

?

開發環境安裝及配置

?

1.1? 安裝JDK 

??????? 到SUN公司網站可以下載到最新版的JDK。下載下來後開始安裝,一路選擇默認配置即可,本文檔中假定安裝的是JDK1.4,安裝目錄爲C:\j2sdk1.4.2_15。





1.2? 配置VC6.0 

???????? 通過Visual C++ 6的菜單Tools→Options打開選項對話框。在Directories標籤頁下添加JDK的相關目錄到Include和目錄下。 
???????????? 

?

?


?開發測試用到的JAVA類

 

2.1? 開發JAVA類

??????? 在硬盤的任意地方新建一個名叫test的文件夾,本文檔示例中將test文件夾建立在C盤根目錄,然後在裏面新建一個名稱叫Demo.java的JAVA文件,將下面測試用的代碼粘貼到該文件中。

?

?

package test;

/**

* 該類是爲了演示JNI如何訪問各種對象屬性等

*/

public class Demo

{

        //用於演示如何訪問靜態的基本類型屬性

        public static int COUNT = 8;

        //演示對象型屬性

        private String msg;

        private int[] counts;

 

        public Demo()

        {

               this("缺省構造函數");

        }

        /**

         * 演示如何訪問構造器

         */

        public Demo(String msg)

        {

               this.msg = msg;

               this.counts = null;

        }

        public String getMessage()

        {

               return msg;

        }

        /**

         * 該方法演示如何訪問一個靜態方法

         */

        public static String getHelloWorld()

        {

               return "Hello world!";

        }

 

        /**

         * 該方法演示參數的傳入傳出及中文字符的處理

         */

        public String append(String str, int i)

        {

               return str + i;

        }

        /**

         * 演示數組對象的訪問

         */

        public int[] getCounts()

        {

         return counts;

        }

        /**

         * 演示如何構造一個數組對象

        */

        public void setCounts(int[] counts)

        {

         this.counts = counts;

        }

        /**

         * 演示異常的捕捉

        */

        public void throwExcp()throws IllegalAccessException

        {

               throw new IllegalAccessException("exception occur.");

        }

}

?

2.2 編譯JAVA類

????? 運行CMD控制檯程序進入命令行模式,輸入命令javac -classpath c:\ c:\test\Demo.java,-classpath參數指定classpath的路徑,這裏就是test目錄所在的路徑。(注意:如果你沒有將 JDK的環境變量設置好,就需要先進入JDK的bin目錄下,如下圖所示。)

 

?

2.3 查看方法的簽名

????? 我們知道Java中允許方法的多態,僅僅是通過方法名並沒有辦法定位到一個具體的方法,因此需要一個字符串來唯一表示一個方法。但是怎麼利用一個字符串來 表示方法的具體定義呢?JDK中已經準備好一個反編譯工具javap,通過這個工具就可以得到類中每個屬性、方法的簽名。在CMD下運行javap -s -p -classpath c:\ test.Demo即可看到屬性和方法的簽名。如下圖紅色矩形框起來的字符串爲方法String append(String str, int i)的簽名。

?

?

在VC中調用JAVA類

?

3.1 快速調用JAVA中的函

????? 在VC中新建一個控制檯程序,然後新建一個CPP文件,將下面的代碼添加到該文件中。運行該文件,即可得到Demo類中String append(String str, int i)函數返回的字符串。

#include "windows.h"

#include "jni.h"

#include <string>

#include <iostream>

using namespace std;

 

jstring NewJString(JNIEnv *env, LPCTSTR str);

string  JStringToCString (JNIEnv *env, jstring str);

 

int main()

{

    //定義一個函數指針,下面用來指向JVM中的JNI_CreateJavaVM函數

    typedef jint (WINAPI *PFunCreateJavaVM)(JavaVM **, void **, void *);

 

    int res;

    JavaVMInitArgs vm_args;

    JavaVMOption options[3];

    JavaVM *jvm;

    JNIEnv *env;

 

    /*設置初始化參數*/

    //disable JIT,這是JNI文檔中的解釋,具體意義不是很清楚 ,能取哪些值也不清楚。

    //從JNI文檔裏給的示例代碼中搬過來的

    options[0].optionString = "-Djava.compiler=NONE";

    //設置classpath,如果程序用到了第三方的JAR包,也可以在這裏麪包含進來

    options[1].optionString = "-Djava.class.path=.;c:\\";

    //設置顯示消息的類型,取值有gc、class和jni,如果一次取多個的話值之間用逗號格開,如-verbose:gc,class

    //該參數可以用來觀察C++調用JAVA的過程,設置該參數後,程序會在標準輸出設備上打印調用的相關信息

    options[2].optionString = "-verbose:NONE";

 

    //設置版本號,版本號有JNI_VERSION_1_1,JNI_VERSION_1_2和JNI_VERSION_1_4

    //選擇一個根你安裝的JRE版本最近的版本號即可,不過你的JRE版本一定要等於或者高於指定的版本號

    vm_args.version = JNI_VERSION_1_4;

    vm_args.nOptions = 3;

    vm_args.options = options;

    //該參數指定是否忽略非標準的參數,如果填JNI_FLASE,當遇到非標準參數時,JNI_CreateJavaVM會返回JNI_ERR

    vm_args.ignoreUnrecognized = JNI_TRUE;

    //加載JVM.DLL動態庫

    HINSTANCE hInstance = ::LoadLibrary("C:\\j2sdk1.4.2_15\\jre\\bin\\client\\jvm.dll");

    if (hInstance == NULL)

    {

        return false;

    }

    //取得裏面的JNI_CreateJavaVM函數指針

    PFunCreateJavaVM funCreateJavaVM = (PFunCreateJavaVM)::GetProcAddress(hInstance, "JNI_CreateJavaVM");

    //調用JNI_CreateJavaVM創建虛擬機

    res = (*funCreateJavaVM)(&jvm, (void**)&env, &vm_args);

    if (res < 0)

    {

        return -1;

    }

    //查找test.Demo類,返回JAVA類的CLASS對象

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

    //根據類的CLASS對象獲取該類的實例

    jobject obj = env->AllocObject(cls);

 

    //獲取類中的方法,最後一個參數是方法的簽名,通過javap -s -p 文件名可以獲得

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

    //構造參數並調用對象的方法

    const char szTest[] = "電信";

    jstring arg = NewJString(env, szTest);

    jstring msg = (jstring) env->CallObjectMethod(obj, mid, arg, 12);

    cout<<JStringToCString(env, msg);

 

    //銷燬虛擬機並釋放動態庫

    jvm->DestroyJavaVM();

    ::FreeLibrary(hInstance);

    return 0;

}

 

string  JStringToCString (JNIEnv *env, jstring str)// (jstring str, LPTSTR desc, int desc_len)

{

    if(str==NULL)

    {

        return "";

    }

    //在VC中wchar_t是用來存儲寬字節字符(UNICODE)的數據類型

    int len = env->GetStringLength(str);

    wchar_t *w_buffer = new wchar_t[len+1];

    char *c_buffer = new char[2*len+1];

    ZeroMemory(w_buffer,(len+1)*sizeof(wchar_t));

    //使用GetStringChars 而不是GetStringUTFChars

    const jchar * jcharString = env->GetStringChars (str, 0);

    wcscpy(w_buffer,jcharString);    

    env->ReleaseStringChars(str,jcharString);

    ZeroMemory(c_buffer,(2*len+1)*sizeof(char));

    /調用字符編碼轉換函數(Win32 API)將UNICODE轉爲ASCII編碼格式字符串

    len = WideCharToMultiByte(CP_ACP,0,w_buffer,len,c_buffer,2*len,NULL,NULL);

    string cstr = c_buffer;

    delete[] w_buffer;

    delete[] c_buffer;

 

    return cstr;

}

 

jstring NewJString(JNIEnv *env, LPCTSTR str)

{

    if(!env || !str)

    {

        return 0;

    }

    int slen = strlen(str);

    jchar* buffer = new jchar[slen];

    int len = MultiByteToWideChar(CP_ACP,0,str,strlen(str),buffer,slen);

    if(len>0 && len < slen)

    {

        buffer[len]=0;

    }

    jstring js = env->NewString(buffer,len);

    delete [] buffer;

    return js;

}

?

3.2 調用步驟分析及注意事項

?

???? a、加載jvm.dll動態庫,然後獲取裏面的JNI_CreateJavaVM函數。這個步驟也可以通過在VC工程的LINK標籤頁裏添加對 jvm.lib的連接,然後在環境變量裏把jvm.dll所在的路徑加上去來實現。但後面這種方法在部署的時候會比前一個方法麻煩。


???? b、利用構造好的參數,調用JNI_CreateJavaVM函數創建JVM。JNI_CreateJavaVM函數內部會自動根據jvm.dll的路徑 來獲取JRE的環境,所以千萬不要把jvm.dll文件拷貝到別的地方,然後再通過LoadLibrary函數導入。


???? c、JVM創建成功後,JNI_CreateJavaVM函數會傳出一個JNI上下文環境對象(JNIEnv),利用該對象的相關函數就可以調用JAVA類的屬性和方法了。


???? d、以上面的代碼爲例:先調用JNIEnv的FindClass方法,該函數傳入一個參數,該參數就是java類的全局帶包名的名稱,如上面示例中的 test/Demo表示test包中的Demo類。這個方法會在你創建JVM時設置的classpath路徑下找相應的類,找到後就會返回該類的 class對象。 Class是JAVA中的一個類,每個JAVA類都有唯一的一個靜態的Class對象,Class對象包含類的相關信息。爲了使FindClass方法能 找到你的類,請確保創建JVM時-Djava.class.path=參數設置正確。注意:系統環境變量中的CLASSPATH對這裏創建JVM沒有影 響,所以不要以爲系統CLASSPATH設置好了相關路徑後這裏就不用設置了。


???? e、利用FindClass返回的class對象,調用GetMethodID函數可以獲得裏面方法的ID,在這裏GetMethodID函數傳入了三個 參數:第一個參數是class對象,因爲方法屬於某個具體的類;第二個參數是方法的名稱;第三個參數是方法的簽名,這個簽名可以在前面3.3中介紹的方法 獲得。


???? f、利用class對象,可以通過調用AllocObject函數獲得該class對象對應類的一個實例,即Demo類的對象。


???? g、利用上面獲取的函數ID和Demo類的對象,就可以通過CallObjectMethod函數調用相應的方法,該函數的參數跟printf函數的參數 一樣,個數是不定的。第一個參數是類的對象;第二個參數是要調用的方法的ID;後面的參數就是需要傳給調用的JAVA類方法的參數,如果調用的JAVA類 方法沒有參數,則調用CallObjectMethod時傳前兩個參數就可以了。


???? h、從上面的示例中可以看到,在調用JAVA的方法前,構造傳入的字符串時,用到了NewJString函數;在調用該方法後,對傳出的字符串調用了 JstringToCString函數。這是由於Java中所有的字符都是Unicode編碼,但是在本地方法中,例如用VC編寫的程序,如果沒有特殊的 定義一般都沒有使用Unicode的編碼方式。爲了讓本地方法能夠訪問Java中定義的中文字符及Java訪問本地方法產生的中文字符串,定義了兩個方法 用來做相互轉換。


?? ? i、避免在被調用的JAVA類中使用靜態final成員變量,因爲在C++中生成一個JAVA類的對象時,靜態final成員變量不會像JAVA中new 對象時那樣先賦值。如果出現這種情況,在C++中調用該對象的方法時會發現該對象的靜態final成員變量值全爲0或者null(根據成員變量的類型而 定)。

?

3.3 調用JAVA中的靜態方法

?

//調用靜態方法

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

jmethodID mid = env->GetStaticMethodID(cls, "getHelloWorld","()Ljava/lang/String;");

jstring msg = (jstring)env->CallStaticObjectMethod(cls, mid);       

cout<<JStringToCString(env, msg);

?

3.4 調用JAVA中的靜態屬性

?

//調用靜態方法

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

jfieldID fid = env->GetStaticFieldID(cls, "COUNT","I");

int count = (int)env->GetStaticIntField(cls, fid);  

cout<<count<<endl;

?

3.5 調用JAVA中的帶參數構造函數

?

//調用構造函數

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

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

const char szTest[] = "電信";

jstring arg = NewJString(env, szTest);

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

//驗證是否構造成功

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

jstring msg = (jstring)env->CallObjectMethod(demo, mid);    

cout<<JStringToCString(env, msg);

?

3.6 傳入傳出數組

?

//傳入傳出數組

//構造數組

long           arrayCpp[] = {1,3,5,7,9};

jintArray array = env->NewIntArray(5);

env->SetIntArrayRegion(array, 0, 5, arrayCpp);

//傳入數組

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

jobject obj = env->AllocObject(cls);

jmethodID mid = env->GetMethodID(cls,"setCounts","([I)V");

env->CallVoidMethod(obj, mid, array);

//獲取數組

mid = env->GetMethodID(cls,"getCounts","()[I");

jintArray msg = (jintArray)env->CallObjectMethod(obj, mid, array);

int len =env->GetArrayLength(msg);

jint* elems =env-> GetIntArrayElements(msg, 0);

for(int i=0; i< len; i++)

{

    cout<<"ELEMENT "<<i<<" IS "<<elems[i]<<endl;

}

env->ReleaseIntArrayElements(msg, elems, 0);

?

3.7 異常處理 
???? 由於調用了Java的方法,因此難免產生操作的異常信息,如JAVA函數返回的異常,或者調用JNI方法(如GetMethodID)時拋出的異常。這些 異常沒有辦法通過C++本身的異常處理機制來捕捉到,但JNI可以通過一些函數來獲取Java中拋出的異常信息。

//異常處理

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

jobject obj = env->AllocObject(cls);

jmethodID mid = env->GetMethodID(cls,"throwExcp","()V");

env->CallVoidMethod(obj, mid);

//獲取異常信息

string exceptionInfo = "";

jthrowable excp = 0;

excp = env->ExceptionOccurred();     

if(excp)

{

    jclass cls = env->GetObjectClass(excp);

    env->ExceptionClear();

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

    jstring msg = (jstring) env->CallObjectMethod(excp, mid);

    out<<JStringToCString(env, msg)<<endl;   

    env->ExceptionClear();

}

?

 

Ch26:JNI中文處理問題小結

由於工作關係,需要利用JNI在C++與Java程序之間進行方法調用和數據傳遞,但以前總是在英文環境下工作,對中文(其他語言編碼同理)問題反倒沒有太關注,最近抽了點時間研究了一下,將自己的體會整理如下,供大家討論或參考。
在進一步討論之前,有幾點基礎知識需要說明:

在 Java內部,所有的字符串編碼採用的是Unicode即UCS-2。Unicode是用兩個字節表示每個字符的字符編碼方案。Unicode有一個特 性:它包括了世界上所有的字符字形。所以,各個地區的語言都可以建立與Unicode的映射關係,而Java正是利用了這一點以達到異種語言之間的轉換;

UTF-8是另一種不同於UCS-2/UCS-4的編碼方案,其中UTF代表UCS Transformation Format,它採用變長的方式進行編碼,編碼長度可以是1~3(據說理論上最長可以到6,不懂)。
由 於UCS-2/UCS-4編碼定長的原因,編碼產生的字符串會包含一些特殊的字符,如\0(即0x0,所有0~256的字符Unicode編碼的第一個 字節),這在有些情況下(如傳輸或解析時)會給我們帶來一些麻煩,而且對於一般的英文字母浪費了太多的空間,此外,據說UTF-8還有Unicode所沒 有的糾錯能力(不懂!),因此,Unicode往往只是被用作一種中間碼,用於邏輯表示。關於Unicode/UTF-8的更多信息,見參考1;

Java中文亂碼問題在很多情況下都可能發生:不同應用間,不同平臺間等等,但以上問題已有大量優秀的文章討論過,這裏不作深入探討,詳見參考2、3、4、5。下面簡要總結一下:

 

 

當我們使用默認編碼方式保存源文件時,文件內容實際上是按照我們的系統設定進行編碼保存的,這個設定值即file.encoding可以通過下面的程序獲得:

public class Encoding {  

 public static void main(String[] args) {       

System.out.println(

System.getProperty("file.encoding")

);   

}

}

javac在不指定encoding參數時,如果區域設定不正確,則可能造成編/解碼錯誤,這個問題在編譯一個從別的環境傳過來的文件時可能發生;

2、雖然在Java內部(即運行期間,Runtime)字符串是以Unicode形式存在的,但在class文件中信息是以UTF-8形式存儲的(Unicode僅被用作邏輯表示中間碼) ;

3. 對 於Web應用,以Tomcat爲例,JSP/Servlet引擎提供的JSP轉換工具(jspc)搜索JSP文件中用<%@ page contentType ="text/html; charset=<Jsp-charset>"%>指定的charset。如果在JSP文件中未指定<Jsp-charset& amp; amp; gt;,則取系統默認的file.encoding(這個值在中文平臺上是GBK),可通過控制面板的Regional Options進行修改;jspc用相當於“javac –encoding <Jsp-charset>”的命令解釋JSP文件中出現的所有字符,包括中文字符和ASCII字符,然後把這些字符轉換成Unicode字 符,再轉化成UTF-8格式,存爲JAVA文件。
我曾經偶然將jsp文件存成UTF-8,而在文件內部使用的charset卻是GB2312, 結果運行時總是無法正常顯示中文,後來轉存爲默認編碼方式才 正常。只要文件存儲格式與JSP開頭的charset設置一致,就都可以正常顯示(不過將文件保存成UTF-16的情況下我還沒有試驗成功);

4. 在 XML文件中,encoding表示的是文件本身的編碼方式,如果這個參數設定與文件本身實際的編碼方式不一致的話,則可能解碼失敗,所以應該總是將 encoding設置成與文件編碼方式一致的值;而JSP/HTML的charset則表示按照何種字符集來解碼從文件中讀取出來的字符串(在理解中文問 題時應該把字符串理解成一個二進制或16進制的串,按照不同的charset可能映射成不同的字符)。
我曾經在網上就encoding的具體含義跟別人討論過:如果encoding指的是文件本身的編碼方式,那麼讀取該文件的應用程序在不知道encoding設置的情況下如何正確解讀該文件呢?
根 據討論及個人理解,處理程序(如jspc)總是按ISO8859-1來讀取輸入文件,然後檢查文件開始的幾個字節(即Byte Order Mark,BOM,具體如何判斷,可以參考Tomcat源碼$SOURCE_DIR\jasper\jasper2\src\share\org \apache\jasper\xmlparser\XMLEncodingDetector.java的getEncodingName方法,在JSP Specification的Page Character Encoding一節也有詳細論述)以探測文件是以何種格式保存的,當解析到encoding選項時,若encoding設置與文件實際保存格式不一致, 會嘗試進行轉換,但這種轉換可能在文件實際以ISO8859-1/UTF-8等單字節編碼而encoding被設置成Unicode、UTF-16等雙字 節編碼時發生錯誤。

 

 

 

下面重點討論JNI中在C++程序與Java程序間進行數據傳遞時需要注意的問題。

   在JNI中jstring採用的是UCS-2編碼,與Java中String的編碼方式一致。但是在C++中,字符串是用char(8位)或者 wchar_t(16位,Unicode編碼與jchar一致,但並非所有開發平臺上都是Unicode編碼,詳見參考6),下面的程序證明了這一點(編 譯環境:VC6):

#include <iostream>

using namespace std;

int main()

{   

locale loc( "Chinese-simplified" );  

  //locale loc( "chs" );  

 //locale loc( "ZHI" );   

//locale loc( ".936" );  

 wcout.imbue( loc );   

wcout << L"中文" << endl; //若沒有L,會出問題   

wchar_t wch[] = {0x4E2D, 0x6587, 0x0};

//"中文"二字的Unicode編碼  

 wcout << wch << endl;   

return 0;

}

 

JNI提供了幾個方法來實現jstring與char/wchar_t之間的轉換。

jsize GetStringLength(jstring str)

const jchar *GetStringChars(jstring str, jboolean *isCopy)void ReleaseStringChars(jstring str, const jchar *chars)

此外,爲了便於以UTF-8方式進行傳輸、存儲,JNI還提供了幾個操作UTF格式的方法:

jsize GetStringUTFLength(jstring str)const char* GetStringUTFChars(jstring str, jboolean *isCopy)void ReleaseStringUTFChars(jstring str, const char* chars)

 

GetStringChars返回的是Unicode格式的編碼串,而GetStringUTFChars返回的是UTF-8格式的編碼串。要創建一個 jstring,可以用如下方式:

jstring NewJString( JNIEnv * env, LPCTSTR str )

{   

if (!env || !str)       

return 0;   

int slen = strlen(str);   

jchar * buffer = new jchar[slen];   

int len = MultiByteToWideChar(CP_ACP, 0,

str, strlen(str), buffer, slen);   

if (len > 0 && len < slen)       

buffer[len] = 0;   

jstring js = env->NewString(buffer, len);   

delete [] buffer;   

return js;

}

 

而要將一個jstring對象轉爲一個char字符串數組,可以:

int JStringToChar( JNIEnv * env,

jstring str,

LPTSTR desc,

int desc_len )

{   

int len = 0;   

if (desc == NULL || str == NULL)       

return -1;   

// Check buffer size   

if (env->GetStringLength(str) * 2 + 1 > desc_len)   

{        return -2;    } 

 

memset(desc, 0, desc_len);   

const wchar_t * w_buffer = env->GetStringChars(str, 0);   

len = WideCharToMultiByte(CP_ACP, 0,

w_buffer, wcslen(w_buffer) + 1, desc, desc_len, NULL, NULL);   

env->ReleaseStringChars(str, w_buffer);   

if (len > 0 && len < desc_len)       

desc[len] = 0;   

return strlen(desc);

}

當 然,按照上面的分析,你也可以直接將GetStringChars的返回結果作爲wchar_t串來進行操作。或者,如果你願意,你也可以將 GetStringUTFChars的結果通過MultiByteToWideChar轉換爲UCS2編碼串,再通過 WideCharToMultiByte轉換爲多字節串。

 

const char* pstr = env->GetStringUTFChars(str, false);

int nLen = MultiByteToWideChar( CP_UTF8, 0, pstr, -1, NULL, NULL );

//得到UTF-8編碼的字符串長度

LPWSTR lpwsz = new WCHAR[nLen];   

MultiByteToWideChar( CP_UTF8, 0, pstr, -1, lpwsz, nLen );

//轉換的結果是UCS2格式的編碼串

int nLen1 = WideCharToMultiByte( CP_ACP,

0, lpwsz, nLen,

 NULL, NULL, NULL, NULL );   

LPSTR lpsz = new CHAR[nLen1];

WideCharToMultiByte( CP_ACP, 0, lpwsz, nLen, lpsz, nLen1, NULL, NULL );

//將UCS2格式的編碼串轉換爲多字節

cout << "Out:" << lpsz << endl;

delete [] lpwsz;

delete [] lpsz;

 

當然,我相信很少有人想要或者需要這麼做。這裏需要注意一點,GetStringChars的返回值是jchar,而 GetStringUTFChars的返回值是const char*。

除 了上面的辦法外,當需要經常在jstring和char*之間進行轉換時我們還有一個選擇,那就是下面的這個類。這個類本來是一個叫 Roger S. Reynolds的老外提供的,想法非常棒,但用起來卻不太靈光,因爲作者將考慮的重心放在UTF格式串上,但在實際操作中,我們往往使用的卻是 ACP(ANSI code page)串。下面是原作者的程序:

class UTFString {

private:    UTFString ();

// Default ctor - disallowed

public:   

// Create a new instance from the specified jstring    

UTFString(JNIEnv* env, const jstring& str) :       

mEnv (env),       

mJstr (str),      

 mUtfChars ((char* )mEnv->GetStringUTFChars (mJstr, 0)),        mString (mUtfChars) { }   

// Create a new instance from the specified string    

UTFString(JNIEnv* env, const string& str) :      

mEnv (env),       

mString (str),       

mJstr (env->NewStringUTF (str.c_str ())),       

mUtfChars ((char* )mEnv->GetStringUTFChars (mJstr, 0)) { }   

// Create a new instance as a copy of the specified UTFString

UTFString(const UTFString& rhs) :      

 mEnv (rhs.mEnv),      

 mJstr (mEnv->NewStringUTF (rhs.mUtfChars)),       

mUtfChars ((char* )mEnv->GetStringUTFChars (mJstr, 0)),        mString (mUtfChars) { }   

// Delete the instance and release allocated storage    

~UTFString() {

mEnv->ReleaseStringUTFChars (mJstr, mUtfChars);

}   

// assign a new value to this instance from the given string     UTFString & operator =(const string& rhs) {       

mEnv->ReleaseStringUTFChars (mJstr, mUtfChars);       

mJstr = mEnv->NewStringUTF (rhs.c_str ());       

mUtfChars = (char* )mEnv->GetStringUTFChars (mJstr, 0);        mString = mUtfChars;       

return *this;   

}   

// assign a new value to this instance from the given char* 

   UTFString & operator =(const char* ptr) {       

mEnv->ReleaseStringUTFChars (mJstr, mUtfChars);       

mJstr = mEnv->NewStringUTF (ptr);       

mUtfChars = (char* )mEnv->GetStringUTFChars (mJstr, 0);        mString = mUtfChars;        

return *this;   

}   

// Supply operator methods for converting the UTFString to a string

// or char*, making it easy to pass UTFString arguments to functions     // that require string or char* parameters.    

string & GetString() { return mString; }   

operator string() { return mString; }   

operator const char* () {

return mString.c_str ();

}   

operator jstring() {

return mJstr;

}

private:   

JNIEnv* mEnv;   

// The enviroment pointer for this native method.    

jstring mJstr;  

// A copy of the jstring object that this UTFString represents     char* mUtfChars;

// Pointer to the data returned by GetStringUTFChars     string mString; 

// string buffer for holding the "value" of this instance };

 

我將它改了改:

class JNIString {private:    JNIString (); // Default ctor - disallowedpublic:    // Create a new instance from the specified jstring    JNIString(JNIEnv* env, const jstring& str) :        mEnv (env) {        const jchar* w_buffer = env->GetStringChars (str, 0);        mJstr = env->NewString (w_buffer,                                wcslen (w_buffer)); // Deep Copy, in usual case we only need       // Shallow Copy as we just need this class to       // provide some convenience for handling jstring        mChars = new char[wcslen (w_buffer) * 2 + 1];        WideCharToMultiByte (CP_ACP, 0, w_buffer, wcslen (w_buffer) + 1, mChars, wcslen (w_buffer) * 2 + 1,                             NULL,   NULL);        env->ReleaseStringChars (str, w_buffer);        mString = mChars;    }    // Create a new instance from the specified string    JNIString(JNIEnv* env, const string& str) :        mEnv (env) {        int slen = str.length ();        jchar* buffer = new jchar[slen];        int len = MultiByteToWideChar (CP_ACP, 0, str.c_str (), str.length (), buffer, slen);        if (len > 0 && len < slen)            buffer[len] = 0;        mJstr = env->NewString (buffer, len);        delete [] buffer;        mChars = new char[str.length () + 1];        strcpy (mChars, str.c_str ());        mString.empty ();        mString = str.c_str ();    }    // Create a new instance as a copy of the specified JNIString    JNIString(const JNIString& rhs) :        mEnv (rhs.mEnv) {        const jchar* wstr = mEnv->GetStringChars (rhs.mJstr, 0);        mJstr = mEnv->NewString (wstr, wcslen (wstr));        mEnv->ReleaseStringChars (rhs.mJstr, wstr);        mChars = new char[strlen (rhs.mChars) + 1];        strcpy (mChars, rhs.mChars);        mString = rhs.mString.c_str ();    }    // Delete the instance and release allocated storage    ~JNIString() { delete [] mChars; }    // assign a new value to this instance from the given string    JNIString & operator =(const string& rhs) {        delete [] mChars;        int slen = rhs.length ();        jchar* buffer = new jchar[slen];        int len = MultiByteToWideChar (CP_ACP, 0, rhs.c_str (), rhs.length (), buffer, slen);        if (len > 0 && len < slen)            buffer[len] = 0;        mJstr = mEnv->NewString (buffer, len);        delete [] buffer;        mChars = new char[rhs.length () + 1];        strcpy (mChars, rhs.c_str ());        mString = rhs.c_str ();        return *this;    }    // Supply operator methods for converting the JNIString to a string    // or char*, making it easy to pass JNIString arguments to functions    // that require string or char* parameters.    string & GetString() { return mString; }    operator string() { return mString; }    operator const char* () { return mString.c_str (); }    operator jstring() { return mJstr; }private:    JNIEnv* mEnv;   // The enviroment pointer for this native method.    jstring mJstr;  // A copy of the jstring object that this JNIString represents    char* mChars;   // Pointer to a ANSI code page char array    string mString; // string buffer for holding the "value" of this instance (ANSI code page)};

後者除了將面向 UTF編碼改成了面向ANSI編碼外,還去掉了operator =(const char* ptr)的定義,因爲 operator =(const string& rhs)可以在需要的時候替代前者而無需任何額外編碼。(因爲按照C++規範,const reference可以自動轉換,詳見本人另一文章“關於 const reference 的幾點說明 ”)
   如果你願意,給JNIString再加個JNIString(JNIEnv* env, const wstring& str)和一個operator =(const wstring& rhs)操作符重載就比較完美了,:),很簡單,留給用得到的朋友自己加吧。

 


下面是一個使用該類的例子(真正跟用於演示的code很少,大部分都是些routine code,:)):

#include <iostream>#include <string>#include <assert.h>#include <jni.h>using namespace std;int main() {    int res;    JavaVM* jvm;    JNIEnv* env;    JavaVMInitArgs vm_args;    JavaVMOption options[3];    options[0].optionString = "-Djava.compiler=NONE";    options[1].optionString = "-Djava.class.path=.;.."; // .. is specially for this project    options[2].optionString = "-verbose:jni";    vm_args.version = JNI_VERSION_1_4;    vm_args.nOptions = 3;    vm_args.options = options;    vm_args.ignoreUnrecognized = JNI_TRUE;    res = JNI_CreateJavaVM (& jvm, (void* * )& env, & vm_args);    if (res < 0) {        fprintf (stderr, "Can't create Java VM\n");        return 1;    }    jclass cls = env->FindClass ("jni/test/Demo");    assert (0 != cls);    jmethodID mid = env->GetMethodID (cls, "", "(Ljava/lang/String;)V");    assert (0 != mid);    wchar_t* p = L"中國";    jobject obj = env->NewObject (cls, mid, env->NewString (reinterpret_cast (p), wcslen (p)));    assert (0 != obj);    mid = env->GetMethodID (cls, "getMessage", "()Ljava/lang/String;");    assert (0 != mid);    jstring str = (jstring)env->CallObjectMethod (obj, mid);    // use JNIString for easier handling.    JNIString jnistr (env, str);    cout << "JNIString:" << jnistr.GetString () << endl;    jnistr = "中文";    cout << jnistr.GetString () << endl;    jvm->DestroyJavaVM ();    fprintf (stdout, "Java VM destory.\n");    return 0;}

參考資料:

  1. UTF-8 and Unicode FAQ for Unix/Linuxs,http://www.cl.cam.ac.uk/~mgk25/unicode.html ,
    其中文翻譯見http://www.linuxforum.net/books/UTF-8-Unicode.html
  2. 深入剖析Java編程中的中文問題及建議最優解決方法,http://blog.csdn.net/abnerchai/archive/2004/04/28/18576.aspx
  3. 關於Java中文問題的幾條分析原則,http://www-900.ibm.com/developerWorks/cn/java/l-javachinese/index.shtml
  4. Java 編程技術中漢字問題的分析及解決,http://www-900.ibm.com/developerWorks/cn/java/java_chinese/index.shtml
  5. 深入剖析JSP和Servlet對中文的處理過程,http://blog.csdn.net/deuso/archive/2005/12/01/541511.aspx
  6. 寬字符標量L"xx"在VC6.0/7.0和GNU g++中的不同實現,http://blog.vckbase.com/smileonce/archive/2004/12/09/1972.html

XML Encoding,http://www.w3schools.com/xml/xml_encoding.asp

 

 

 

最新評論
// 編碼長度可以是1~3(據說理論上最長可以到6,不懂)。

直到Unicode 2.0,Unicode還是一個很簡單的編碼,每個字符16位——兩個字節
到了Unicode 3.0,爲了支持龐大的東亞象形文字,Unicode編碼空間增加爲0~10FFFF,提出代理對機制(用兩個w_char存儲一個圖形字符)來支持10000~10FFFF之間的編碼(這就是UTF-16的前身)
到了Unicode 4.0,直接定義爲31位空間——羣、面、行、格 四級,並提出多種編碼方案:UTF-7、UTF-8、UTF-16、UTF-32

UTF-8是變長編碼,首字節標示了長度值,其餘字節帶有6位數據。由於設計得很巧妙,存在冗餘位,所以可以糾錯。
其攜帶信息:
1 Byte:7bit(7位ASCII)
2 Byte:5 + 6*1 = 11bit
3 Byte:4 + 6*2 = 16bit(16位基本語義平面字符)
4 Byte:3 + 6*3 = 21bit(21位代理對)
5 Byte:2 + 6*4 = 26bit
6 Byte:1 + 6*5 = 31bit

 

 

 

Ch27:JNI的crash終於搞定

 

今天終於搞定困擾我一週的一個問題了。

我們的算法通過jni封裝,在java 調 用的時候總是隨機的crash掉,具體的位置在jvm裏面,應該可以肯定是jvm做垃圾回收的時候死掉的。但是並不知道是在回收哪塊內存出的問題,所以也 就無從知道死的具體原因了。我們的程序是在jni層創建了一些java對象,然後返回給java層,大體結構像下面代碼一樣,我只能基本判斷是我們的 jni層在創建對象的時候(也就是createInfo函數)出問題了,至於具體什麼問題,我也不清楚。
public class Test {
    public class Info {
        public int x;
        public int y;
        public Info() {
            x = 0;
            y = 0;
        }
    }
    
    public native Info createInfo();
   
    // ...
}

因 爲我對java不是很熟悉,所以只好一邊學,一邊弄。最初就是在local/glbal reference這些概念上下功夫,來回的讀jni的specification,也沒有發現自己的問題。後期又學着使用一些java的調試工具,比如 jhat啊,hpjmeter啊,但是仍然沒有什麼頭緒。上週一週,就在這個問題上不斷的嘗試,也沒結果。

今天終於發現了問題所在,其實 說來也很簡單。jni要 創建的那些返回對象,是作爲內部類定義的,所以在構造的時候需要傳一個外層類實例才能初始化。也就是說,雖然看上去Info類的構造函數是無參數的,但實 際上它是有一個隱含參數的,相當於Info(Test outer)。如果在java層構造這個對象,那麼outer參數會被自動傳入,但我們在jni層構造,就需要自己傳入這個參數了。如果沒有給出這個參 數,jni編譯運行都沒有問題,但實際上,它是用了一個未知的對象(就是在棧裏面的一個隨機值)來作爲這個outer參數的,所以當這個對象需要釋放的時 候(一般也就是在垃圾回收的時候)就會crash了。

現在想起來,其實這個問題我原來曾經有過一次小遭遇,那時我在使用有參數構造函數來 創建一個內部嵌套類,發現構造出來的對象值是錯掉的。其實就是因爲少傳了一個outer參數啊,但是當時我沒有去解決這個問題,而是繞過問題,採用構造函 數無參數,然後在創建之後,再手工給每個數據字段賦值的方法。這樣雖然表面上也達到了目的,但是隱藏了問題。

事實一次次的告訴我們,遇到問題一定要解決。就算你暫時繞過這個問題,但早晚它還會出來的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章