1. JNR簡單介紹
繼上文“JNI的替代者—使用JNA訪問Java外部函數接口”,我們知道JNI越來越不受歡迎,JNI是編寫Java本地方法以及將Java虛擬機嵌入本地應用程序的標准編程接口。它管理著JVM和非托管的本地環境之間的邊界,提供數據編組和對象生命周期管理協議。
根據JEP(JDK增強提案) 191,JNI在下列幾個方面最令開發人員痛苦:
需要開發人員編寫C代碼,這意味著他們需要具備一個完全不同於Java的世界的專業知識。
由於開發人員必須對JVM如何管理內存和代碼多少有一些了解,所以典型的C和Java開發人員通常並不具備使用JNI所需的專業知識。
開發人員必須能夠為他們想要支持的每個平台構建代碼,或者為終端用戶提供適當的工具,由他們來完成這項工作。
相比於相同的庫綁定到本地應用程序,基於JNI的庫性能通常較差。
JNI充當了一個不透明的安全邊界。JDK並不知道庫中的函數可能會調用什麼,或者庫中的代碼是否會損害JVM的穩定或安全。
因此JNI創建本地函數的方式並不簡單,於是產生了像Java Native Access(JNA)和Java Native Runtime(JNR)這樣的庫。JNA和JNR都是基於JNI創建的,而JEP 191定義的Java Foreign Function Interface(FFI)可能會基於JNR。使用FFI API而不是JNI綁定本地代碼和內存將成為開發人員更喜歡的方式。
FFI API將提供下列特性:
一個描述本地庫調用和本地內存結構的元數據系統。
發現和加載本地庫的機制。
基於元數據將庫/函數或內存結構綁定到Java端點的機制。
用於Java數據類型和本地數據類型之間編組和解組的代碼。
對Java FFI的需求已經產生了JNA和JNR庫。JNA庫應用更廣泛(具體使用參見“JNI的替代者—使用JNA訪問Java外部函數接口”)。JNR庫更全面,因為它實現了不同層次的抽象,提供了函數和內存元數據,對庫和函數綁定進行了抽象。JNR已經在JRuby項目中大量使用,它可能會成為JEP 191的基礎。
上面段落來自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命令打包,不過需要注意這裡有第三方依賴包,不是一句簡單的命令就可搞定。
注意:雖然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,那麼請首先到這裡下載其二進制包,無需安裝,只要解壓到某個路徑下,然後將其路徑添加到環境變量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文件。
不管如何,如果你打包不順利的話,這裡有我打的jnr-ffi_2.0.0jar包下載地址
查看本欄目
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的入門使用,更多的使用方法還期待官方給出更多的例子和說明文檔。