1. 程式人生 > >JNI的又一替代者—使用JNR訪問Java外部函式介面(jnr-ffi)

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的穩定或安全。
因此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庫應用更廣泛(具體使用參見“”)。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命令打包,不過需要注意這裡有第三方依賴包,不是一句簡單的命令就可搞定。

將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的入門使用,更多的使用方法還期待官方給出更多的例子和說明文件。

4. 參考文獻