JNI的又一替代者—使用JNR訪問Java外部函式介面(jnr-ffi)
1. JNR簡單介紹
繼上文“”,我們知道JNI越來越不受歡迎,JNI是編寫Java本地方法以及將Java虛擬機器嵌入本地應用程式的標準程式設計介面。它管理著JVM和非託管的本地環境之間的邊界,提供資料編組和物件生命週期管理協議。
根據JEP(JDK增強提案) 191,JNI在下列幾個方面最令開發人員痛苦:
- 需要開發人員編寫C程式碼,這意味著他們需要具備一個完全不同於Java的世界的專業知識。
- 由於開發人員必須對JVM如何管理記憶體和程式碼多少有一些瞭解,所以典型的C和Java開發人員通常並不具備使用JNI所需的專業知識。
- 開發人員必須能夠為他們想要支援的每個平臺構建程式碼,或者為終端使用者提供適當的工具,由他們來完成這項工作。
- 相比於相同的庫繫結到本地應用程式,基於JNI的庫效能通常較差。
- JNI充當了一個不透明的安全邊界。JDK並不知道庫中的函式可能會呼叫什麼,或者庫中的程式碼是否會損害JVM的穩定或安全。
FFI API將提供下列特性:
- 一個描述本地庫呼叫和本地記憶體結構的元資料系統。
- 發現和載入本地庫的機制。
- 基於元資料將庫/函式或記憶體結構繫結到Java端點的機制。
- 用於Java資料型別和本地資料型別之間編組和解組的程式碼。
上面段落來自JEP 191的描述(由參考文獻(1)翻譯),由此可見雖然JNA使用廣泛,但JNR可能更漸趨勢,也許在不久的將來JNR-FFI(jffi)就會內建在JDK中與JNI一樣成為Java訪問外部函式的標準介面。因此,學習使用JNR是非常有必要的。
JNR-FFI專案也託管自Github,其使用方法與JNA差不多,不過JNR並沒有給出相應的jar包,需要我們自己打包使用。
2. JNR專案打包(jnr-ffi.jar)——如何打包Github上的maven專案
首先要明確,Github上託管的專案一般是用maven管理構建的,而不是Eclipse/MyEclipse,因此如果你想通過從Github上直接下載專案原始碼(Download Zip的方式下載)然後匯入或拷貝進Eclipse裡打包是行不通的。我一開始也是這麼做的,發現專案不完整,缺少一些包,因此打成的jar包也是不能用的。
讓我驚訝的,在maven官方庫裡的jnr-ffi.jar包也是不完整的,下載下來也不能用,還有這個地方的所有jnr包,我都試過了,全部不完整,因此只能自己打包。
在打包之前,你首先需要將完整的原始碼下載下來,然後有兩種方式打包成jar檔案。
- 將maven專案匯入Eclipse中打包
- 通過maven命令mvn打包
兩種方法都有需要注意的地方。不熟悉maven的人可以採取第一種方式,上手簡單。熟悉maven的當然推薦用mvn命令打包,不過需要注意這裡有第三方依賴包,不是一句簡單的命令就可搞定。
將maven專案匯入Eclipse中打包
注意:雖然Eclipse內建了Maven外掛,但表示不太好用,經常出現問題,建議解除安裝Eclipse的自帶的maven外掛,然後安裝第三方的m2eclipse外掛,該外掛目前有效的安裝地址為:http://download.eclipse.org/technology/m2e/releases,通過Eclipse中Help—Install New
Software...—Add Repository安裝即可。
有了maven外掛後,打包的具體步驟如下:
(1)從Github下載原始碼
這個其實非常關鍵,因為不能通過“Download Zip”的方式直接從Github網頁上下載,這樣下載的原始碼缺少很多j依賴的ar包,需要通過git clone的方式下載
git clone https://github.com/jnr/jnr-ffi.git
下載後的專案原始碼就在當前命令列路徑下。
(2)匯入maven專案
將剛下載的完整的jnr原始碼匯入到Eclipse中,注意匯入的是Maven專案
選擇剛下載的專案根路徑
這裡出現了錯誤,如果沒錯的就可以直接打包了,如果跟我一樣出現下面的錯誤,那麼請繼續
從出錯資訊可以看出是缺少Maven-antrun外掛,這是Maven的ant外掛,用來自動構建專案的,沒有這個外掛,maven配置檔案pom.xml中的<execution></execution>之間的任務就執行不了,因此如果忽略這個出錯繼續點“Finish”那麼pom.xml檔案就有錯誤,具體的出錯資訊如下:
Plugin execution not covered by lifecycle configuration: org.apache.maven.plugins:maven-antrun-plugin:1.1:run (execution: default, phase: test-compile)這裡有官方給出的解決方案,我就直接用第一種方法:在<plugins>前面加上<pluginManagement>,在</plugins>後面加上加上</pluginManagement> 即可。
其實我的Eclipse工程裡還有另外一個錯誤,就是在NativeClosureFactory.java檔案中:
The method expunge(NativeClosureFactory.ClosureReference, Integer) in the type NativeClosureFactory is not applicable for the arguments (NativeClosureFactory<T>.ClosureReference, Integer)屬於Java泛型錯誤,不知道完整的程式碼你可能不知道具體的問題所在,下面舉個簡單的例子:
public final class Native<T> {
private void test1(Ref ref, Integer key) {
}
final class Ref {
private final Native factory;
private Ref(Native factory) {
this.factory = factory;
}
public void test2() {
factory.test1(this, 1);
}
}
}
你能看出問題所在嗎?Native類是個泛型類,但在其內建類Ref中使用時沒有加上泛型的標誌,將Native當作普通類使用,忽略了泛型<T>標誌。其實這可能與Java編譯器有關,有的版本可能不會報這個錯,那麼改正方法也很簡單,將
privatefinalNative factory;privateRef(Native factory){
改成
privatefinalNative<T> factory;privateRef(Native<T> factory){
即可。
至此,專案沒有任何錯誤產生了,就可以開始打包了(據我測試,前面的兩個錯誤不改正直接打包其實也沒什麼關係,jar包照樣能用,但是知錯改錯我們能學到更多額外的東西)。
(3)用Build fat jar 打包
這裡為什麼說要用“Build fat jar”工具打包而不是直接的export出jar包的方式打包呢?因為該工程依賴了很多其它的第三方jar包,如果直接export而不作配置,這些依賴的jar包不會被打進去,也就錯了,需要自定義配置檔案MANIFEST.MF,有些麻煩,具體配置可參考“Eclipse將引用了第三方jar包的Java專案打包成jar檔案的兩種方法”。
使用Fat jar打包外掛就不一樣了,無需任何配置,一鍵打包,該外掛安裝方法也請參考上述文章:
修改jar包檔案,加上目前的版本號即可。可以看到用Eclipse打包還是挺麻煩的,至少我遇到了N多問題,因此推薦用mvn命令打包。
通過maven命令mvn打包
如果你機子上沒有安裝maven,那麼請首先到這裡下載其二進位制包,無需安裝,只要解壓到某個路徑下,然後將其路徑新增到環境變數PATH中即可在任何地方使用。
命令列進入到jnr-ffi所在根目錄,一般用mvn命令打jar命令如下即可:
mvn jar:jar
但是這樣的不對的,該命令打成的jar包不包含依賴的第三方jar檔案,因此是錯誤的。其實我發現在網上找到的所有jnr-ffi的jar包都是直接用這個命令打包的,因此全部不能用。
正確的打包方式是:
將包含第三方依賴jar的maven專案打包成jar檔案有兩種方法,我這裡使用比較簡單的方法:使用maven-assembly-plugin打包,步驟如下:
(1)pom.xml新增assembly外掛
<plugin> <artifactId>maven-assembly-plugin</artifactId> <configuration> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> </configuration> </plugin>由於第三方jar沒有main檔案,所以不需要加manifest。
(2)執行如下命令
mvn assembly:assembly這樣就在jnr-ffi根目錄下的target資料夾裡生成一個jnr-ffi-2.0.0-SNAPSHOT-jar-with-dependencies.jar檔案。
這就是我們所需要的jar檔案。
3. JNR簡單例項
將打包好的jar檔案加到Eclipse中,還是以“Hello World”為例,這次用C中的puts()函式列印,如下:
package helloworld;
import jnr.ffi.LibraryLoader;
public class HelloWorld {
public static interface LibC {
int puts(String s);
}
public static void main(String[] args) {
LibC libc = LibraryLoader.create(LibC.class).load("msvcrt");
libc.puts("Hello, World");
}
}
(1)定義一個靜態介面
與JNA不同的是,該靜態介面不用繼承JNR中的某個類,更加簡單。
接口裡的內容就是你要用的動態連結庫函式原型,同樣的,該原型必須與C/C++中的保持一致,這同樣是技術難點(詳見上篇文章中的技術難點詳述)。
(2)如何呼叫宣告的外部函式
首先通過LibraryLoader.create().laod()得到該介面的一個例項,然後通過該例項直接呼叫裡面的方法即可。
LibraryLoader.create().load()中第一個括號裡是該介面的Class型別,第二個括號是要載入的動態連結庫名稱,同樣沒有.dll/.so字尾。這兩個引數與JNA下的兩個引數是一樣的,使用情況也是一樣。
Java的型別與C型別的對應關係為:
- byte - 8 bit signed integer
- short - 16 bit signed integer
- int - 32 bit signed integer
- long - natural long (i.e. 32 bits wide on 32 bit systems, 64 bit wide on 64bit systems)
- float - 32 bit float
- double - 64 bit float
- String - equivalent to "const char *"
- Pointer - equivalent to "void *"
- Buffer - equivalent to "void *"
這只是JNR的入門使用,更多的使用方法還期待官方給出更多的例子和說明文件。