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