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