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();
? }
}
|
運行它,如果控制臺輸出了“您好!中國?!保材悖阋呀洺晒Φ赜?/span>
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
代碼的方法,有興趣的讀者不妨自己研究一下。
posted on 2006-12-19 05:41
壞男孩 閱讀(1296)
評論(1) 編輯 收藏 所屬分類:
java命令學習