JNI(Java Native Interface
,
Java
本地接口
)
技術(shù)大家都不陌生,它可以幫助解決
Java
訪問底層硬件的局限和執(zhí)行效率的提高。關(guān)于
JNI
的開發(fā),大多數(shù)資料討論的都是如何用
C/C++
語言開發(fā)
JNI
,甚至于
JDK
也提供了一個
javah
工具來自動生成
C
語言程序框架。但是,對于廣大的
Delphi
程序員來說,難道就不能用自己喜愛的
Delphi
與
Java
互通消息了嗎?
通過對
javah
生成的
C
程序框架和
JDK
中的
jni.h
文件的分析,我們發(fā)現(xiàn),
Java
利用
JNI
訪問本地代碼的關(guān)鍵在于
jni.h
中定義的
JNINativeInterface_
這個結(jié)構(gòu)
(Struct)
,如果用
Delhpi
語言改寫它的定義,應(yīng)該也可以開發(fā)
JNI
的本地代碼。幸運的是,在網(wǎng)上有現(xiàn)成的代碼可以幫助你完成這個繁雜的工作,在
http://delphi-jedi.org
上提供了一個
jni.pas
文件,就是用
Delphi
語言重寫的
jni.h
。我們只需在自己的
Delphi
工程中加入
jni.pas
就可以方便地開發(fā)出基于
Delphi
語言的
JNI
本地代碼。
本文將利用
jni.pas
,討論用
Delphi
語言開發(fā)
JNI
本地代碼的基本方法。
先來看一個經(jīng)典的
HelloWorld
例子。編寫以下
Java
代碼:
class HelloWorld
{
? public native void displayHelloWorld();
? static
? {
??? System.loadLibrary("HelloWorldImpl");
? }
}
|
這段代碼聲明了一個本地方法
displayHelloWorld
,它沒有參數(shù),也沒有返回值,但是希望它能在屏幕上打印出“您好!中國。”字樣。這個任務(wù)我們打算交給了本地的
Delphi
來實現(xiàn)。同時,在這個類的靜態(tài)域中,用
System.loadLibrary()
方法裝載
HelloWorldImpl.dll
。注意,這里只需要給出文件名而不需要給出擴(kuò)展名
dll
。
這時候,如果在我們的
Java
程序中使用
HelloWorld
類的
displayHelloWorld
方法,系統(tǒng)將拋出一個
java.lang.UnsatisfiedLinkError
的錯誤,因為我們還沒有為它實現(xiàn)本地代碼。
下面再看一下在
Delphi
中的本地代碼的實現(xiàn)。新建一個
DLL
工程,工程名為
HelloWorldImpl
,輸入以下代碼:
Uses
? JNI;
procedure Java_HelloWorld_displayHelloWorld(PEnv: PJNIEnv; Obj: JObject);stdcall;
begin
? Writeln('
您好!中國。
');
end;
exports
? Java_HelloWorld_DisplayHelloWorld;
end.
|
這段代碼首先導(dǎo)入
jni.pas
單元。然后實現(xiàn)了一個叫
Java_HelloWorld_displayHelloWorld
的過程,這個過程的命名很有講究,它以
Java
開頭,用下劃線將
Java
類的包名、類名和方法名連起來。這個命名方法不能有誤,否則,
Java
類將無法將
nativ
方法與它對應(yīng)起來。同時,在
Win32
平臺上,此過程的調(diào)用方式只能聲明為
stdcall
。
雖然在
HelloWorld
類中聲明的本地方法沒有參數(shù),但在
Delphi
中實現(xiàn)的具體過程則帶有兩個參數(shù):
PEnv : PJNIEnv
和
Obj : JObject
。(這兩種類型都是在
jni.pas
中定義的)。其中,
PEnv
參數(shù)代表了
Jvm
環(huán)境,而
Obj
參數(shù)則代表調(diào)用此過程的
Java
對象。當(dāng)然,這兩個參數(shù),在我們這個簡單的例子中是不會用到的。因為我們編譯的是
dll
文件,所以在
exports
需要輸出這個方法。
編譯
Delphi
工程,生成
HelloWorldImp.dll
文件,放在運行時系統(tǒng)能夠找到的目錄,一般是當(dāng)前目錄下,
并編寫調(diào)用
HelloWorld
類的
Java
類如下:
class MainTest
{
? public static void main(String[] args)
? {
??? new HelloWorld().displayHelloWorld();
? }
}
|
運行它,如果控制臺輸出了“您好!中國。”,恭喜你,你已經(jīng)成功地用
Delphi
開發(fā)出第一個
JNI
應(yīng)用了。
接下來,我們稍稍提高一點,來研究一下參數(shù)的傳遞。還是
HelloWorld
,修改剛才寫的
displayHelloWorld
方法,讓顯示的字符串由
Java
類動態(tài)確定。新的
displayHelloWorld
方法的
Java
代碼如下:
public native void displayHelloWorld(String str);
|
修改
Delphi
的代碼,這回用到了過程的第一個固有參數(shù)
PEnv
,如下:
procedure Java_HelloWorld_displayHelloWorld(PEnv: PJNIEnv; Obj: JObject; str: JString); stdcall;
var
? JVM: TJNIEnv;
begin
? JVM := TJNIEnv.Create(PEnv);
? Writeln(JVM.UnicodeJStringToString(str));
? JVM.Free;
end;
|
在該過程的參數(shù)表中我們增加了一個參數(shù)
str : JString
,這個
str
就負(fù)責(zé)接收來自
HelloWorld
傳入的
str
實參。注意實現(xiàn)代碼的不同,因為使用了參數(shù),就涉及到參數(shù)的數(shù)據(jù)類型之間的轉(zhuǎn)換。從
Java
程序傳過來的
Java
的
String
對象現(xiàn)在成了特殊的
JString
類型,而
JString
在
Delphi
中是不可以直接使用的。需要借助
TJNIEnv
提供的
UnicodeJStringToString()
方法來轉(zhuǎn)換成
Delphi
能識別的
string
類型。所以,需要構(gòu)造出
TJNIEnv
的實例對象,使用它的方法(
TJNIEnv
提供了眾多的方法,這里只使用了它最基本最常用的一個方法),最后,記得要釋放它。對于基本數(shù)據(jù)類型的參數(shù),從
Java
傳到
Delphi
中并在
Delphi
中使用的步驟就是這么簡單。
我們再提高一點點難度,構(gòu)建一個自定義類
Book
,并把它的實例對象作為參數(shù)傳入
Delphi
,研究一下在本地代碼中如何訪問對象參數(shù)的公共字段。
首先,定義一個簡單的
Java
類
Book
,為了把問題弄得稍微復(fù)雜一點,我們在
Book
中增加了一個
java.util.Date
類型的字段,代碼如下:
public class Book
{
? public String title;? //
標(biāo)題
? public double price; //
價格
? public Date pdate;? //
購買日期
}
|
同樣,在
HelloWorld
類中增加一個本地方法
displayBookInfo
,代碼如下:
public native void displayBookInfo(Book b);
|
Delphi
的代碼相對于上面幾個例子來說,顯得復(fù)雜了一點,先看一下代碼:
procedure Java_HelloWorld_displayBookInfo(PEnv: PJNIEnv; Obj: JObject; b:JObject); stdcall;
var
?JVM: TJNIEnv;
?c,c2: JClass;
?fid:JFieldID;
mid:JMethodID;
title,datestr:string;
price:double;
pdate:JObject;
begin
? JVM := TJNIEnv.Create(PEnv);
? c:=JVM.GetObjectClass(b);
? fid:=JVM.GetFieldID(c,'title','Ljava/lang/String;');
? title:=JVM.UnicodeJStringToString(JVM.GetObjectField(b,fid));
? fid:=JVM.GetFieldID(c,'price','D');
? price:=JVM.GetDoubleField(b,fid);
? fid:=JVM.GetFieldID(c,'pdate','Ljava/util/Date;');
? pdate:=JVM.GetObjectField(b,fid);
? c2:=JVM.GetObjectClass(pdate);
? mid:=JVM.GetMethodID(c2,'toString','()Ljava/lang/String;');
? datestr:=JVM.JStringToString(JVM.CallObjectMethodA(pdate,mid,nil));
?
? WriteLn(Format('%s? %f ?%s',[title,price,datestr]));
?
? JVM.Free;
end;
|
參數(shù)
b:JObject
就是傳入的
Book
對象。先調(diào)用
GetObjectClass
方法,根據(jù)
b
對象獲得它所屬的類
c
,然后調(diào)用
GetFieldID
方法從
?
中獲取一個叫做
title
的屬性的字段
ID
,一定要傳入正確的類型簽名。然后通過
GetObjectField
方法就可以根據(jù)得到的字段
ID
從對象中得到字段的值。注意這里的次序:我們得到傳入的對象參數(shù)
(Object)
,就要先得到它的類
(Class)
,這樣既有了對象實例,又有了類,以后就從類中得到字段
ID
,根據(jù)字段
ID
從對象中得到字段值。對于類的靜態(tài)字段,則可以直接從類中獲取它的值而不需要通過對象。
如果要調(diào)用對象的方法,操作步驟也基本類似,也需要從類中獲取方法
ID
,再執(zhí)行對象的相應(yīng)方法。在本例中,因為我們增加了一個
java.util.Date
類型的字段,要訪問這樣的字段,也只能先把它做為
JObject
讀入,再以同樣的方法進(jìn)一步去訪問它的成員(屬性或方法)。本例中演示了如何訪問
Date
對象的成員方法
toString
。
要正確地訪問類對象的成員屬性(字段)及成員方法,最重要的一點是一定要給出正確的簽名,在
Java
中對于數(shù)據(jù)類型和方法的簽名有如下的約定:
數(shù)據(jù)類型
/
方法
|
簽名
|
byte
|
B
|
char
|
C
|
double
|
D
|
float
|
F
|
int
|
I
|
long
|
J (
注意:是
J
不是
L)
|
short
|
S
|
void
|
V
|
boolean
|
Z
(注意:是
Z
不是
B
)
|
類類型
|
L
跟完整類名,如
Ljava/lang/String;
(注意:以
L
開頭,要包括包名,以斜杠分隔,最后有一個分號作為類型表達(dá)式的結(jié)束)
|
數(shù)組
type[]
|
[type
,例如
float[]
的簽名就是
[float
,如果是二維數(shù)組,如
float[][]
,則簽名為
[[float
,(注意:這里是兩個
[
符號)。
|
方法
|
(
參數(shù)類型簽名
)
返回值類型簽名,例如方法:
float fun(int a,int b)
,它的簽名為
(II)F
,
(
注意:兩個
I
之間沒有逗號!
)
,而對于方法
String toString()
,則是
()Ljava/lang/String;
。
|
通過上面的例子,我們了解了訪問對象參數(shù)的成員屬性或方法的基本步驟和多個
Get
方法的使用。
TJNIEnv
同時提供了多個
Set
方法,可以修改傳入的對象參數(shù)的字段值,因為
Java
對象參數(shù)都是以傳址的方式進(jìn)行傳遞的,所以修改的結(jié)果可以在
Java
程序中得到反映。
TJNIEnv
提供的
Get/Set
方法,都需要兩個基本參數(shù):對象實例(
JObject
類型)和字段
ID
(
JField
類型),就可以根據(jù)提供的對象和字段
ID
來獲取或設(shè)置這個對象的這個字段的值。
現(xiàn)在我們了解了在
Delphi
代碼中使用以及修改
Java
對象的操作步驟。進(jìn)一步,如果需要在
Delphi
中從無到有地創(chuàng)建一個新的
Java
對象,可以嗎?再來看一個例子,在
Delphi
中創(chuàng)建
Java
類的實例,操作方法其實也非常簡單。
先在
Java
代碼中增加一個本地方法,如下:
?public native Book findBook(String t);
|
然后,修改
Delphi
代碼,增加一個函數(shù)(因為有返回值,所以不再是過程而是函數(shù)了):
function Java_HelloWorld_findBook(PEnv: PJNIEnv; Obj: JObject; t:JString):JObject; stdcall;
var
?JVM: TJNIEnv;
?c: JClass;
?fid:JFieldID;
?b:JObject;
?mid:JMethodID;
begin
? JVM := TJNIEnv.Create(PEnv);
?
? c:=JVM.FindClass('Book');
? mid:=JVM.GetMethodID(c,'<init>','()V');
? b:=JVM.NewObjectV(c,mid,nil);
? fid:=JVM.GetFieldID(c,'title','Ljava/lang/String;');
? JVM.SetObjectField(b,fid,t);
? fid:=JVM.GetFieldID(c,'price','D');
? JVM.SetDoubleField(b,fid,99.8);
? Result:=b;
?
? JVM.Free;
end;
|
這里先用
FindClass
方法根據(jù)類名查找到類,然后獲取構(gòu)造函數(shù)的方法
ID
,構(gòu)造函數(shù)名稱固定為“
<init>
”,注意簽名為“
()V
”說明使用了
Book
類的一個空的構(gòu)造函數(shù)。然后就是使用方法
NewObjectV
根據(jù)類和構(gòu)造函數(shù)的方法
ID
來創(chuàng)建類的實例。創(chuàng)建了類實例,再對它進(jìn)行操作就與前面的例子沒有什么兩樣了。對于非空的構(gòu)造函數(shù),則略為復(fù)雜一點。需要設(shè)置它的參數(shù)表。還是上面的例子,在
Book
類中增加一個非空構(gòu)造函數(shù):
public Book(Strint t,double p){
?this.title=t;
this.price=p;
}
|
在
Delphi
代碼中,
findBook
函數(shù)修改獲取方法
ID
的代碼如下:
mid:=JVM.GetMethodID(c,'<init>','(Ljava/lang/String;D)V');
|
構(gòu)造函數(shù)名稱仍是“
<init>
”,方法簽名表示它有兩個參數(shù),分別是
String
和
double
。然后就是參數(shù)的傳入了,在
Delphi
調(diào)用
Java
對象的方法如果需要傳入?yún)?shù),都需要構(gòu)造出一個參數(shù)數(shù)組。在變量聲明中加上:
args : array[0..1] of JValue;
|
注意!參數(shù)都是
JValue
類型,不管它是基本數(shù)據(jù)類型還是對象,都作為
JValue
的數(shù)組來處理。在代碼實現(xiàn)中為參數(shù)設(shè)置值,并將數(shù)組的地址作為參數(shù)傳給
NewObjectA
方法:
? args[0].l:=t; // t
是傳入的
JString
參數(shù)
? args[1].d:=9.8;
?
? b:=JVM.NewObjectA(c,mid,@args);
|
為
JValue
類型的數(shù)據(jù)設(shè)置值的語句有點特殊,是吧?我們打開
jni.pas
,查看一下
JValue
的定義,原來它是一個
packed record
,已經(jīng)包括了多種數(shù)據(jù)類型,
JValue
的定義如下:
? JValue = packed record
? case Integer of
??? 0: (z: JBoolean);
??? 1: (b: JByte?? );
??? 2: (c: JChar?? );
??? 3: (s: JShort? );
??? 4: (i: JInt??? );
??? 5: (j: JLong?? );
??? 6: (f: JFloat? );
??? 7: (d: JDouble );
??? 8: (l: JObject );
? end;
|
下面再來看一下錯誤處理,在調(diào)試前面的例子中,大家也許看到了一旦在
Delphi
的執(zhí)行過程中發(fā)生了錯誤,控制臺就會輸出一大堆錯誤信息,如果想要屏蔽這些信息,也就是說希望在
Delphi
中捕獲錯誤并直接處理它,應(yīng)該怎么做?也很簡單,在
TJNIEnv
中提供了兩個方法可以方便地處理在訪問
Java
對象時發(fā)生的錯誤。
var
… …
ae:JThrowable;
begin
… …
ae:=JVM.ExceptionOccurred;
? if ( ae<>nil ) then
?? begin
??? Writeln(Format('Exception handled in Main.cpp: %d', [longword(ae)]));
??? JVM.ExceptionDescribe;
??? JVM.ExceptionClear;
?? end;
… …
|
用方法
ExceptionOccurred
可以捕獲
Java
拋出的錯誤,并存入
JThrowable
類型的變量中。用
ExceptionDescribe
可以顯示出
Java
的錯誤信息,而
ExceptionClear
顯然就是清除錯誤,讓它不再被拋出。
至此,我們已經(jīng)把從
Java
代碼通過
JNI
技術(shù)訪問
Delphi
本地代碼的步驟做了初步的探討。在
jni.pas
中也提供了從
Delphi
中打開
Java
虛擬機(jī)執(zhí)行
Java
代碼的方法,有興趣的讀者不妨自己研究一下。
posted on 2006-12-19 05:41
壞男孩 閱讀(1295)
評論(1) 編輯 收藏 所屬分類:
java命令學(xué)習(xí)