java高階用法之:在JNA中將本地方法對映到JAVA程式碼中

flydean發表於2022-04-13

簡介

不管是JNI還是JNA,最終呼叫的都是native的方法,但是對於JAVA程式來說,一定需要一個呼叫native方法的入口,也就是說我們需要在JAVA方法中定義需要呼叫的native方法。

對於JNI來說,我們可以使用native關鍵字來定義本地方法。那麼在JNA中有那些在JAVA程式碼中定義本地方法的方式呢?

Library Mapping

要想呼叫本地的native方法,首選需要做的事情就是載入native的lib檔案。我們把這個過程叫做Library Mapping,也就是說把native的library 對映到java程式碼中。

JNA中有兩種Library 對映的方法,分別是interface和direct mapping。

先看下interface mapping,假如我們要載入 C library, 如果使用interface mapping的方式,我們需要建立一個interface繼承Library:

public interface CLibrary extends Library {
    CLibrary INSTANCE = (CLibrary)Native.load("c", CLibrary.class);
}

上面程式碼中Library是一個interface,所有的interface mapping都需要繼承這個Library。

然後在interface內部,通過使用Native.load方法來載入要使用的c library。

上面的程式碼中,load方法傳入兩個引數,第一個引數是library的name,第二個引數是interfaceClass.

下面的表格展示了Library Name和傳入的name之間的對映關係:

OSLibrary NameString
Windowsuser32.dlluser32
LinuxlibX11.soX11
Mac OS Xlibm.dylibm
Mac OS X Framework/System/Library/Frameworks/Carbon.framework/CarbonCarbon
Any Platformcurrent processnull

事實上,load還可以接受一個options的Map引數。預設情況下JAVA interface中要呼叫的方法名稱就是native library中定義的方法名稱,但是有些情況下我們可能需要在JAVA程式碼中使用不同的名字,在這種情況下,可以傳入第三個引數map,map的key可以是 OPTION_FUNCTION_MAPPER,而它的value則是一個 FunctionMapper ,用來將JAVA中的方法名稱對映到native library中。

傳入的每一個native library都可以用一個NativeLibrary的例項來表示。這個NativeLibrary的例項也可以通過呼叫NativeLibrary.getInstance(String)來獲得。

另外一種載入native libary的方式就是direct mapping,direct mapping使用的是在static block中呼叫Native.register方式來載入本地庫,如下所示:

public class CLibrary {
    static {
        Native.register("c");
    }
}

Function Mapping

當我們載入完native library之後,接下來就是定義需要呼叫的函式了。實際上就是做一個從JAVA程式碼到native lib中函式的一個對映,我們將其稱為Function Mapping。

和Library Mapping一樣,Function Mapping也有兩種方式。分別是interface mapping和direct mapping。

在interface mapping中,我們只需要按照native library中的方法名稱定義一個一樣的方法即可,這個方法不用實現,也不需要像JNI一樣使用native來修飾,如下所示:

public interface CLibrary extends Library {
    int atol(String s);
}
注意,上面我們提到了JAVA中的方法名稱不一定必須和native library中的方法名稱一致,你可以通過給Native.load方法傳入一個FunctionMapper來實現。

或者,你可以使用direct mapping的方式,通過給方法新增一個native修飾符:


public class HelloWorld {
            
    public static native double cos(double x);
    public static native double sin(double x);
    
    static {
        Native.register(Platform.C_LIBRARY_NAME);
    }

    public static void main(String[] args) {
        System.out.println("cos(0)=" + cos(0));
        System.out.println("sin(0)=" + sin(0));
    }
}

對於direct mapping來說,JAVA方法可以對映到native library中的任何static或者物件方法。

雖然direct mapping和我們常用的java JNI有些類似,但是direct mapping存在著一些限制。

大部分情況下,direct mapping和interface mapping具有相同的對映型別,但是不支援Pointer/Structure/String/WString/NativeMapped陣列作為函式引數值。

在使用TypeMapper或者NativeMapped的情況下,direct mapping不支援 NIO Buffers 或者基本型別的陣列作為返回值。

