JNI(Java Native Interface , Java 本地接口 ) 技術(shù)大家都不陌生,它可以幫助解決 Java 訪問底層硬件的局限和執(zhí)行效率的提高。關(guān)于 JNI 的開發(fā),大多數(shù)資料討論的都是如何用 C/C++ 語言開發(fā) JNI ,甚至于 JDK 也提供了一個(gè) javah 工具來自動(dòng)生成 C 語言程序框架。但是,對(duì)于廣大的 Delphi 程序員來說,難道就不能用自己喜愛的 Delphi 與 Java 互通消息了嗎?
通過對(duì)
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
方法與它對(duì)應(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
對(duì)象。當(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ù)表中我們?cè)黾恿艘粋€(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 對(duì)象現(xiàn)在成了特殊的 JString 類型,而 JString 在 Delphi 中是不可以直接使用的。需要借助 TJNIEnv 提供的 UnicodeJStringToString() 方法來轉(zhuǎn)換成 Delphi 能識(shí)別的 string 類型。所以,需要構(gòu)造出 TJNIEnv 的實(shí)例對(duì)象,使用它的方法( TJNIEnv 提供了眾多的方法,這里只使用了它最基本最常用的一個(gè)方法),最后,記得要釋放它。對(duì)于基本數(shù)據(jù)類型的參數(shù),從 Java 傳到 Delphi 中并在 Delphi 中使用的步驟就是這么簡單。
我們?cè)偬岣咭稽c(diǎn)點(diǎn)難度,構(gòu)建一個(gè)自定義類 Book ,并把它的實(shí)例對(duì)象作為參數(shù)傳入 Delphi ,研究一下在本地代碼中如何訪問對(duì)象參數(shù)的公共字段。
首先,定義一個(gè)簡單的 Java 類 Book ,為了把問題弄得稍微復(fù)雜一點(diǎn),我們?cè)?/span> 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 的代碼相對(duì)于上面幾個(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
對(duì)象。先調(diào)用
GetObjectClass
方法,根據(jù)
b
對(duì)象獲得它所屬的類
c
,然后調(diào)用
GetFieldID
方法從
?
中獲取一個(gè)叫做
title
的屬性的字段
ID
,一定要傳入正確的類型簽名。然后通過
GetObjectField
方法就可以根據(jù)得到的字段
ID
從對(duì)象中得到字段的值。注意這里的次序:我們得到傳入的對(duì)象參數(shù)
(Object)
,就要先得到它的類
(Class)
,這樣既有了對(duì)象實(shí)例,又有了類,以后就從類中得到字段
ID
,根據(jù)字段
ID
從對(duì)象中得到字段值。對(duì)于類的靜態(tài)字段,則可以直接從類中獲取它的值而不需要通過對(duì)象。
如果要調(diào)用對(duì)象的方法,操作步驟也基本類似,也需要從類中獲取方法
ID
,再執(zhí)行對(duì)象的相應(yīng)方法。在本例中,因?yàn)槲覀冊(cè)黾恿艘粋€(gè)
java.util.Date
類型的字段,要訪問這樣的字段,也只能先把它做為
JObject
讀入,再以同樣的方法進(jìn)一步去訪問它的成員(屬性或方法)。本例中演示了如何訪問
Date
對(duì)象的成員方法
toString
。
要正確地訪問類對(duì)象的成員屬性(字段)及成員方法,最重要的一點(diǎn)是一定要給出正確的簽名,在 Java 中對(duì)于數(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è)分號(hào)作為類型表達(dá)式的結(jié)束) |
數(shù)組 type[] |
[type
,例如
float[]
的簽名就是
[float
,如果是二維數(shù)組,如
float[][]
,則簽名為
[[float
,(注意:這里是兩個(gè)
[
符號(hào))。
|
方法 |
( 參數(shù)類型簽名 ) 返回值類型簽名,例如方法: float fun(int a,int b) ,它的簽名為 (II)F , ( 注意:兩個(gè) I 之間沒有逗號(hào)! ) ,而對(duì)于方法 String toString() ,則是 ()Ljava/lang/String; 。 |
通過上面的例子,我們了解了訪問對(duì)象參數(shù)的成員屬性或方法的基本步驟和多個(gè) Get 方法的使用。 TJNIEnv 同時(shí)提供了多個(gè) Set 方法,可以修改傳入的對(duì)象參數(shù)的字段值,因?yàn)?/span> Java 對(duì)象參數(shù)都是以傳址的方式進(jìn)行傳遞的,所以修改的結(jié)果可以在 Java 程序中得到反映。 TJNIEnv 提供的 Get/Set 方法,都需要兩個(gè)基本參數(shù):對(duì)象實(shí)例( JObject 類型)和字段 ID ( JField 類型),就可以根據(jù)提供的對(duì)象和字段 ID 來獲取或設(shè)置這個(gè)對(duì)象的這個(gè)字段的值。
現(xiàn)在我們了解了在 Delphi 代碼中使用以及修改 Java 對(duì)象的操作步驟。進(jìn)一步,如果需要在 Delphi 中從無到有地創(chuàng)建一個(gè)新的 Java 對(duì)象,可以嗎?再來看一個(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í)例,再對(duì)它進(jìn)行操作就與前面的例子沒有什么兩樣了。對(duì)于非空的構(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 對(duì)象的方法如果需要傳入?yún)?shù),都需要構(gòu)造出一個(gè)參數(shù)數(shù)組。在變量聲明中加上:
args : array[0..1] of JValue; |
注意!參數(shù)都是 JValue 類型,不管它是基本數(shù)據(jù)類型還是對(duì)象,都作為 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
對(duì)象時(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 代碼的方法,有興趣的讀者不妨自己研究一下。