用Delphi開發JNI應用

來源:互聯網
上載者:User
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,輸入以下代碼:

UsesJNI;procedure Java_HelloWorld_displayHelloWorld(PEnv: PJNIEnv; Obj: JObject);stdcall; beginWriteln('您好!中國。');end;exportsJava_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; varJVM: TJNIEnv;beginJVM := 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提供了眾多的方法,這裡只使用了它最基本最常用的一個方法),最後,記得要釋放它。對於基礎資料型別 (Elementary Data Type)的參數,從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;varJVM: TJNIEnv;c,c2: JClass;fid:JFieldID;mid:JMethodID;title,datestr:string;price:double;pdate:JObject;beginJVM := 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中對於資料類型和方法的簽名有如下的約定:

通過上面的例子,我們瞭解了訪問對象參數的成員屬性或方法的基本步驟和多個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;varJVM: TJNIEnv;c: JClass;fid:JFieldID;b:JObject;mid:JMethodID;beginJVM := 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類型,不管它是基礎資料型別 (Elementary Data Type)還是對象,都作為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 recordcase Integer of0: (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 ) thenbeginWriteln(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代碼的方法,有興趣的讀者不妨自己研究一下。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.