如果要使用基礎型別的包裝類,則必須使用自定義的TypeMapper.

物件JAVA中的方法對映來說,該對映最終會建立一個Function物件。

Invocation Mapping

講完library mapping和function mapping之後,我們接下來講解一下Invocation Mapping。

Invocation Mapping代表的是Library中的OPTION_INVOCATION_MAPPER,它對應的值是一個InvocationMapper。

之前我們提到了FunctionMapper,可以實現JAVA中定義的方法名和native lib中的方法名不同,但是不能修改方法呼叫的狀態或者過程。

而InvocationMapper則更進一步, 允許您任意重新配置函式呼叫,包括更改方法名稱以及重新排序、新增或刪除引數。

下面舉個例子:

   new InvocationMapper() {
       public InvocationHandler getInvocationHandler(NativeLibrary lib, Method m) {
           if (m.getName().equals("stat")) {
               final Function f = lib.getFunction("_xstat");
               return new InvocationHandler() {
                   public Object invoke(Object proxy, Method method, Object[] args) {
                       Object[] newArgs = new Object[args.length+1];
                       System.arraycopy(args, 0, newArgs, 1, args.length);
                       newArgs[0] = Integer.valueOf(3); // _xstat version
                       return f.invoke(newArgs);
                   }
               };
           }
           return null;
       }
   }

看上面的呼叫例子,感覺有點像是反射呼叫,我們在InvocationMapper中實現了getInvocationHandler方法,根據給定的JAVA程式碼中的method去查詢具體的native lib,然後獲取到lib中的function,最後呼叫function的invoke方法實現方法的最終呼叫。

在這個過程中,我們可以修改方傳入的引數,或者做任何我們想做的事情。

還有一種情況是c語言中的行內函數或者預處理巨集,如下所示:

// Original C code (macro and inline variations)
   #define allocblock(x) malloc(x * 1024)
   static inline void* allocblock(size_t x) { return malloc(x * 1024); }

上面的程式碼中定義了一個allocblock(x)巨集,它實際上等於malloc(x * 1024),這種情況就可以使用InvocationMapper,將allocblock使用具體的malloc來替換:

   // Invocation mapping
   new InvocationMapper() {
       public InvocationHandler getInvocationHandler(NativeLibrary lib, Method m) {
           if (m.getName().equals("allocblock")) {
               final Function f = lib.getFunction("malloc");
               return new InvocationHandler() {
                   public Object invoke(Object proxy, Method method, Object[] args) {
                       args[0] = ((Integer)args[0]).intValue() * 1024;
                       return f.invoke(newArgs);
                   }
               };
           }
           return null;
       }
   }

防止VM崩潰

JAVA方法和native方法對映肯定會出現一些問題,如果對映方法不對或者引數不匹配的話,很有可能出現memory access errors,並且可能會導致VM崩潰。

通過呼叫Native.setProtected(true),可以將VM崩潰轉換成為對應的JAVA異常,當然,並不是所有的平臺都支援protection,如果平臺不支援protection,那麼Native.isProtected()會返回false。

如果要使用protection,還要同時使用 jsig library,以防止訊號和JVM的訊號衝突。libjsig.so一般存放在JRE的lib目錄下,${java.home}/lib/${os.arch}/libjsig.so, 可以通過將環境變數設定為LD_PRELOAD (或者LD_PRELOAD_64)來使用。

效能考慮

上面我們提到了JNA的兩種mapping方式,分別是interface mapping和direct mapping。相較而言,direct mapping的效率更高,因為direct mapping呼叫native方法更加高效。

但是上面我們也提到了direct mapping在使用上有一些限制,所以我們在使用的時候需要進行權衡。

另外,我們需要避免使用基礎型別的封裝類,因為對於native方法來說,只有基礎型別的匹配,如果要使用封裝類,則必須使用Type mapping,從而造成效能損失。

總結

JNA是呼叫native方法的利器,如果數量掌握的話,肯定是如虎添翼。

本文已收錄於 http://www.flydean.com/03-jna-library-mapping/

最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!

歡迎關注我的公眾號:「程式那些事」,懂技術,更懂你!

相關文章