Chap 4:用javah產生一個.h文件... 17mysql
Chap5:jni教程(very very good) 19linux
Chap8:如何將java傳遞過來的jbyteArray轉換成C/C++中的BYTE數組... 47
Chap5:使用JNI技術實現java程序調用第三方dll(c/c++)文件的功能... 47
一、實例一:在jni中調用標準c中自帶的函數printf(): 57
三、實例3、在jni函數中訪問java類中的對象實例域... 58
五、實例五:在jni函數中調用java對象的方法... 60
六、實例六:在jni函數中調用java類的靜態方法... 61
Chap10:在 Windows 中實現 Java 本地方法... 66
Chap12:基本JNI調用技術(c/c++與java互調) 93
Chap13:JNI的c代碼中,另一個線程獲取 JNIEnv. 96
chap 14:當JNI遇到多線程--java對象如何被C++中的多個線程訪問?. 97
chap 17:使用 Java Native Interface 的最佳實踐... 106
4、 C/C++訪問Java成員變量和成員方法... 138
System.loadLibrary("HelloWorld"); 149
JNIEXPORT void JNICALL Java_HelloWorld_print (JNIEnv *, jobject); 150
1. 在通常的Java類中定義native方法... 156
注:chap1~13, JNI 函數編寫教程,其中chap5講得好;
Chap14~, JNIEnv和多線程,其中chap17講得好。
最近在公司裏作了一個手機的項目,須要Java程序在發送短信的時候和第三方的短信服務器鏈接。短信接口是用C++寫的。琢磨了三天,大體搞懂了JNI的主體部分。先將心得整理,但願各位朋友少走彎路。
首先引用一篇文章,介紹一個簡單的JNI的調用的過程。
JAVA以其跨平臺的特性深受人們喜好,而又正因爲它的跨平臺的目的,使得它和本地機器的各類內部聯繫變得不多,約束了它的功能。解決JAVA對本地操做的一種方法就是JNI。
JAVA經過JNI調用本地方法,而本地方法是以庫文件的形式存放的(在WINDOWS平臺上是DLL文件形式,在UNIX機器上是SO文件形式)。經過調用本地的庫文件的內部方法,使JAVA能夠實現和本地機器的緊密聯繫,調用系統級的各接口方法。
簡單介紹及應用以下:
1、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++程序調用來生成所需的庫文件。
2、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("
目錄
定義
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);
D:\Program Files\Java\jdk1.6.0_12\bin>javah
用法:javah [選項] <類>
其中 [選項] 包括:
-help 輸出此幫助消息並退出
-classpath <路徑> 用於裝入類的路徑
-bootclasspath <路徑> 用於裝入引導類的路徑
-d <目錄> 輸出目錄
-o <文件> 輸出文件(只能使用 -d 或 -o 中的一個)
-jni 生成 JNI樣式的頭文件(默認)
-version 輸出版本信息
-verbose 啓用詳細輸出
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寄存的路徑]
本文來源: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 程序員能夠先學習本教程的前兩部分,掌握以後再開始學習高級主題。
要運行本教程中的示例,您須要下列工具與組件:
雖然您能夠使用本身喜歡的任何開發環境,但咱們將在本教程中使用示例是用隨 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 代碼,這一變化很重要:
在兩種狀況下,在您的本機庫能與版本 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 代碼調用 C/C++ 的六個步驟
從 Java 程序調用 C 或 C ++ 代碼的過程由六個步驟組成。咱們將在下面幾頁中深刻討論每一個步驟,但仍是先讓咱們迅速地瀏覽一下它們。
步驟 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關鍵字便可),因此本機方法不能擁有方法主體。
如今,讓咱們逐行研究一下代碼:
注:基於 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 調用 C 或 C++ 本機代碼(雖然不簡單)是 Java 平臺中一種良好集成的功能。雖然 JNI 支持 C 和 C++,但 C++ 接口更清晰一些而且一般比 C 接口更可取。
正如您已經看到的,調用 C 或 C++ 本機代碼須要賦予函數特殊的名稱,並建立共享庫文件。當利用現有代碼庫時,更改代碼一般是不可取的。要避免這一點,在 C++ 中,一般建立代理代碼或代理類,它們有專門的 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開發。從下一節開始,環境和步驟再也不是闡述的重點,將再也不花費專門的篇 幅,例程中的關鍵點將成爲咱們關注的焦點。
到目前爲止,咱們尚未實現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;
}
第九、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;
}
程序的第九、10行讀者們應該不會陌生,在上一節的例 程中已經出現過,不一樣之處在於此次經過FindClass()函數在JNI上下文中獲取的是java.lang.Object的類型(Class),並將 其做爲基類開闢出一個長度爲5的對象數組,準備用來存放MailInfo對象。
程序的第十二、13行的目的則是建立一個jfieldID類型的變量,在JNI中,操做對 象屬性都是經過jfieldID進行的。第12行首先查找獲得MailInfo的類型(Class),而後基於這個jclass進一步獲取其名爲 topic的屬性,並將其賦予jfieldID變量。
程序的第1八、19行的目的是循環向對象數組中放入jobject對象。 SetObjectField()函數屬於首次使用,該函數的做用是向jobject的屬性賦值,而值的內容正是Java程序傳入的jstring變量 值。請注意在向對象屬性賦值和向對象數組中放入對象的過程當中,咱們使用了在函數頭部分定義的jobject類型的環境參數obj做爲中介。至此,JNI框 架固有的兩個環境入參env和obj,咱們都有涉及。
如何使用JNI的一些基本方法和過程在網上多如牛毛,若是你對Jni不甚瞭解,不知道Jni是作什麼的,如何創建一個基本的jni程序,或許能夠參考下面下面這些文章:
<利用VC++6.0實現JNI的最簡單的例子>
<JNI入門教程之HelloWorld篇>
<SUN JNI Tutorial>
這些資料的例子中,大多數只是輸入一些簡單的參數,獲取沒有參數。而在實際的使用過程當中,每每須要對參數進行處理轉換。才能夠被C/C++程序識別。好比咱們在C++中有一個結構(Struct)DiskInfo ,須要傳遞一個相似於DiskInfo *pDiskInfo的參數,相似於在C++這樣參數如何傳遞到Java中呢?下面咱們就來討論C++到Java中方法的一些常見參數的轉換:
1.定義Native Java類:
若是你習慣了使用JNI,你就不會以爲它難了。既然本地方法是由其餘語言實現的,它們在Java中沒有函數體。可是,全部本地代碼必須用本地關鍵詞native聲明,成爲Java類的成員。假設咱們在C++中有這麼一個結構,它用來描述硬盤信息:
//硬盤信息
struct {
char name[256];
int serial;
}DiskInfo;
那麼咱們須要在Java中定義一個類來與之匹配,聲明能夠寫成這樣:
class DiskInfo {
//名字
public String name;
//序列號
public int serial;
}
在這個類中,申明一些Native的本地方法,來測試方法參數的傳遞,分別定義了一些函數,用來傳遞結構或者結構數組,具體定義以下面代碼:
/**//****************** 定義本地方法 ********************/
//輸入經常使用的數值類型(Boolean,Byte,Char,Short,Int,Float,Double)
public native void displayParms(String showText, int i, boolean bl);
//調用一個靜態方法
public native int add(int a, int b);
//輸入一個數組
public native void setArray(boolean[] blList);
//返回一個字符串數組
public native String[] getStringArray();
//返回一個結構
public native DiskInfo getStruct();
//返回一個結構數組
public native DiskInfo[] getStructArray();
2.編譯生成C/C++頭文件
定義好了Java類以後,接下來就要寫本地代碼。本地方法符號提供一個知足約定的頭文件,使用Java工具Javah能夠很容易地建立它而不用手動去建立。你對Java的class文件使用javah命令,就會爲你生成一個對應的C/C++頭文件。
1)、在控制檯下進入工做路徑,本工程路徑爲:E:\work\java\workspace\JavaJni。
2)、運行javah 命令:javah -classpath E:\work\java\workspace\JavaJni com.sundy.jnidemo ChangeMethodFromJni
本文生成的C/C++頭文件名爲: com_sundy_jnidemo_ChangeMethodFromJni.h
3.在C/C++中實現本地方法
生成C/C++頭文件以後,你就須要寫頭文件對應的本地方法。注意:全部的本地方法的第一個參數都是指向JNIEnv結構的。這個結構是用來調用JNI函數的。第二個參數jclass的意義,要看方法是否是靜態的(static)或者實例(Instance)的。前者,jclass表明一個類對象的引用,然後者是被調用的方法所屬對象的引用。
返回值和參數類型根據等價約定映射到本地C/C++類型,如表JNI類型映射所示。有些類型,在本地代碼中可直接使用,而其餘類型只有經過JNI調用操做。
表A ※ JNI類型映射
Java類型 本地類型 描述
boolean jboolean C/C++8位整型
byte jbyte C/C++帶符號的8位整型
char jchar C/C++無符號的16位整型
short jshort C/C++帶符號的16位整型
int jint C/C++帶符號的32位整型
long jlong C/C++帶符號的64位整型e
float jfloat C/C++32位浮點型
double jdouble C/C++64位浮點型
Object jobject 任何Java對象,或者沒有對應java類型的對象
Class jclass Class對象
String jstring 字符串對象
Object[] jobjectArray 任何對象的數組
boolean[] jbooleanArray 布爾型數組
byte[] jbyteArray 比特型數組
char[] jcharArray 字符型數組
short[] jshortArray 短整型數組
int[] jintArray 整型數組
long[] jlongArray 長整型數組
float[] jfloatArray 浮點型數組
double[] jdoubleArray 雙浮點型數組
3.1 使用數組:
JNI經過JNIEnv提供的操做Java數組的功能。它提供了兩個函數:一個是操做java的簡單型數組的,另外一個是操做對象類型數組的。
由於速度的緣由,簡單類型的數組做爲指向本地類型的指針暴露給本地代碼。所以,它們能做爲常規的數組存取。這個指針是指向實際的Java數組或者Java數組的拷貝的指針。另外,數組的佈置保證匹配本地類型。
爲了存取Java簡單類型的數組,你就要要使用GetXXXArrayElements函數(見表B),XXX表明了數組的類型。這個函數把Java數組當作參數,返回一個指向對應的本地類型的數組的指針。
表B
函數 Java數組類型 本地類型
GetBooleanArrayElements jbooleanArray jboolean
GetByteArrayElements jbyteArray jbyte
GetCharArrayElements jcharArray jchar
GetShortArrayElements jshortArray jshort
GetIntArrayElements jintArray jint
GetLongArrayElements jlongArray jlong
GetFloatArrayElements jfloatArray jfloat
GetDoubleArrayElements jdoubleArray jdouble
JNI數組存取函數
當你對數組的存取完成後,要確保調用相應的ReleaseXXXArrayElements函數,參數是對應Java數組和GetXXXArrayElements返回的指針。若是必要的話,這個釋放函數會複製你作的任何變化(這樣它們就反射到java數組),而後釋放全部相關的資源。
爲了使用java對象的數組,你必須使用GetObjectArrayElement函數和SetObjectArrayElement函數,分別去get,set數組的元素。GetArrayLength函數會返回數組的長度。
3.2 使用對象
JNI提供的另一個功能是在本地代碼中使用Java對象。經過使用合適的JNI函數,你能夠建立Java對象,get、set 靜態(static)和實例(instance)的域,調用靜態(static)和實例(instance)函數。JNI經過ID識別域和方法,一個域或方法的ID是任何處理域和方法的函數的必須參數。
表C列出了用以獲得靜態(static)和實例(instance)的域與方法的JNI函數。每一個函數接受(做爲參數)域或方法的類,它們的名稱,符號和它們對應返回的jfieldID或jmethodID。
表C
函數 描述
GetFieldID 獲得一個實例的域的ID
GetStaticFieldID 獲得一個靜態的域的ID
GetMethodID 獲得一個實例的方法的ID
GetStaticMethodID 獲得一個靜態方法的ID
※域和方法的函數
若是你有了一個類的實例,它就能夠經過方法GetObjectClass獲得,或者若是你沒有這個類的實例,能夠經過FindClass獲得。符號是從域的類型或者方法的參數,返回值獲得字符串,如表D所示。
表D
Java類型 符號
boolean Z
byte B
char C
short S
int I
long L
float F
double D
void V
objects對象 Lfully-qualified-class-name;L類名
Arrays數組 [array-type [數組類型
methods方法 (argument-types)return-type(參數類型)返回類型
※肯定域和方法的符號
下面咱們來看看,若是經過使用數組和對象,從C++中的獲取到Java中的DiskInfo 類對象,並返回一個DiskInfo數組:
//返回一個結構數組,返回一個硬盤信息的結構數組
JNIEXPORT jobjectArray JNICALL
Java_com_sundy_jnidemo_ChangeMethodFromJni_getStructArray
(JNIEnv *env, jobject _obj)
{
//申明一個object數組
jobjectArray args = 0;
//數組大小
jsize len = 5;
//獲取object所屬類,通常爲java/lang/Object就能夠了
jclass objClass = (env)->FindClass("java/lang/Object");
//新建object數組
args = (env)->NewObjectArray(len, objClass, 0);
/**//* 下面爲獲取到Java中對應的實例類中的變量*/
//獲取Java中的實例類
jclass objectClass = (env)->FindClass("com/sundy/jnidemo/DiskInfo");
//獲取類中每個變量的定義
//名字
jfieldID str = (env)->GetFieldID(objectClass,"name","Ljava/lang/String;");
//序列號
jfieldID ival = (env)->GetFieldID(objectClass,"serial","I");
//給每個實例的變量付值,而且將實例做爲一個object,添加到objcet數組中
for(int i=0; i < len; i++ )
{
//給每個實例的變量付值
jstring jstr = WindowsTojstring(env,"個人磁盤名字是 D:");
//(env)->SetObjectField(_obj,str,(env)->NewStringUTF("my name is D:"));
(env)->SetObjectField(_obj,str,jstr);
(env)->SetShortField(_obj,ival,10);
//添加到objcet數組中
(env)->SetObjectArrayElement(args, i, _obj);
}
//返回object數組
return args;
}
所有的C/C++方法實現代碼以下:
/**//*
*
* 一縷陽光(sundy)版權全部,保留全部權利。
*/
/**//**
*
* TODO Jni 中一個從Java到C/C++參數傳遞測試類
*
* @author 劉正偉(sundy)
* @see http://www.cnweblog.com/sundy
* @see mailto:sundy26@126.com
* @version 1.0
* @since 2005-4-30
*
* 修改記錄:
*
* 日期 修改人 描述
* ----------------------------------------------------------------------------------------------
*
*
*
*/
// JniManage.cpp : 定義 DLL 應用程序的入口點。
//
package com.sundy.jnidemo;
#include "stdafx.h"
#include <stdio.h>
#include <math.h>
#include "jni.h"
#include "jni_md.h"
#include "./head/Base.h"
#include "head/wmi.h"
#include "head/com_sundy_jnidemo_ChangeMethodFromJni.h" //經過javah –jni javactransfer 生成
#include <stdio.h>
#include "stdlib.h"
#include "string.h"
#pragma comment (lib,"BaseInfo.lib")
#pragma comment (lib,"jvm.lib")
//硬盤信息
struct {
char name[256];
int serial;
}DiskInfo;
/**//*BOOL APIENTRY DllMain( HANDLE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
LPTSTR strName = new CHAR[256] ;
(*GetHostName)(strName);
printf("%s\n",strName);
delete [] strName;
return TRUE;
}*/
//將jstring類型轉換成windows類型
char* jstringToWindows( JNIEnv *env, jstring jstr );
//將windows類型轉換成jstring類型
jstring WindowsTojstring( JNIEnv* env, char* str );
//主函數
BOOL WINAPI DllMain(HANDLE hHandle, DWORD dwReason, LPVOID lpReserved)
{
return TRUE;
}
//輸入經常使用的數值類型 Boolean,Byte,Char,Short,Int,Float,Double
JNIEXPORT void JNICALL Java_com_sundy_jnidemo_ChangeMethodFromJni_displayParms
(JNIEnv *env, jobject obj, jstring s, jint i, jboolean b)
{
const char* szStr = (env)->GetStringUTFChars(s, 0 );
printf( "String = [%s]\n", szStr );
printf( "int = %d\n", i );
printf( "boolean = %s\n", (b==JNI_TRUE ? "true" : "false") );
(env)->ReleaseStringUTFChars(s, szStr );
}
//調用一個靜態方法,只有一個簡單類型輸出
JNIEXPORT jint JNICALL Java_com_sundy_jnidemo_ChangeMethodFromJni_add
(JNIEnv *env, jobject, jint a, jint b)
{
int rtn = (int)(a + b);
return (jint)rtn;
}
/**/////輸入一個數組,這裏輸入的是一個Boolean類型的數組
JNIEXPORT void JNICALL Java_com_sundy_jnidemo_ChangeMethodFromJni_setArray
(JNIEnv *env, jobject, jbooleanArray ba)
{
jboolean* pba = (env)->GetBooleanArrayElements(ba, 0 );
jsize len = (env)->GetArrayLength(ba);
int i=0;
// change even array elements
for( i=0; i < len; i+=2 )
{
pba[i] = JNI_FALSE;
printf( "boolean = %s\n", (pba[i]==JNI_TRUE ? "true" : "false") );
}
(env)->ReleaseBooleanArrayElements(ba, pba, 0 );
}
/**/////返回一個字符串數組
JNIEXPORT jobjectArray JNICALL Java_com_sundy_jnidemo_ChangeMethodFromJni_getStringArray
(JNIEnv *env, jobject)
{
jstring str;
jobjectArray args = 0;
jsize len = 5;
char* sa[] = { "Hello,", "world!", "JNI", "is", "fun" };
int i=0;
args = (env)->NewObjectArray(len,(env)->FindClass("java/lang/String"),0);
for( i=0; i < len; i++ )
{
str = (env)->NewStringUTF(sa[i] );
(env)->SetObjectArrayElement(args, i, str);
}
return args;
}
//返回一個結構,這裏返回一個硬盤信息的簡單結構類型
JNIEXPORT jobject JNICALL Java_com_sundy_jnidemo_ChangeMethodFromJni_getStruct
(JNIEnv *env, jobject obj)
{
/**//* 下面爲獲取到Java中對應的實例類中的變量*/
//獲取Java中的實例類
jclass objectClass = (env)->FindClass("com/sundy/jnidemo/DiskInfo");
//獲取類中每個變量的定義
//名字
jfieldID str = (env)->GetFieldID(objectClass,"name","Ljava/lang/String;");
//序列號
jfieldID ival = (env)->GetFieldID(objectClass,"serial","I");
//給每個實例的變量付值
(env)->SetObjectField(obj,str,(env)->NewStringUTF("my name is D:"));
(env)->SetShortField(obj,ival,10);
return obj;
}
//返回一個結構數組,返回一個硬盤信息的結構數組
JNIEXPORT jobjectArray JNICALL Java_com_sundy_jnidemo_ChangeMethodFromJni_getStructArray
(JNIEnv *env, jobject _obj)
{
//申明一個object數組
jobjectArray args = 0;
//數組大小
jsize len = 5;
//獲取object所屬類,通常爲ava/lang/Object就能夠了
jclass objClass = (env)->FindClass("java/lang/Object");
//新建object數組
args = (env)->NewObjectArray(len, objClass, 0);
/**//* 下面爲獲取到Java中對應的實例類中的變量*/
//獲取Java中的實例類
jclass objectClass = (env)->FindClass("com/sundy/jnidemo/DiskInfo");
//獲取類中每個變量的定義
//名字
jfieldID str = (env)->GetFieldID(objectClass,"name","Ljava/lang/String;");
//序列號
jfieldID ival = (env)->GetFieldID(objectClass,"serial","I");
//給每個實例的變量付值,而且將實例做爲一個object,添加到objcet數組中
for(int i=0; i < len; i++ )
{
//給每個實例的變量付值
jstring jstr = WindowsTojstring(env,"個人磁盤名字是 D:");
//(env)->SetObjectField(_obj,str,(env)->NewStringUTF("my name is D:"));
(env)->SetObjectField(_obj,str,jstr);
(env)->SetShortField(_obj,ival,10);
//添加到objcet數組中
(env)->SetObjectArrayElement(args, i, _obj);
}
//返回object數組
return args;
}
//將jstring類型轉換成windows類型
char* jstringToWindows( JNIEnv *env, jstring jstr )
{
int length = (env)->GetStringLength(jstr );
const jchar* jcstr = (env)->GetStringChars(jstr, 0 );
char* rtn = (char*)malloc( length*2+1 );
int size = 0;
size = WideCharToMultiByte( CP_ACP, 0, (LPCWSTR)jcstr, length, rtn,(length*2+1), NULL, NULL );
if( size <= 0 )
return NULL;
(env)->ReleaseStringChars(jstr, jcstr );
rtn[size] = 0;
return rtn;
}
//將windows類型轉換成jstring類型
jstring WindowsTojstring( JNIEnv* env, char* str )
{
jstring rtn = 0;
int slen = strlen(str);
unsigned short * buffer = 0;
if( slen == 0 )
rtn = (env)->NewStringUTF(str );
else
{
int length = MultiByteToWideChar( CP_ACP, 0, (LPCSTR)str, slen, NULL, 0 );
buffer = (unsigned short *)malloc( length*2 + 1 );
if( MultiByteToWideChar( CP_ACP, 0, (LPCSTR)str, slen, (LPWSTR)buffer, length ) >0 )
rtn = (env)->NewString( (jchar*)buffer, length );
}
if( buffer )
free( buffer );
return rtn;
}
Java 測試native代碼
這沒有什麼多說的,看代碼吧
//主測試程序
public static void main(String[] args) {
ChangeMethodFromJni changeJni = new ChangeMethodFromJni();
//輸入經常使用的數值類型(string int boolean)
System.out
.println("------------------輸入經常使用的數值類型(string int boolean)-----------");
changeJni.displayParms("Hello World!", 100, true);
//調用一個靜態方法
System.out.println("------------------調用一個靜態方法-----------");
int ret = changeJni.add(12, 20);
System.out.println("The result is: " + String.valueOf(ret));
//輸入一個數組
System.out.println("------------------輸入一個數組-----------");
boolean[] blList = new boolean[] { true, false, true };
changeJni.setArray(blList);
//返回一個字符串數組
System.out.println("------------------返回一個字符串數組-----------");
String[] strList = changeJni.getStringArray();
for (int i = 0; i < strList.length; i++) {
System.out.print(strList[i]);
}
System.out.println();
System.out.println("------------------返回一個結構-----------");
//返回一個結構
DiskInfo disk = changeJni.getStruct();
System.out.println("name:" + disk.name);
System.out.println("Serial:" + disk.serial);
//返回一個結構數組
System.out.println("------------------返回一個結構數組 -----------");
DiskInfo[] diskList = changeJni.getStructArray();
for (int i = 0; i < diskList.length; i++) {
System.out.println("name:" + diskList[i].name);
System.out.println("Serial:" + diskList[i].serial);
}
}
注:本程序在VS2003,eclipse (jse5.0) winxp sp2編譯經過
posted on 2005-05-02 20:22 sundy 閱讀(4406) 評論(21) 編輯 收藏 所屬分類: Java
評論
# re: Jni中C++和Java的參數傳遞 2005-05-22 14:35 張磊
請問若是想返回byte[]類型該怎麼作 回覆 更多評論
# re: Jni中C++和Java的參數傳遞 2005-05-23 08:37 sundy
由於:
byte[] jbyteArray 比特型數組
因此你將byte[] 做爲一個jbyteArray數組傳遞就能夠了
回覆 更多評論
# re: Jni中C++和Java的參數傳遞 2005-09-21 14:46 小影
請問若是我想把在C++裏面計算好的一個二維數組傳回給java程序接受,該怎麼寫代碼呢?我找了不少這方面的書和資料,都沒有關於傳遞二維數組的介紹,請您給予指導,多謝啦^_^ 回覆 更多評論
# re: Jni中C++和Java的參數傳遞 2005-09-21 17:47 sundy
我沒有直接傳遞過二維數組
但我想你能夠把試一試二維數組轉換成爲一個Hashmap的數組傳出來。
請參考"如何在Jni中傳遞出Hashmap的數組?"的一些代碼
回覆 更多評論
# re: Jni中C++和Java的參數傳遞 2005-12-26 16:32 wangjian
返回一個結構數組時,爲何每一個對象的數據都是同樣的?即5個Diskinfo的成員值都相同,能不能不相同? 回覆 更多評論
# re: Jni中C++和Java的參數傳遞 2005-12-26 16:55 wangjian
我把5個DiskInfo對象的成員serial分別設置爲一、二、三、四、5,但是傳遞到java後5個對象的serial成員值都是5,爲何這樣阿?盼回覆,多謝! 回覆 更多評論
# re: Jni中C++和Java的參數傳遞 2005-12-27 21:51 sundy
//給每個實例的變量付值,而且將實例做爲一個object,添加到objcet數組中
for(int i=0; i < len; i++ )
{
......
//添加到objcet數組中
(env)->SetObjectArrayElement(args, i, _obj);
}
你看看設置的_Obj是否是都是同一個??
回覆 更多評論
# re: Jni中C++和Java的參數傳遞 2005-12-28 13:32 wangjian
以下所示,我就是把你程序中(env)->SetShortField(_obj,ival,10)的參數10換成i,結果每一個對象都是對象的serial成員值都是4,請問怎樣實現多個不一樣對象的傳遞?
for(int i=0; i < len; i++ )
{
jstring jstr = WindowsTojstring(env,"個人磁盤名字是D:");
(env)->SetObjectField(_obj,str,jstr);
(env)->SetShortField(_obj,ival,i);
(env)->SetObjectArrayElement(args, i, _obj);
}
回覆 更多評論
# re: Jni中C++和Java的參數傳遞 2005-12-28 15:15 sundy
應該沒有問題的呀,
SetObjectArrayElement的時候,_obj是不一樣的嗎?
要不你將for循環改成:
jstring jstr = WindowsTojstring(env,"個人磁盤名字是C:");
(env)->SetObjectField(_obj,str,jstr);
(env)->SetShortField(_obj,ival,0);
(env)->SetObjectArrayElement(args, 0, _obj);
jstring jstr = WindowsTojstring(env,"個人磁盤名字是D:");
(env)->SetObjectField(_obj,str,jstr);
(env)->SetShortField(_obj,ival,1);
(env)->SetObjectArrayElement(args, 1, _obj);
看看對嗎? 回覆 更多評論
# re: Jni中C++和Java的參數傳遞 2005-12-29 20:42 wangjian
這樣做不對,不過我找到正確的方法了,要用構造函數生成新的對象。 回覆 更多評論
# re: Jni中C++和Java的參數傳遞 2006-01-17 11:07 luli
SQLRETURN SQLAllocHandle( SQLSMALLINT HandleType,
SQLHANDLE InputHandle,
SQLHANDLE * OutputHandlePtr
)
這是odbc api裏的一個函數 SQLHANDLE 是一個結構
c#裏的引用方式以下
[DllImport("ODBC32.dll")]
private static extern short SQLAllocHandle(short hType, IntPtr inputHandle, out IntPtr outputHandle);
但我不清楚 SQLHANDLE 結構具體怎麼構造的 所以我沒法用java類來模擬
我是菜鳥 望解答 謝過了 回覆 更多評論
# re: Jni中C++和Java的參數傳遞 2006-01-17 14:25 luli
忘了補充 SQLHANDLE InputHandle與SQLHANDLE * OutputHandlePtr
一個是結構 一個是結構指針 那我是否該以下模擬
class SQLHANDLE
{
}
public class test
{
SQLHANDLE a=new SQLHANDLE ();
public static void main(String args[]) {
int i=SQLAllocHandle( SQLSMALLINT HandleType, new SQLHANDLE(),a)
}
}
回覆 更多評論
# re: Jni中C++和Java的參數傳遞 2006-03-21 17:31 Hefe
WideCharToMultiByte();
MultiByteToWideChar();
請問這兩個函數實現什麼功能,請做者給出代碼,多謝!
回覆 更多評論
# re: Jni中C++和Java的參數傳遞 2006-03-22 08:47 sundy
@Hefe look here: http://www.google.com/search?hl=zh-CN&lr=lang_zh-CN&q=WideCharToMultiByte
回覆 更多評論
# re: Jni中C++和Java的參數傳遞 2006-03-28 17:40 dijk
要在c函數中調用java類的類成員的方法,好比調用JEditorPane類型成員的setText方法,該怎麼辦? 回覆 更多評論
# re: Jni中C++和Java的參數傳遞 2006-04-16 21:33 陳世雄
java中函數的處理中,對於對象類型(非基本類型int,long...)的輸入參數,函數中是能夠修改輸入參數的內容,函數執行完畢,修改仍然是有效的。
jni中 是否也是這樣呢?
回覆 更多評論
# re: Jni中C++和Java的參數傳遞 2006-04-18 17:50 王文波
你好:
向你請教一個問題:我想用jini來調用dll。我在jbuilder中新建的簡單的project調用jini運行正常。可是,我如今要對一個工程軟件進行二次開發,該軟件的
開發也使用jbuilder生成一個project,而後放在指定的路徑下就能夠了,該軟件在運行的時候會自動讀取該project。我在這個軟件二次開發的project中使用
jini,則老是報錯:unsatisfiedlinkError get()。其中get()方法名。請問該怎麼解決這個問題?
個人郵箱:zwj23232@tom.com 回覆 更多評論
# re: Jni中C++和Java的參數傳遞 2006-05-29 21:25 single
# re: Jni中C++和Java的參數傳遞 2005-12-29 20:42 wangjian
這樣做不對,不過我找到正確的方法了,要用構造函數生成新的對象。 回覆
---------------------------------------------------
能說說方法嗎? 回覆 更多評論
# re: Jni中C++和Java的參數傳遞 2006-08-29 11:34 yangyongfa
我正在作JNI,是在C++中調用JAVA類的方法,請問,我在JAVA類的方法中參數使 用的是byte[],而我在C++中是把一個文件讀成unsigned char*,請問怎麼能夠正確調用JAVA中的方法? 類中方法原型:public boolean AddHoyuBox2DB(String BoxName, byte[] BoxFile,byte[] WDHPic,int BoxFileBinLen, int WDHPicBinLen, String ParameterText, byte[] XXPic,int PicBinLen, byte[] SeriousPics,int SeriousPicsBinLen,String FileLenStr) ?
回覆 更多評論
# re: Jni中C++和Java的參數傳遞 2007-10-25 15:27 vampire
c的結構提裏寫有一個**p,指針的指針,在java中該如何封裝??? 回覆 更多評論
# re: Jni中C++和Java的參數傳遞 2007-12-11 13:13 Focus
@single
for(int i=0; i < len; i++ )
{
jobject objTemp = (env)->AllocObject(objectClass); //釋放問題??這個是否須要釋放不是很懂
//objectClass是函數上面給的 那個
jstring jstr = WindowsTojstring(env,"個人磁盤名字是D:");
(env)->SetObjectField(objTemp,str,jstr);
(env)->SetShortField(objTemp,ival,i);
(env)->SetObjectArrayElement(args, i, objTemp);
}
這個 能夠實現 數組 元素相同的問題
近遇到一個問題,請各位幫忙解決下:
如何將java傳遞過來的jbyteArray轉換成C/C++中的BYTE數組?BYTE爲unsigned char類型
這兩個我理解應該是相同的吧,強制類型轉換好像不啓做用,應該如何轉換呢?
該問題已經關閉: 問題已解決,以前代碼有問題 jbyte * arrayBody = env->GetByteArrayElements(data,0); jsize theArrayLengthJ = env->GetArrayLength(data); BYTE * starter = (BYTE *)arrayBody;
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文件,有興趣的能夠研究一下.但 是我用的時候不太順手,有不少規則限制,像本身寫程序時能夠隨意定義返回值,隨意轉換類型,用這個包的話這些都是不可能的了,因此個人項目還沒開始就把它 拋棄了.
做者:crazycow 發佈時間:2008-11-16 14:44:21.0
http://blog.chinaunix.NET/u1/38994/showart_1099528.html
1、概述:
在 這篇文章中將會簡單介紹如何編制一些簡單的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類對象
2、基本步驟:
在介紹這些例子以前,讓咱們先來看看編寫jni方法所須要的基本步驟,這些實例都是用c來實例來說解,至於c++的實例和c的實例區別不大,只要做稍微的修改便可,在文檔的末尾咱們將介紹這些內容:
一、要想定義jni方法,首先得要在java語言中對這一方法進行聲明(天然這一聲明過程要在類中進行)
聲明格式以下:
publicnativevoid print(); System.loadLibrary(「JNIExamples」); }
jni 函數用關鍵字native方法聲明。
二、對該類的源文件進行編譯使用javac命令,生成相應的.class文件。
三、用javah -jni爲函數生成一個在java調用和實際的c函數之間的轉換存根,該存根經過從虛擬機棧中取出參數信息,並將其傳遞給已編譯的C函數來實現轉換。
四、創建一個特殊的共享庫,並從該共享庫處處這個存根,在上面的例子中使用了System.loadLibrary,來加載libJNIExamples共享庫。
3、配置運行環境:
在 編寫一個簡單的jni函數以前咱們必須配置相應的運行環境。jdk的配置在這裏就不做介紹,這裏主要說的是庫的路徑。當調用 System.loadLibrary(..)時,編譯器會到咱們系統設置的庫路徑中尋找該庫。修改路徑的方法和修改任何環境變量的方法基本相同,只要在 /etc/bash.bashrc目錄下增長一行LD_LIBRARY_PATH=.:./lib:$(LD_LIBRARY_PATH)便可。也能夠通 過命令行export LD_LIBRARY_PATH=.:./lib:$(LD_LIBRARY_PATH)
4、運行實例分析:
下面以實例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 .
下面介紹的每一個實例實現的步驟也都是按着上述步驟執行的。因此介紹時只介紹實現的關鍵部分。
(源程序爲: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()函數沒有任何區別。
(源程序爲: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 值爲將要設置的值。
(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和;)是不可少的,其中(;)是表達是的終止符。其餘三個函數 和訪問對象數據域基本沒什麼區別。
(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(...)。
(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(...)。
其中的機制和實例五是同樣的。再次就不作過多的介紹。
(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語言中的基本數據類型的處理無異。
(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類對象的方法中的操做相同,就不做詳解。
(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(...)用於當該字符串使用完成後,將其進行垃圾回收。記住,當使用完字符串時必定不要忘記調用該函數。
(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(...)函數來釋放該數組指針。
(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參數的訪問。
(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++實例代碼.
級別: 初級
David WendtWebSphere 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 測試。可是,若是必須執行本地調用,則要考慮幾個準則:
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
對於調用 C 函數的 Java 方法,必須在 Java 類中聲明一個本地方法。在本部分的全部示例中,咱們將建立一個名爲 MyNative 的類,並逐步在其中加入新的功能。這強調了一種思想,即將本地方法集中在單個類中,以便將之後所需的移植工做減到最少。
在第一個示例中,咱們將三個經常使用參數類型傳遞給本地函數: 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 |
本例將說明如何在本地方法中實現返回代碼。
將這個方法添加到 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 做爲斜邊的值。
您可能在上面的示例中已經注意到,實例化的 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 對象。本文後面的示例將使用靜態方法。
本例說明如何傳遞數組型參數。本例使用一個基本類型,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 頭文件)。
本例將經過最經常使用的非基本類型,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的一篇論文。
最後一個示例說明如何在本地代碼中建立一個字符串數組並將它返回給 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 存入數組中的。
現 在您已經爲您的應用程序建立了一個本地 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 項目來調試本地方法的步驟。
當執行這個程序時,忽略「在 java.exe 中找不到任何調試信息」的消息。當調用本地方法時,在 C 代碼中設置的任何斷點將在適當的地方中止 Java 程序的執行。
JNI 方法和 C++
上面這些示例說明了如何在 C 源文件中使用 JNI 方法。若是使用 C++,則請將相應方法的格式從:
(*env)->JNIMethod( env, .... ); |
更改成:
env->JNIMethod( ... ); |
在 C++ 中,JNI 函數被看做是 JNIEnv 類的成員方法。
字符串和國家語言支持
本文中使用的技術用 UTF 方法來轉換字符串。使用這些方法只是爲了方便起見,若是應用程序須要國家語言支持 (NLS),則不能使用這些方法。有關在 Windows 和 NLS 環境中處理 Java 字符串正確方法,請參標題爲 NLS Strings and JNI 的一篇論文。
4.小結
本文提供的示例用最經常使用的數據類據(如 jint 和 jstring)說明了如何實現本地方法,並討論了 Windows 特定的幾個問題,如顯示字符串。本文提供的示例並未包括所有 JNI,JNI 還包括其餘參數類型,如 jfloat、jdouble、jshort、jbyte 和 jfieldID,以及用來處理這些類型的方法。有關這個主題的詳細信息,請參閱 Sun Microsystems 提供的 Java 本地接口規範。
5.關於做者: David Wendt 是 IBM WebSphere Studio 的一名程序員,該工做室位於北卡羅萊納州的 Research Triangle Park。能夠經過 wendt@us.ibm.com 與他聯繫。
編程技術 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的類型。諸如此類的問題,上面這個簡單的例子是回答不了的。在下一篇,我將解釋這些問題。
編程技術 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
字號: 大 中 小
和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數組,就不介紹了,對於其餘基本類型的數組,方法相似。
在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編程,須要仔細閱讀這個文檔。接下來的高級篇,也會討論更深刻的話題。
編程技術 2008-12-10 17:08 閱讀6 評論0
字號: 大 中 小
在本篇中,將會涉及關於JNI編程更深刻的話題,包括:在native方法中訪問Java類的域和方法,將Java中自定義的類做爲參數和返回值傳遞等等。瞭解這些內容,將會對JNI編程有更深刻的理解,寫出的程序也更清晰,易用性更好。
在前兩篇的例子中,都是將native方法放在main方法的Java類中,實際上,徹底能夠在任何類中定義native方法。這樣,對於外部來講,這個類和其餘的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。這些均可以在文檔中查到。
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編程的水平也更上一層樓。下面要討論的話題也是一個重要內容,至少若是沒有它,咱們的程序只能停留在演示階段,不具備實用價值。
在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編程的文章了。固然,限於篇幅,有些地方不可能講的很細。限於水平,也可能有一些錯誤。文中所用的代碼,都親自編譯執行過。但願這些內容能爲須要的朋友提供幫助,畢竟,分享是一種美德。
做者:劉冬 發文時間: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開發環境。
爲了讓本文如下部分的代碼可以正常工做,咱們必須創建一個完整的開發環境。首先須要下載並安裝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.");
}
}
本地代碼在調用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。
初始化了Java虛擬機後,就能夠開始調用Java的方法。要調用一個Java對象的方法必須通過幾個步驟:
有兩種途徑來獲取對象的類定義:
第一種是在已知類名的狀況下使用FindClass來查找對應的類。可是要注意類名並不一樣於平時寫的Java代碼,例如要獲得類jni.test.Demo的定義必須調用以下代碼:
jclass cls = (*env)->FindClass(env, "jni/test/Demo"); //把點號換成斜槓
而後經過對象直接獲得其所對應的類定義:
jclass cls = (*env)-> GetObjectClass(env, obj); //其中obj是要引用的對象,類型是jobject
咱們先來看看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);
*/
爲了調用對象的某個方法,能夠使用函數Call<TYPE>Method或者 CallStatic<TYPE>Method(訪問類的靜態方法),<TYPE>根據不一樣的返回類型而定。這些方法都是使用可 變參數的定義,若是訪問某個方法須要參數時,只須要把全部參數按照順序填寫到方法中就能夠。在講到構造函數的訪問時,將演示如何訪問帶參數的構造函數。
訪問類的屬性與訪問類的方法大致上是一致的,只不過是把方法變成屬性而已。
這一步與訪問類方法的第一步徹底相同,具體使用參看訪問類方法的第一步。
在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; */
其中第二行註釋的內容就是第四個參數要填的信息,這跟訪問類方法時是相同的。
有了屬性的定義要訪問屬性值就很容易了。有幾個方法用來讀取和設置類的屬性,它們是: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);
不少人剛剛接觸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.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]);
中文字符的處理每每是讓人比較頭疼的事情,特別是使用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;
}
因爲調用了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來獲取異常信息的內容。訪問這兩個方法同前面講到的如何訪問類的方法是相同的。
*/
}
有些時候須要使用多線程的方式來訪問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);
}
關於時間的話題是我在實際開發中遇到的一個問題。當要發佈使用了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的文件都刪除後從新打包。
一.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++時,遵循幾個步驟:
一、 用Java native關鍵字聲明方法爲本地方法(非Java語言實現)。
二、 編譯該聲明類,獲得XXX.class文件。
三、 用「javah –jni XXX」命令從該class文件生成C語言頭文件(XXX.h)。
四、 採用C語言實現該頭文件聲明的方法,將實現類編譯成庫文件(libXXX.so)。
五、 在Java程序中使用System.loadLibrary(XXX)加載該庫文件(須要設置-Djava.library.path環境變量指向該庫文件存放路徑)。
六、 便可象調用Java方法同樣,調用native方式聲明的本地方法。
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);
}
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);
}
引文地址: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指針到全局變量中,而後在另一個線程中使用它.幸運的是,能夠用
來將傳入的obj(局部變量)保存到gs_object中,從而其餘線程能夠使用這個gs_object(全局變量)來操縱這個java對象了.
示例代碼以下:
(1)java代碼:Test.java:
(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:
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
JNI 的發展
JNI 自從 JDK 1.1 發行版以來一直是 Java 平臺的一部分,而且在 JDK 1.2 發行版中獲得了擴展。JDK 1.0 發行版包含一個早期的本機方法接口,可是未明確分隔本機代碼和 Java 代碼。在這個接口中,本機代碼能夠直接進入 JVM 結構,所以沒法跨 JVM 實現、平臺或者甚至各類 JDK 版本進行移植。使用 JDK 1.0 模型升級含有大量本機代碼的應用程序,以及開發能支持多個 JVM 實現的本機代碼的開銷是極高的。
JDK 1.1 中引入的 JNI 支持:
有一個有趣的地方值得注意,一些較年輕的語言(如 PHP)在它們的本機代碼支持方面仍然在努力克服這些問題。
、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、
2009 年 7 月 27 日
Java™ 本機接口(Java Native Interface,JNI)是一個標準的 Java API,它支持將 Java 代碼與使用其餘編程語言編寫的代碼相集成。若是您但願利用已有的代碼資源,那麼能夠使用 JNI 做爲您工具包中的關鍵組件 —— 好比在面向服務架構(SOA)和基於雲的系統中。可是,若是在使用時未注意某些事項,則 JNI 會迅速致使應用程序性能低下且不穩定。本文將肯定 10 大 JNI 編程缺陷,提供避免這些缺陷的最佳實踐,並介紹可用於實現這些實踐的工具。
Java 環境和語言對於應用程序開發來講是很是安全和高效的。可是,一些應用程序卻須要執行純 Java 程序沒法完成的一些任務,好比:
ping
時,您可能須要 Internet Control Message Protocol (ICMP) 功能,但基本類庫並未提供它。JNI 容許您完成這些任務。它明確分開了 Java 代碼與本機代碼(C/C++)的執行,定義了一個清晰的 API 在這二者之間進行通訊。從很大程度上說,它避免了本機代碼對 JVM 的直接內存引用,從而確保本機代碼只需編寫一次,而且能夠跨不一樣的 JVM 實現或版本運行。
藉助 JNI,本機代碼能夠隨意與 Java 對象交互,獲取和設計字段值,以及調用方法,而不會像 Java 代碼中的相同功能那樣受到諸多限制。這種自由是一把雙刃劍:它犧牲 Java 代碼的安全性,換取了完成上述所列任務的能力。在您的應用程序中使用 JNI 提供了強大的、對機器資源(內存、I/O 等)的低級訪問,所以您不會像普通 Java 開發人員那樣受到安全網的保護。JNI 的靈活性和強大性帶來了一些編程實踐上的風險,好比致使性能較差、出現 bug 甚至程序崩潰。您必須格外留意應用程序中的代碼,並使用良好的實踐來保障應用程序的整體完整性。
本文介紹 JNI 用戶最常遇到的 10 大編碼和設計錯誤。其目標是幫助您認識到並避免它們,以便您能夠編寫安全、高效、性能出衆的 JNI 代碼。本文還將介紹一些用於在新代碼或已有代碼中查找這些問題的工具和技巧,並展現如何有效地應用它們。
JNI 編程缺陷能夠分爲兩類:
程序員在使用 JNI 時的 5 大性能缺陷以下:
要訪問 Java 對象的字段並調用它們的方法,本機代碼必須調用 FindClass()
、GetFieldID()
、GetMethodId()
和GetStaticMethodID()
。對於 GetFieldID()
、GetMethodID()
和 GetStaticMethodID()
, 爲特定類返回的 ID 不會在 JVM 進程的生存期內發生變化。可是,獲取字段或方法的調用有時會須要在 JVM 中完成大量工做,由於字段和方法多是從超類中繼承而來的,這會讓 JVM 向上遍歷類層次結構來找到它們。因爲 ID 對於特定類是相同的,所以您只須要查找一次,而後即可重複使用。一樣,查找類對象的開銷也很大,所以也應該緩存它們。
舉例來講,清單 1 展現了調用靜態方法所需的 JNI 代碼:
清單 1. 使用 JNI 調用靜態方法
int val=1; jmethodID method; jclass cls; cls = (*env)->FindClass(env, "com/ibm/example/TestClass"); if ((*env)->ExceptionCheck(env)) { return ERR_FIND_CLASS_FAILED; } method = (*env)->GetStaticMethodID(env, cls, "setInfo", "(I)V"); if ((*env)->ExceptionCheck(env)) { return ERR_GET_STATIC_METHOD_FAILED; } (*env)->CallStaticVoidMethod(env, cls, method,val); if ((*env)->ExceptionCheck(env)) { return ERR_CALL_STATIC_METHOD_FAILED; } |
當咱們每次但願調用方法時查找類和方法 ID 都會產生六個本機調用,而不是第一次緩存類和方法 ID 時須要的兩個調用。
緩存會對您應用程序的運行時形成顯著的影響。考慮下面兩個版本的方法,它們的做用是相同的。清單 2 使用了緩存的字段 ID:
清單 2. 使用緩存的字段 ID
int sumValues2(JNIEnv* env, jobject obj, jobject allValues){ jint avalue = (*env)->GetIntField(env, allValues, a); jint bvalue = (*env)->GetIntField(env, allValues, b); jint cvalue = (*env)->GetIntField(env, allValues, c); jint dvalue = (*env)->GetIntField(env, allValues, d); jint evalue = (*env)->GetIntField(env, allValues, e); jint fvalue = (*env)->GetIntField(env, allValues, f); return avalue + bvalue + cvalue + dvalue + evalue + fvalue; } |
|
清單 3 沒有使用緩存的字段 ID:
清單 3. 未緩存字段 ID
int sumValues2(JNIEnv* env, jobject obj, jobject allValues){ jclass cls = (*env)->GetObjectClass(env,allValues); jfieldID a = (*env)->GetFieldID(env, cls, "a", "I"); jfieldID b = (*env)->GetFieldID(env, cls, "b", "I"); jfieldID c = (*env)->GetFieldID(env, cls, "c", "I"); jfieldID d = (*env)->GetFieldID(env, cls, "d", "I"); jfieldID e = (*env)->GetFieldID(env, cls, "e", "I"); jfieldID f = (*env)->GetFieldID(env, cls, "f", "I"); jint avalue = (*env)->GetIntField(env, allValues, a); jint bvalue = (*env)->GetIntField(env, allValues, b); jint cvalue = (*env)->GetIntField(env, allValues, c); jint dvalue = (*env)->GetIntField(env, allValues, d); jint evalue = (*env)->GetIntField(env, allValues, e); jint fvalue = (*env)->GetIntField(env, allValues, f); return avalue + bvalue + cvalue + dvalue + evalue + fvalue } |
清單 2 用 3,572 ms 運行了 10,000,000 次。清單 3 用了 86,217 ms — 多花了 24 倍的時間。
JNI 在 Java 代碼和本機代碼之間提供了一個乾淨的接口。爲了維持這種分離,數組將做爲不透明的句柄傳遞,而且本機代碼必須回調 JVM 以便使用 set 和 get 調用操做數組元素。Java 規範讓 JVM 實現決定讓這些調用提供對數組的直接訪問,仍是返回一個數組副本。舉例來講,當數組通過優化而不須要連續存儲時,JVM 能夠返回一個副本。(參見 參考資料 獲取關於 JVM 的信息)。
隨後,這些調用能夠複製被操做的元素。舉例來講,若是您對含有 1,000 個元素的數組調用GetLongArrayElements()
,則會形成至少分配或複製 8,000 字節的數據(每一個 long
1,000 元素 * 8 字節)。當您隨後使用 ReleaseLongArrayElements()
更新數組的內容時,須要另外複製 8,000 字節的數據來更新數組。即便您使用較新的 GetPrimitiveArrayCritical()
,規範仍然准許 JVM 建立完整數組的副本。
|
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 倍的時間!
|
另外一方面,若是您最終要獲取數組中的全部元素,則使用GetTypeArrayRegion()
逐個獲取數組中的元素是得不償失的。要獲取最佳的性能,應該確保以儘量大的塊的來獲取和更新數組元素。若是您要迭代一個數組中的全部元素,則 清單 4 中這兩個getElement()
方法都不適用。比較好的方法是在一個調用中獲取大小合理的數組部分,而後再迭代全部這些元素,重複操做直到覆蓋整個數組。
在 調用某個方法時,您常常會在傳遞一個有多個字段的對象以及單獨傳遞字段之間作出選擇。在面向對象設計中,傳遞對象一般能提供較好的封裝,由於對象字段的變 化不須要改變方法簽名。可是,對於 JNI 來講,本機代碼必須經過一個或多個 JNI 調用返回到 JVM 以獲取須要的各個字段的值。這些額外的調用會帶來額外的開銷,由於從本機代碼過渡到 Java 代碼要比普通方法調用開銷更大。所以,對於 JNI 來講,本機代碼從傳遞進來的對象中訪問大量單獨字段時會致使性能下降。
考慮清單 5 中的兩個方法,第二個方法假定咱們緩存了字段 ID:
清單 5. 兩個方法版本
int sumValues(JNIEnv* env, jobject obj, jint a, jint b,jint c, jint d, jint e, jint f){ return a + b + c + d + e + f; } int sumValues2(JNIEnv* env, jobject obj, jobject allValues){ jint avalue = (*env)->GetIntField(env, allValues, a); jint bvalue = (*env)->GetIntField(env, allValues, b); jint cvalue = (*env)->GetIntField(env, allValues, c); jint dvalue = (*env)->GetIntField(env, allValues, d); jint evalue = (*env)->GetIntField(env, allValues, e); jint fvalue = (*env)->GetIntField(env, allValues, f); return avalue + bvalue + cvalue + dvalue + evalue + fvalue; } |
|
sumValues2()
方法須要 6 個 JNI 回調,而且運行 10,000,000 次須要 3,572 ms。其速度比 sumValues()
慢 6 倍,後者只須要 596 ms。經過傳遞 JNI 方法所需的數據,sumValues()
避免了大量的 JNI 開銷。
本 機代碼和 Java 代碼之間的界限是由開發人員定義的。界限的選定會對應用程序的整體性能形成顯著的影響。從 Java 代碼中調用本機代碼以及從本機代碼調用 Java 代碼的開銷比普通的 Java 方法調用高不少。此外,這種越界操做會干擾 JVM 優化代碼執行的能力。舉例來講,隨着 Java 代碼與本機代碼之間互操做的增長,實時編譯器的效率會隨之下降。通過測量,咱們發現從 Java 代碼調用本機代碼要比普通調用多花 5 倍的時間。一樣,從本機代碼中調用 Java 代碼也須要耗費大量的時間。
|
因 此,在設計 Java 代碼與本機代碼之間的界限時應該最大限度地減小二者之間的相互調用。消除沒必要要的越界調用,而且應該竭力在本機代碼中彌補越界調用形成的成本損失。最大限度地減小越界調用的一個關鍵因素是確保數據處於 Java/本機界限的正確一側。若是數據未在正確的一側,則另外一側訪問數據的需求則會持續發起越界調用。
舉例來講,若是咱們但願使用 JNI 爲某個串行端口提供接口,則能夠構造兩種不一樣的接口。第一個版本如清單 6 所示:
清單 6. 到串行端口的接口:版本 1
/** * Initializes the serial port and returns a java SerialPortConfig objects * that contains the hardware address for the serial port, and holds * information needed by the serial port such as the next buffer * to write data into * * @param env JNI env that can be used by the method * @param comPortName the name of the serial port * @returns SerialPortConfig object to be passed ot setSerialPortBit * and getSerialPortBit calls */ jobject initializeSerialPort(JNIEnv* env, jobject obj, jstring comPortName); /** * Sets a single bit in an 8 bit byte to be sent by the serial port * * @param env JNI env that can be used by the method * @param serialPortConfig object returned by initializeSerialPort * @param whichBit value from 1-8 indicating which bit to set * @param bitValue 0th bit contains bit value to be set */ void setSerialPortBit(JNIEnv* env, jobject obj, jobject serialPortConfig, jint whichBit, jint bitValue); /** * Gets a single bit in an 8 bit byte read from the serial port * * @param env JNI env that can be used by the method * @param serialPortConfig object returned by initializeSerialPort * @param whichBit value from 1-8 indicating which bit to read * @returns the bit read in the 0th bit of the jint */ jint getSerialPortBit(JNIEnv* env, jobject obj, jobject serialPortConfig, jint whichBit); /** * Read the next byte from the serial port * * @param env JNI env that can be used by the method */ void readNextByte(JNIEnv* env, jobject obj); /** * Send the next byte * * @param env JNI env that can be used by the method */ void sendNextByte(JNIEnv* env, jobject obj); |
在 清單 6 中,串行端口的全部配置數據都存儲在由 initializeSerialPort()
方法返回的 Java 對象中,而且將 Java 代碼徹底控制對硬件中各數據位的設置。清單 6 所示版本的一些問題會形成其性能差於清單 7 中的版本:
清單 7. 到串行端口的接口:版本 2
/** * Initializes the serial port and returns an opaque handle to a native * structure that contains the hardware address for the serial port * and holds information needed by the serial port such as * the next buffer to write data into * * @param env JNI env that can be used by the method * @param comPortName the name of the serial port * @returns opaque handle to be passed to setSerialPortByte and * getSerialPortByte calls */ jlong initializeSerialPort2(JNIEnv* env, jobject obj, jstring comPortName); /** * sends a byte on the serial port * * @param env JNI env that can be used by the method * @param serialPortConfig opaque handle for the serial port * @param byte the byte to be sent */ void sendSerialPortByte(JNIEnv* env, jobject obj, jlong serialPortConfig, jbyte byte); /** * Reads the next byte from the serial port * * @param env JNI env that can be used by the method * @param serialPortConfig opaque handle for the serial port * @returns the byte read from the serial port */ jbyte readSerialPortByte(JNIEnv* env, jobject obj, jlong serialPortConfig); |
|
最顯著的一個問題就是,清單 6 中的接口在設置或檢索每一個位,以及從串行端口讀取字節或者向串行端口寫入字節都須要一個 JNI 調用。這會致使讀取或寫入的每一個字節的 JNI 調用變成原來的 9 倍。第二個問題是,清單 6 將串行端口的配置信息存儲在 Java/本機界限的錯誤一側的某個 Java 對象上。咱們僅在本機側須要此配置數據;將它存儲在 Java 側會致使本機代碼向 Java 代碼發起大量回調以獲取/設置此配置信息。清單 7 將配置信息存儲在一個本機結構中(好比,一個 struct
),並向 Java 代碼返回了一個不透明的句柄,該句柄能夠在後續調用中返回。這意味着,當本機代碼正在運行時,它能夠直接訪問該結構,而不須要回調 Java 代碼獲取串行端口硬件地址或下一個可用的緩衝區等信息。所以,使用 清單 7 的實現的性能將大大改善。
JNI 函數返回的任何對象都會建立本地引用。舉例來講,當您調用 GetObjectArrayElement()
時,將返回對數組中對象的本地引用。考慮清單 8 中的代碼在運行一個很大的數組時會使用多少本地引用:
清單 8. 建立本地引用
void workOnArray(JNIEnv* env, jobject obj, jarray array){ jint i; jint count = (*env)->GetArrayLength(env, array); for (i=0; i < count; i++) { jobject element = (*env)->GetObjectArrayElement(env, array, i); if((*env)->ExceptionOccurred(env)) { break; } /* do something with array element */ } } |
每次調用 GetObjectArrayElement()
時都會爲元素建立一個本地引用,而且直到本機代碼運行完成時纔會釋放。數組越大,所建立的本地引用就越多。
|
這些本地引用會在本機方法終止時自動釋放。JNI 規範要求各本機代碼至少能建立 16 個本地引用。雖然這對許多方法來講都已經足夠了,但一些方法在其生存期中卻須要更多的本地引用。對於這種狀況,您應該刪除再也不須要的引用,方法是使用 JNIDeleteLocalRef()
調用,或者通知 JVM 您將使用更多的本地引用。
清單 9 向 清單 8 中的示例添加了一個 DeleteLocalRef()
調用,用於通知 JVM 本地引用已再也不須要,以及將可同時存在的本地引用的數量限制爲一個合理的數值,而與數組的大小無關:
清單 9. 添加 DeleteLocalRef()
void workOnArray(JNIEnv* env, jobject obj, jarray array){ jint i; jint count = (*env)->GetArrayLength(env, array); for (i=0; i < count; i++) { jobject element = (*env)->GetObjectArrayElement(env, array, i); if((*env)->ExceptionOccurred(env)) { break; } /* do something with array element */ (*env)->DeleteLocalRef(env, element); } } |
|
您能夠調用 JNI EnsureLocalCapacity()
方法來通知 JVM 您將使用超過 16 個本地引用。這將容許 JVM 優化對該本機代碼的本地引用的處理。若是沒法建立所需的本地引用,或者 JVM 採用的本地引用管理方法與所使用的本地引用數量之間不匹配形成了性能低下,則未成功通知 JVM 會致使 FatalError
。
5 大 JNI 正確性缺陷包括:
JNIEnv
執行本機代碼的線程使用 JNIEnv
發起 JNI 方法調用。可是,JNIEnv
並非僅僅用於分派所請求的方法。JNI 規範規定每一個 JNIEnv
對於線程來講都是本地的。JVM 能夠依賴於這一假設,將額外的線程本地信息存儲在 JNIEnv
中。一個線程使用另外一個線程中的 JNIEnv
會致使一些小 bug 和難以調試的崩潰問題。
|
線程能夠調用經過 JavaVM
對象使用 JNI 調用接口的 GetEnv()
來獲取 JNIEnv
。JavaVM
對象自己能夠經過使用 JNIEnv
方法調用 JNIGetJavaVM()
來獲取,而且能夠被緩存以及跨線程共享。緩存JavaVM
對象的副本將容許任何能訪問緩存對象的線程在必要時獲取對它本身的 JNIEnv
訪問。要實現最優性能,線程應該繞過 JNIEnv
,由於查找它有時會須要大量的工做。
本 機能調用的許多 JNI 方法都會引發與執行線程相關的異常。當 Java 代碼執行時,這些異常會形成執行流程發生變化,這樣便會自動調用異常處理代碼。當某個本機方法調用某個 JNI 方法時會出現異常,但檢測異常並採用適當措施的工做將由本機來完成。一個常見的 JNI 缺陷是調用 JNI 方法而未在調用完成後測試異常。這會形成代碼有大量漏洞以及程序崩潰。
舉例來講,考慮調用 GetFieldID()
的代碼,若是沒法找到所請求的字段,則會出現 NoSuchFieldError
。若是本機代碼繼續運行而未檢測異常,並使用它認爲應該返回的字段 ID,則會形成程序崩潰。舉例來講,若是 Java 類通過修改,致使 charField
字段再也不存在,則清單 10 中的代碼可能會形成程序崩潰 — 而不是拋出一個NoSuchFieldError
:
清單 10. 未能檢測異常
jclass objectClass; jfieldID fieldID; jchar result = 0; objectClass = (*env)->GetObjectClass(env, obj); fieldID = (*env)->GetFieldID(env, objectClass, "charField", "C"); result = (*env)->GetCharField(env, obj, fieldID); |
|
添加異常檢測代碼要比在過後嘗試調試崩潰簡單不少。常常,您只須要檢測是否出現了某個異常,若是是則當即返回 Java 代碼以便拋出異常。而後,使用常規的 Java 異常處理流程處理它或者顯示它。舉例來講,清單 11 將檢測異常:
清單 11. 檢測異常
jclass objectClass; jfieldID fieldID; jchar result = 0; objectClass = (*env)->GetObjectClass(env, obj); fieldID = (*env)->GetFieldID(env, objectClass, "charField", "C"); if((*env)->ExceptionOccurred(env)) { return; } result = (*env)->GetCharField(env, obj, fieldID); |
不檢測和清除異常會致使出現意外行爲。您能夠肯定如下代碼的問題嗎?
fieldID = (*env)->GetFieldID(env, objectClass, "charField", "C"); if (fieldID == NULL){ fieldID = (*env)->GetFieldID(env, objectClass,"charField", "D"); } return (*env)->GetIntField(env, obj, fieldID); |
問題在於,儘管代碼處理了初始 GetFieldID()
未返回字段 ID 的狀況,但它並未清除 此調用將設置的異常。所以,本機返回的結果會形成當即拋出一個異常。
許多 JNI 方法都經過返回值來指示調用成功與否。與未檢測異常類似,這也存在一個缺陷,即代碼未檢測返回值卻假定調用成功而繼續運行。對於大多數 JNI 方法來講,它們都設置了返回值和異常狀態,這樣應用程序更能夠經過檢測異常狀態或返回值來判斷方法運行正常與否。
|
您能夠肯定如下代碼的問題嗎?
clazz = (*env)->FindClass(env, "com/ibm/j9//HelloWorld"); method = (*env)->GetStaticMethodID(env, clazz, "main", "([Ljava/lang/String;)V"); (*env)->CallStaticVoidMethod(env, clazz, method, NULL); |
問題在於,若是未發現 HelloWorld
類,或者若是 main()
不存在,則本機將形成程序崩潰。
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; } |
|
在提供直接指向數組的指針的 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; } |
|
可是,咱們須要驗證在調用 processBufferHelper()
時能夠運行的全部代碼都沒有違反任何限制。這些限制適用於在 Get
和 Release
調用之間執行的全部代碼,不管它是否是本機的一部分。
本機能夠建立一些全局引用,以保證對象在再也不須要時纔會被垃圾收集器回收。常見的缺陷包括忘記刪除已建立的全局引用,或者徹底失去對它們的跟蹤。考慮一個本機建立了全局引用,可是未刪除它或將它存儲在某處:
lostGlobalRef(JNIEnv* env, jobject obj, jobject keepObj) { jobject gref = (*env)->NewGlobalRef(env, keepObj); } |
|
創 建全局引用時,JVM 會將它添加到一個禁止垃圾收集的對象列表中。當本機返回時,它不只會釋放全局引用,應用程序還沒法獲取引用以便稍後釋放它 — 所以,對象將會始終存在。不釋放全局引用會形成各類問題,不只由於它們會保持對象自己爲活動狀態,還由於它們會將經過該對象能接觸到的全部對象都保持爲活動狀態。在某些狀況下,這會顯著加重內存泄漏。
假設您編寫了一些新 JNI 代碼,或者繼承了別處的某些 JVI 代碼,如何才能確保避免了常見缺陷,或者在繼承代碼中發現它們?表 1 提供了一些肯定這些常見缺陷的技巧:
表 1. 肯定 JNI 編程缺陷的清單
|
未緩存 |
觸發數組副本 |
錯誤界限 |
過多回訪 |
使用大量本地引用 |
使用錯誤的 JNIEnv |
未檢測異常 |
未檢測返回值 |
未正確使用數組 |
未正確使用全局引用 |
規範驗證 |
|
|
|
|
|
X |
X |
|
X |
|
方法跟蹤 |
X |
X |
X |
X |
|
|
X |
|
X |
X |
轉儲 |
|
|
|
|
|
|
|
|
|
X |
|
|
|
|
|
X |
|
|
|
|
|
代碼審查 |
X |
X |
X |
X |
X |
X |
X |
X |
X |
X |
您能夠在開發週期的早期肯定許多常見缺陷,方法以下:
維持規範的限制列表並審查本機與列表的聽從性是一個很好的實踐,這能夠經過手動或自動代碼分析來完成。確保聽從性的工做可能會比調試因爲違背限制而出現的細小和間斷性故障輕鬆不少。下面提供了一個專門針對新開發代碼(或對您來講是新的)的規範順從性檢查列表:
JNIEnv
僅與與之相關的線程使用。GetXXXCritical()
的 ReleaseXXXCritical()
部分調用 JNI 方法。Get
/Release
調用在各 JNI 方法中都是相匹配的。IBM 的 JVM 實現包括開啓自動 JNI 檢測的選項,其代價是較慢的執行速度。與出色的代碼單元測試相結合,這是一種極爲強大的工具。您能夠運行應用程序或單元測試來執行聽從性檢查,或者肯定所遇到的 bug 是不是由本機引發的。除了執行上述規範聽從性檢查以外,它還能確保:
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 缺陷。
生成對已調用本機方法以及這些本機方法發起的 JNI 回調的跟蹤,這對肯定大量常見缺陷的根源是很是有用的。可肯定的問題包括:
GetFieldID()
和 GetMethodID()
調用 — 特別是,若是這些調用針對相同的字段和方法 — 表示字段和方法未被緩存。GetTypeArrayElements()
調用實例(而非 GetTypeArrayRegion()
)有時表示存在沒必要要的複製。GetFieldID()
調用,這種模式表示並未傳遞所需的參數,而是強制本機回訪完成工做所需的數據。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 回調。
-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 生成的跟蹤文件,並查看 警告。
運行中的 Java 進程生成的轉儲包含大量關於 JVM 狀態的信息。對於許多 JVM 來講,它們包括關於全局引用的信息。舉例來講,最新的 Sun JVM 在轉儲信息中包括這樣一行:
JNI global references: 73 |
經過生成先後轉儲,您能夠肯定是否建立了任何未正常釋放的全局引用。
您能夠在 UNIX® 環境中經過對 java
進程發起 kill -3
或 kill -QUIT
來請求轉儲。在 Windows® 上,使用 Ctrl+Break 組合鍵。
對於 IBM JVM,使用如下步驟獲取關於全局引用的信息:
-Xdump:system:events=user
添加到命令行。這樣,當您在 UNIX 系統上調用 kill -3
或者在 Windows 上按下 Ctrl+Break 時,JVM 便會生成轉儲。jextract -nozip core.XXX output.xml
,這將會將轉儲信息提取到可讀格式的 output.xml 中。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
的更多信息。
代 碼審查常常可用於肯定常見缺陷,而且能夠在各類級別上完成。繼承新代碼時,快速掃描能夠發現各類問題,從而避免稍後花費更多時間進行調試。在某些狀況下, 審查是肯定缺陷實例(好比未檢查返回值)的惟一方法。舉例來講,此代碼的問題可能能夠經過代碼審查輕鬆肯定,但卻很難經過調試來發現:
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
並不在應該在的線程上。
|
如今,您已經瞭解了 10 大 JNI 編程缺陷,以及一些用於在已有或新代碼中肯定它們的良好實踐。堅持應用這些實踐有助於提升 JNI 代碼的正確率,而且您的應用程序能夠實現所需的性能水平。
有 效集成已有代碼資源的能力對於面向對象架構(SOA)和基於雲的計算這兩種技術的成功相當重要。JNI 是一項很是重要的技術,用於將非 Java 舊有代碼和組件集成到基於 Java 的平臺中,充當 SOA 或基於雲的系統的基本元素。正確使用 JNI 能夠加速將這些組件轉變爲服務的過程,並容許您從現有投資中得到最大優點。
參考資料
學習
jextract
的更多信息。
本文爲在 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大小以及所採用的技術方案?
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程序。
以上步驟雖簡單,但有不少地方值得注意。若是一招不慎,可能形成滿盤皆輸。
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互傳參數須要本身在編程過程當中仔細摸索與體味。
咱們修改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函數爲:GettypeField()和GetStatictypeField();與之相對應的設置其值的函數爲SettypeField()和SetStatictypeField();在本例中,成員變量circleRadius聲明成int型,則對應的函數分別爲GetIntField()和SetIntField();
其實JNI API函數名是頗有規律的,從上面已窺全貌。得到成員方法的ID也是一樣的分類方法。具體爲GetMethodID()和GetStaticMethodID()。調用成員方法跟得到成員變量的值相相似,也根據其方法返回值的type不一樣而不一樣,分別爲CalltypeMethod()和CallStatictypeMethod()。對於返回值爲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()來顯示異常棧裏面的內容。
可能你們每天都在用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,釋放資源。
7、 附錄
利用JNI函數,咱們能夠從本地化方法的內部與JVM打交道。正如在前面的例子中所看到的那樣,每一個本地化方法中都會接收一個特殊的自變量做爲本身的第一個參數:JNIEnv――它是指向類型爲JNIEnv_的一個特殊JNI數據結構的指針。JNI數據結構的一個元素是指向由JVM生成的一個指針的數組;該數組的每一個元素都是指向一個JNI函數的指針。能夠從本地化方法的內部對JNI函數的調用。第二個參數會根據Java類中本地方法的定義不一樣而不一樣,若是是定義爲static方法,類型會是jclass,表示對特定Class對象的引用,若是是非static方法,類型是jobject,表示當前對象的引用,至關於」 this」。能夠說這兩個變量是本地化方法返回JAVA的大門。
注意:在本地化方法中生成的Dll不具有處處運行的特性,而具備」牽一髮而動全身」的特色。只要包名一改變,那麼你全部的工做就得從新作一遍。緣由就是當用javah生成頭文件時,函數名的生成規則爲Java[ _包名]_類名_方法名[ _函數簽名];當你的包名改變時,生成的函數名也跟着改變了,那麼你再次調用之前編寫的Dll時,會拋出異常。
8、 參考文獻
1. 《Java 編程思想》Bruce Eckel 機械工業出版社
2. 《Java2 核心技術卷2》(第6版)Cay S.Horstmann,Gary Cornell機械工業出版社
3. 《高級Java2 大學教程》(英文版) Harvey M.Deitel ,Paul J.Deitel,Sean E.Santry 電子工業出版社
4. 《windows 核心編程》Jeffrey Richter 機械工業出版社
6. sun公司文檔
如對本文有任何疑問和異議,歡迎與做者探討:normalnotebook@126.com
注:本文最初發表在2004年《開發高手》第12期上。
=====================================================================
利用jawin完成調用window中dll的調用 最近因爲項目的特殊需求,咱們必須在程序調用window的dll。 》至此在Editplus中調試Jawin/NJawin的例子,能夠經過。 而在Eclipse中有時還會出上面的錯誤:COMException : no jawin in java.library.path。 》調用 dll,dll 的方式不須要導出了,直接調用就能夠了,下面是下載的包中提供的一個例子: import org.jawin.ReturnFlags; /** public static void main(String[] args) { try { FuncPtr msgBox = new FuncPtr("USER32.DLL", "MessageBoxW");
msgBox.invoke_I(0, "Hello From a DLL", "From Jawin", 0, ReturnFlags.CHECK_NONE); } catch (Exception e) { e.printStackTrace(); } } } |
{
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);
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;
1.觀察者設計模式
Java 本地應用程序偵聽器
2.Java Invocation API
Invocation API 讓您能夠將 JVM 加載到一個本地應用程序中,而沒必要顯式地連接 JVM 源。 經過在 jvm.dll 中調用一個函數,能夠建立一個 JVM,jvm.dll 還將當前本地線程鏈接到 JVM。而後,您能夠在 JVM 中從本地線程調用全部 Java 方法。
4.多線程問題
本地方法接收 JNI 接口指針做爲一個參數。可是,一個想要將事件委託回其關聯 Java 代理的本地偵聽器沒有現成的 JNI 接口指針。一旦得到 JNI 接口指針,應該將其保存起來以便後續使用。
JNI 接口指針只在當前線程中有效。實現 JNI 的 JVM 能夠在 JNI 接口指針指向的區域中分配和存儲本地線程數據。這意味着您也須要以本地線程數據保存 JNI 接口指針。
JNI 接口指針能夠兩種方式得到:
5.環境設置
公共接口
IMouseDownListener 和 IEventSource 接口定義在 common.h 中。IMouseDownListener 只有一個方法:onMouseDown()。該方法接收鼠標單擊的屏幕位置。IEventSource 接口包含了 addMouseDownListener() 和 removeMouseDownListener() 方法,用於註冊和取消註冊偵聽器。
Java Invocation API 的幫助例程
有 7 個必需的經常使用工具方法可用於簡化 Java Invocation API 的使用,它們定義在 Jvm.h 中,在 Jvm.cpp 中實現:
其他方法都本身附帶有解釋:
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 添加到現有的本地應用程序。
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 JavaTM, Design Principles and Patterns, by Doug Lea (Addison-Wesley, 1997).
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.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 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.obj
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 MonitorExit
when the current thread does not own the monitor results in an error and causes an Illegal-MonitorStateException
to be raised. The above code contains a matched pair of MonitorEnter
and MonitorExit
calls, yet we still need to check for possible errors. Monitor operations may fail if, for example, the underlying thread implementation cannot allocate the resources necessary to perform the monitor operation.
MonitorEnter
and MonitorExit
work on jclass
, jstring
, and jarray
types, which are special kinds of jobject
references.
Remember to match a MonitorEnter
call with the appropriate number of MonitorExit
calls, especially in code that handles errors and exceptions:
if ((*env)->MonitorEnter(env, obj) != JNI_OK) ...;
if ((*env)->ExceptionOccurred(env)) {
... /* exception handling */
/* remember to call MonitorExit here */
if ((*env)->MonitorExit(env, obj) != JNI_OK) ...;
... /* Normal execution path.
if ((*env)->MonitorExit(env, obj) != JNI_OK) ...;
Failure to call MonitorExit
will most likely lead to deadlocks. By comparing the above C code segment with the code segment at the beginning of this section, you can appreciate how much easier it is to program with the Java programming language than with the JNI. Thus, it is preferable to express synchronization constructs in the Java programming language. If, for example, a static native method needs to enter the monitor associated with its defining class, you should define a static synchronized native method as opposed to performing JNI-level monitor synchronization in native code.
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
{
(*env)->CallVoidMethod(env, object, MID_Object_wait,
timeout);
void
{
(*env)->CallVoidMethod(env, object, MID_Object_notify);
void
{
(*env)->CallVoidMethod(env, object, MID_Object_notifyAll);
We assume that the method IDs for , , and 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 argument.Object.waitObject.notifyObject.notifyAlljobject
We explained earlier that a JNIEnv
pointer is only valid in its associated thread. This is generally not a problem for native methods because they receive the JNIEnv
pointer from the virtual machine as the first argument. Occasionally, however, it may be necessary for a piece of native code not called directly from the virtual machine to obtain the JNIEnv
interface pointer that belongs to the current thread. For example, the piece of native code may belong to a "callback" function called by the operating system, in which case the JNIEnv
pointer will probably not be available as an argument.
You can obtain the JNIEnv
pointer for the current thread by calling the AttachCurrentThread
function of the invocation interface:
JavaVM *jvm; /* already set */
f()
JNIEnv *env;
(*jvm)->AttachCurrentThread(jvm, (void **)&env, NULL);
... /* use env */
When the current thread is already attached to the virtual machine, returns the interface pointer that belongs to the current thread.Attach-Current-ThreadJNIEnv
There are many ways to obtain the JavaVM
pointer: by recording it when the virtual machine is created, by querying for the created virtual machines using JNI_GetCreatedJavaVMs
, by calling the JNI function GetJavaVM
inside a regular native method, or by defining a JNI_OnLoad
handler. Unlike the JNIEnv
pointer, the JavaVM
pointer remains valid across multiple threads so it can be cached in a global variable.
Java 2 SDK release 1.2 provides a new invocation interface function GetEnv
so that you can check whether the current thread is attached to the virtual machine, and, if so, to return the JNIEnv
pointer that belongs to the current thread. GetEnv
and AttachCurrentThread
are functionally equivalent if the current thread is already attached to the virtual machine.
Suppose that native code to be run in multiple threads accesses a global resource. Should the native code use JNI functions MonitorEnter
and MonitorExit
, or use the native thread synchronization primitives in the host environment (such as mutex_lock
on Solaris)? Similarly, if the native code needs to create a new thread, should it create a java.lang.Thread
object and perform a callback of Thread.start
through the JNI, or should it use the native thread creation primitive in the host environment (such as thr_create
on Solaris)?
The answer is that all of these approaches work if the Java virtual machine implementation supports a thread model that matches that used by the native code. The thread model dictates how the system implements essential thread operations such as scheduling, context switching, synchronization, and blocking in system calls. In a native thread model the operating system manages all the essential thread operations. In a user thread model, on the other hand, the application code implements the thread operations. For example, the "Green thread" model shipped with JDK and Java 2 SDK releases on Solaris uses the ANSI C functions setjmp
and longjmp
to implement context switches.
Many modern operating systems (such as Solaris and Win32) support a native thread model. Unfortunately, some operating systems still lack native thread support. Instead, there may be one or many user thread packages on these operating systems.
If you write application strictly in the Java programming language, you need not worry about the underlying thread model of the virtual machine implementation. The Java platform can be ported to any host environment that supports the required set of thread primitives. Most native and user thread packages provide the necessary thread primitives for implementing a Java virtual machine.
JNI programmers, on the other hand, must pay attention to thread models. The application using native code may not function properly if the Java virtual implementation and the native code have a different notion of threading and synchronization. For example, a native method could be blocked in a synchronization operation in its own thread model, but the Java virtual machine, running in a different thread model, may not be aware that the thread executing the native method is blocked. The application deadlocks because no other threads will be scheduled.
The thread models match if the native code uses the same thread model as the Java virtual machine implementation. If the Java virtual machine implementation uses native thread support, the native code can freely invoke thread-related primitives in the host environment. If the Java virtual machine implementation is based on a user thread package, the native code should either link with the same user thread package or rely on no thread operations at all. The latter may be harder to achieve than you think: most C library calls (such as I/O and memory allocation functions) perform thread synchronization underneath. Unless the native code performs pure computation and makes no library calls, it is likely to use thread primitives indirectly.
Most virtual machine implementations support only a particular thread model for JNI native code. Implementations that support native threads are the most flexible, hence native threads, when available, are typically preferred on a given host environment. Virtual machine implementations that rely on a particular user thread package may be severely limited as to the type of native code with which they can operate.
Some virtual machine implementations may support a number of different thread models. A more flexible type of virtual machine implementation may even allow you to provide a custom thread model implementation for virtual machine's internal use, thus ensuring that the virtual machine implementation can work with your native code. Before embarking on a project likely to require native code, you should consult the documentation that comes with your virtual machine implementation for thread model limitations.
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.
JNI_OnLoad
HandlerWhen System.loadLibrary
loads a native library, the virtual machine searches for the following exported entry in the native library:
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved);
You can invoke any JNI functions in an implementation of JNI_Onload
. A typical use of the JNI_OnLoad
handler is caching the JavaVM
pointer, class references, or method and field IDs, as shown in the following example:
JavaVM *cached_jvm;
jclass Class_C;
jmethodID MID_C_g;
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 */
}
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 */
if (MID_C_g == NULL) {
return JNI_ERR;
}
return JNI_VERSION_1_2;
The function first caches the pointer in the global variable . It then obtains the pointer by calling . It finally loads the class, caches the class reference, and computes the method ID for . The function returns (§12.4) on error and otherwise returns the version needed by the native library.JNI_OnLoadJavaVMcached_jvmJNIEnvGetEnvCC.gJNI_OnLoadJNI_ERRJNIEnvJNI_VERSION_1_2
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 HandlerJNI_OnUnload
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:
L
of the class C
that issues theSystem.loadLibrary
call.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.JNI_OnUnload
handler runs in a finalizer, and is either invoked synchroniously byjava.lang.System.runFinalization
or invoked asynchronously by the virtual machine.Here is the definition of a JNI_OnUnload
handler that cleans up the resources allocated by the JNI_OnLoad
handler in the last section:
JNIEXPORT void JNICALL
JNI_OnUnload(JavaVM *jvm, void *reserved)
JNIEnv *env;
if ((*jvm)->GetEnv(jvm, (void **)&env, JNI_VERSION_1_2)) {
return;
}
(*env)->DeleteWeakGlobalRef(env, Class_C);
return;
The function deletes the weak global reference to the class created in the handler. We need not delete the method ID because the virtual machine automatically reclaims the resources needed to represent 's method IDs when unloading its defining class .JNI_OnUnloadCJNI_OnLoadMID_C_gCC
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_OnUnload
handler is invoked.
Reflection generally refers to manipulating language-level constructs at runtime. For example, reflection allows you to discover at run time the name of arbitrary class objects and the set of fields and methods defined in the class. Reflection support is provided at the Java programming language level through the java.lang.reflect
package as well as some methods in the java.lang.Object
and java.lang.Class
classes. Although you can always call the corresponding Java API to carry out reflective operations, the JNI provides the following functions to make the frequent reflective operations from native code more efficient and convenient:
GetSuperclass
returns the superclass of a given class reference.IsAssignableFrom
checks whether instances of one class can be used when instances of another class are expected.GetObjectClass
returns the class of a 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.測量CPU和內存的佔用率經常是檢查Java應用程序是否達到特定性能的一個重要環節。儘管Java提 供了一些重要的方法用於測量其堆棧大小,可是使用標準的API是沒法測量本機Java進程的大小和 CPU當前的使用率的。這種測量的結果對於開發人員來講很是重要,它會提供應用程序的實時性能和效率信息。不幸的是,這樣的信息只能從操做系統直接獲取, 而這已經超出了Java標準的可移植能力。
一個主要的解決方案是使用操做系統自帶的本機系統調用,將數據經過JNI(Java Native Interface,Java本機接口)傳輸給Java.與調用各個平臺專用的外部命令(好比ps)並分析輸出結果不一樣,這種方案始終是一種很可靠的方 式。之前碰到這樣的問題時,我嘗試過使用Vladimir Roubtsov本身編寫的一個很小的庫,它只能在Win32系統下測量進程的CPU佔用率。可是,這個庫的能力十分有限,因此我須要某種方式可以同時在Windows和Solaris平臺上測量CPU和內存的佔用率。
我擴展了這個庫的能力,在Windows和Solaris 8平臺上實現了全部功能。新的庫可以測量純CPU使用時間、CPU使用的百分比、本機剩餘內存和已經使用的內存、Java進程的本機內存大小、系統信息 (好比操做系統的名字、補丁程序、硬件信息等)。它由三部分實現: Java通用的部分、Windows實現,以及Solaris實現。依靠操做系統的部分用純C語言實現。
編輯提示:本文能夠下載,全部的源代碼都以單獨的文本方式列出來了。
庫
因此,咱們將建立一個簡單的JNI庫,用於同C層裏的操做系統進行溝通,並把生成的數據提供給Java應用程序。首先,咱們要建立一個 SystemInformation類(列表A),爲測量和記錄CPU的使用率和其餘與系統相關的信息提供一個簡單的API.這個類是抽象的,由於它公開 的是一個徹底靜態的API.
列表A
<PRE>package com.vladium.utils; public abstract class SystemInformation // Custom exception class for throwing /** /** /** /** /** /** /** /** /** /** /** /** // protected: ............................................................. // package: ............................................................... // private: ............................................................... } // end of class |
最重要的方法是getProcessCPUTime(),它會返回當前進程的CPU時間(內核和用戶)的毫秒數,或者是PID在初始化期間被傳送給本機庫 的進程所消耗的CPU時間。返回的值應該根據系統的處理器的個數進行調整;下面咱們來看看這是如何在本機代碼裏作到的。咱們用修改符native來聲明這 個方法,這意味着它必須在JNI代碼裏實現。getProcessCPUTime()方法用來給CPU的使用率數據進行快照,方式是將通過測量的CPU時 間與當前的系統時間進行關聯。這樣的數據庫快照在makeCPUUsageSnapshot()方法裏進行,並輸出一個CPUUsageSnapshot 容器對象。這樣,測量CPU使用率的原理就很容易理解了:咱們按照給定的時間間隔進行兩次CPU快照,按1.0的分數來計算兩個時間點之間的CPU使用 率,方式是兩點所在的CPU時間差除以兩點所在系統時間差。下面就是getProcessCPUUsage()方法的工做原理:
public static double getProcessCPUUsage (final CPUUsageSnapshot start, final CPUUsageSnapshot end) |
只要咱們知道分母裏的快照之間的時間間隔,以及分子裏進程花在活動狀態上的CPU時間,咱們就會獲得所測量的時間間隔過程當中進程的CPU使用率;1.0就表明全部處理器100%的使用率。
事實上這種方式能夠用在全部版本的UNIX的ps工具和Windows的任務管理器上,這兩個都是用於監視特定進程的CPU使用率的程序。很顯然,時間間 隔越長,咱們獲得的結果就越平均、越不許確。可是最小時差應該被強制輸入getProcessCPUUsage()。這種限制的緣由是某些系統上的 System.currentTimeMillis()的解析度很低。Solaris 8操做系統提供了一個系統調用,用於從內核表裏直接得到CPU使用率。出於這個目的,咱們擁有的getProcessCPUPercentage()方法會以百分比的形式返回進程所使用的CPU時間。若是這個特性不被操做系統支持(好比在Windows下),那麼JNI庫就會根據咱們應用程序的設計返回一 個負值。
還有其餘一些本機聲明要在本機部分實現:
getCPUs()用來返回機器上處理器的個數
getMaxMem()用來返回系統上可用的最大物理內存
getFreeMem()用來返回系統上當前可用內存
getSysInfo()用來返回系統信息,包括一些硬件和操做系統的詳細信息
getMemoryUsage()用來返回分配給進程的空間,以KB爲單位(這些頁面文件可能在內存裏,也有可能不在內存裏)
getMemoryResident()用來返回當前進程駐留在內存裏的空間,以KB爲單位。
全部這些方法對於不一樣的Java開發人員來講經常都是很是有用的。爲了確保本機JNI庫被調入內存並在調用任何本機方法以前被初始化,這個庫被加載到一個靜態初始值裏:
static |
在初始化一個.dll(或者.so)庫以後,咱們能夠直接使用本機聲明的方法:
final SystemInformation.CPUUsageSnapshot m_prevSnapshot = |
如今讓咱們來分別看看針對Windows和Solaris的JNI本機實現。C頭文件silib.h(列表B)可以用JDK裏的Javah工具生成,或者手動編寫。
列表B
/* DO NOT EDIT THIS FILE - it is machine generated */ #ifndef _Included_com_vladium_utils_SystemInformation /* /* /* /* /* #ifdef __cplusplus |
Windows
首先咱們來看看Windows的實現(列表C)。
列表C
/* ------------------------------------------------------------------------- */ #include #include "com_vladium_utils_SystemInformation.h" static jint s_PID; #define INFO_BUFFER_SIZE 32768
/* _time.LowPart = time->dwLowDateTime; return _time.QuadPart; /* s_PID = _getpid (); GetSystemInfo (& systemInfo); return JNI_VERSION_1_2; JNIEXPORT void JNICALL if (!alreadyDetached && s_currentProcess!=NULL) { } /* /*
// Test for the specific product. if ( osvi.dwMajorVersion == 5 && osvi.dwMinorVersion == 1 ) if ( osvi.dwMajorVersion == 5 && osvi.dwMinorVersion == 0 ) if ( osvi.dwMajorVersion <= 4 ) // Test for specific product on Windows NT 4.0 SP6 and later. // Test for the server type. else if( osvi.dwMajorVersion == 5 && osvi.dwMinorVersion == 0 ) else // Windows NT 4.0 lRet = RegOpenKeyEx( HKEY_LOCAL_MACHINE, lRet = RegQueryValueEx( hKey, "ProductType", NULL, NULL, RegCloseKey( hKey ); if ( lstrcmpi( "WINNT", szProductType) == 0 ) sprintf(buf2, "%d.%d ", (int)osvi.dwMajorVersion, (int)osvi.dwMinorVersion ); // Display service pack (if any) and build number. if( osvi.dwMajorVersion == 4 && // Test for SP6 versus SP6a. RegCloseKey( hKey );
// Test for the Windows Me/98/95. if (osvi.dwMajorVersion == 4 && osvi.dwMinorVersion == 0) if (osvi.dwMajorVersion == 4 && osvi.dwMinorVersion == 10) if (osvi.dwMajorVersion == 4 && osvi.dwMinorVersion == 90) case VER_PLATFORM_WIN32s: strcat(buf,"Win32s "); next_label: strcat(buf," next_label_2: lRet = RegQueryValueEx( hKey, "ProcessorNameString", NULL, NULL, strcat(buf,")"); jstring retval = (*env)->NewStringUTF(env,buf);
/* printf("[CPUmon] Could not attach to native process. /* /* BOOL resultSuccessful = GetProcessTimes (s_currentProcess, & creationTime, & exitTime, & kernelTime, & userTime); printf("[CPUmon] An error occured while trying to get CPU time. fflush(stdout); return (jlong) ((fileTimeToInt64 (& kernelTime) + fileTimeToInt64 (& userTime)) / /* /*
/* BOOL resultSuccessful = GetProcessTimes (s_currentProcess, & creationTime, & exitTime, & kernelTime, & userTime); printf("[CPUmon] An error occured while trying to get CPU time. /* elapsedTime = fileTimeToInt64 (& nowTime) - fileTimeToInt64 (& creationTime); if (elapsedTime < MIN_ELAPSED_TIME) /* /* if ( GetProcessMemoryInfo( s_currentProcess, &pmc, sizeof(pmc)) ) /* if ( GetProcessMemoryInfo( s_currentProcess, &pmc, sizeof(pmc)) )
/* ------------------------------------------------------------------------- */ |
JNI裏有兩個特殊的函數——JNI_OnLoad和JNI_OnUnload,它們分別在加載和卸載庫的時候被調用。JNI_OnLoad在調用其餘任何方法以前被執行,並且可以很方便地用於初始化在這一進程的生命週期中沒有發生變化的變量,並用於協調JNI規範的版本。在默認狀況下,庫會測量它本身的進程的參數,可是經過調用systemInformation.setPid()方法它能夠從Java應用程序被重載。s_PID C變量用來保存PID,而s_currentProcess用來保存進程句柄(用於Windows的是HANDLE類型,而用於Solaris的是int 類型)。爲了讀取的一些參數,應該首先打開進程句柄,咱們須要在庫關閉使用的時候中止同一個進程句柄(一般它在JVM由於相同的緣由而關閉的時候發生)。 這就是JNI_OnUnload()函數起做用的地方。可是,JVM的一些實現事實上沒有調用JNI_OnUnload(),還有發生句柄會永遠打開的危險。爲了下降這種可能性,咱們應該在Java應用程序里加入一個明確調用detachProcess() C函數的關閉掛鉤。下面就是咱們加入關閉掛鉤的方法:
if (pid!=-1) { |
經過調用WinAPI裏的GetSystemInfo(),咱們還能夠得到關於中央處理器的一些信息。只要它是CPU的佔用率根據這個值來調節,測量進程最重要的值就是處理器的個數(s_numberOfProcessors)。SystemInformation.getSysInfo()的Win32 實現至關麻煩,由於在各個版本的Windows裏,關於操做系統的版本、補丁、服務包以及相關硬件等信息被以不一樣的方式保存。因此須要讀者來分析相關的源代碼和代碼中的註釋。下面就是Windows XP上代碼輸出的示例:
System.out.println("SysInfo: 」+SystemInformation.getSysInfo()): And the same code on Solaris will give: SysInfo: SunOS 5.8 sxav-dev Generic_108528-29 sun4u sparc |
爲了得到CPU上進程所佔用的總時間,咱們使用了WinAPI的函數GetProcessTimes.其餘的函數實現都很是簡單,直接調用WinAPI, 因此沒有什麼必要討論它們。列表D裏是用於Windows版本的GCC的make.bat文件,用來幫助讀者建立相關的。dll庫。
列表D
gcc -D_JNI_IMPLEMENTATION_ -Wl,——kill-at -IC:/jdk1.3.1_12/include -IC:/jdk1.3.1_12/include/win32 -shared C:/cpu_profile/src/native/com_vladium_utils_SystemInformation.c -o C:/cpu_profile/dll/silib.dll C:/MinGW/lib/libpsapi.a 這個庫的Solaris實現見列表E和列表F.這兩個都是C語言文件,應該被編譯到一個共享庫(.so)裏。用於編譯共享庫的幫助器make.sh見列表 G.全部基於Solaris系統的調用被移到列表F裏,這使得列表E就是一個JNI的簡單包裝程序。Solaris實現要比Win32實現更加複雜,要求 更多的臨時數據結構、內核和進程表。列出的代碼裏有更多的註釋。
列表E
/* ------------------------------------------------------------------------- */ #include #include "com_vladium_utils_SystemInformation.h" // Helper Solaris8-dependent external routines static jint s_PID; /* ------------------------------------------------------------------------- */ /* ......................................................................... */ /* /* use kstat to update all processor information */ return JNI_VERSION_1_2; /* /*
/* /* * Class: com_vladium_utils_SystemInformation * Method: detachProcess * Signature: ()I */ JNIEXPORT jint JNICALL Java_com_vladium_utils_SystemInformation_detachProcess (JNIEnv * env, jclass cls) { if (externalCPUmon && !alreadyDetached) { alreadyDetached = 1; /* /* /*
/* /*
/*
#undef MIN_ELAPSED_TIME /* ------------------------------------------------------------------------- */ #include #define _STRUCTURED_PROC 1
static struct nlist nlst[] = }; static kstat_ctl_t *kc = NULL; /* pagetok function is really a pointer to an appropriate function */ int pagetok_none(int size) { int pagetok_left(int size) { int pagetok_right(int size) { #define UPDKCID(nk,ok) void initKVM() { /* perform the kvm_open - suppress error here */ /* calculate pageshift value */ /* calculate an amount to shift to K values */ /* now determine which pageshift function is appropriate for the #define SI_LEN 512 char * sol_getSysInfo() { for (ks = kc->kc_chain; ks; cpu_ks[ncpu] = ks; /* return the number of cpus found */ unsigned long sol_getMaxMem() { unsigned long sol_getFreeMem() { // Returns the number of milliseconds (not nanoseconds and seconds) elapsed on processor // Returns percentage CPU by pid // Returns current space allocated for the process, in bytes. Those pages may or may not be in memory. // Returns current process space being resident in memory. /* |
列表G
#!/bin/sh |
在本文裏,我已告訴你如何編程測量Java進程的內存和CPU佔用率。固然用戶能夠經過查看Windows任務管理器或者ps的輸出來達到相同的目的,可是重要的一點是,用戶如今可以進行任何長期運行和/或自動的軟件性能測試,這對於開發一個分佈式或者可伸縮的,或者實時的性能關鍵的應用程序十分重要。加入獲取系統軟件和硬件信息的能力對於開發人員一樣十分有用。這個庫能夠在Win32和Solaris平臺上實現。可是,把它移植到其餘UNIX平臺上應該也不是問題,好比Linux.
本文主要參考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();
}
?
因爲工做關係,須要利用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中文亂碼問題在不少狀況下均可能發生:不一樣應用間,不一樣平臺間等等,但以上問題已有大量優秀的文章討論過,這裏不做深刻探討,詳見參考二、三、四、5。下面簡要總結一下:
當咱們使用默認編碼方式保存源文件時,文件內容其實是按照咱們的系統設定進行編碼保存的,這個設定值即file.encoding能夠經過下面的程序得到:
public class Encoding {
public static void main(String[] args) {
System.out.println(
System.getProperty("file.encoding")
);
}
}
javac在不指定encoding參數時,若是區域設定不正確,則可能形成編/解碼錯誤,這個問題在編譯一個從別的環境傳過來的文件時可能發生;
二、雖然在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; 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;}
參考資料:
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-七、UTF-八、UTF-1六、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
今天終於搞定困擾我一週的一個問題了。
咱們的算法經過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參數啊,可是當時我沒有去解決這個問題,而是繞過問題,採用構造函數無參數,而後在建立以後,再手工給每一個數據字段賦值的方法。這樣雖然表面上也達到了目的,可是隱藏了問題。 事實一次次的告訴咱們,遇到問題必定要解決。就算你暫時繞過這個問題,但遲早它還會出來的。