java高階用法之:呼叫本地方法的利器JNA

flydean發表於2022-03-31

簡介

JAVA是可以呼叫本地方法的,官方提供的呼叫方式叫做JNI,全稱叫做java native interface。要想使用JNI,我們需要在JAVA程式碼中定義native方法,然後通過javah命令建立C語言的標頭檔案,接著使用C或者C++語言來實現這個標頭檔案中的方法,編譯原始碼,最後將編譯後的檔案引入到JAVA的classpath中,執行即可。

雖然JAVA官方提供了呼叫原生方法的方式,但是好像這種方法有點繁瑣,使用起來沒有那麼的方便。

那麼有沒有更加簡潔的呼叫本地方法的形式嗎?答案是肯定的,這就是今天要講的JNA。

JNA初探

JNA的全稱是Java Native Access,它為我們提供了一種更加簡單的方式來訪問本地的共享庫資源,如果你使用JNA,那麼你只需要編寫相應的java程式碼即可,不需要編寫JNI或者原生程式碼,非常的方便。

本質上JNA使用的是一個小的JNI library stub,從而能夠動態呼叫本地方法。

JNA就是一個jar包,目前最新的版本是5.10.0,我們可以像下面這樣引用它:

<dependency>
            <groupId>net.java.dev.jna</groupId>
            <artifactId>jna</artifactId>
            <version>5.10.0</version>
        </dependency>

JNA是一個jar包,它裡面除了包含有基本的JAVA class檔案之外,還有很多和平臺相關的檔案,這些平臺相關的資料夾下面都是libjnidispatch*的庫檔案。

<img src="https://img-blog.csdnimg.cn/884d316db24a444fb9e8ea34d608e5a8.png" style="zoom:50%"/>

可以看到不同的平臺對應著不同的動態庫。

JNA的本質就是將大多數native的方法封裝到jar包中的動態庫中,並且提供了一系列的機制來自動載入這個動態庫。

接下來我們看一個具體使用JNA的例子:

public class JNAUsage {

    public interface CLibrary extends Library {
        CLibrary INSTANCE = (CLibrary)
                Native.load((Platform.isWindows() ? "msvcrt" : "c"),
                        CLibrary.class);

        void printf(String format, Object... args);
    }

    public static void main(String[] args) {
        CLibrary.INSTANCE.printf("Hello, World\n");
        for (int i=0;i < args.length;i++) {
            CLibrary.INSTANCE.printf("Argument %d: %s\n", i, args[i]);
        }
    }
}

這個例子中,我們想要載入系統的c lib,從而使用c lib中的printf方法。

具體做法就是建立一個CLibrary interface,這個interface繼承自Library,然後使用Native.load方法來載入c lib,最後在這個interface中定義要使用的lib中的方法即可。

那麼JNA到底是怎麼載入native lib的呢?我們一起來看看。

JNA載入native lib的流程

在講解JNA載入native lib之前,我們先回顧一下JNI是怎麼載入native lib的呢?

在JNI中,我們首先在java程式碼中定義要呼叫的native方法,然後使用javah命令,建立C的標頭檔案,然後再使用C或者C++來對這個標頭檔案進行實現。

接下來最重要的一步就是將生成的動態連結庫新增到JAVA的classpath中,從而在JAVA呼叫native方法的時候,能夠載入到對應的庫檔案。

對於上面的JNA的例子來說,直接執行可以得到下面的結果:

Hello, World

我們可以向程式新增JVM引數:-Djna.debug_load=true,從而讓程式能夠輸出一些除錯資訊,再次執行結果如下所示:

12月 24, 2021 9:16:05 下午 com.sun.jna.Native extractFromResourcePath
資訊: Looking in classpath from jdk.internal.loader.ClassLoaders$AppClassLoader@251a69d7 for /com/sun/jna/darwin-aarch64/libjnidispatch.jnilib
12月 24, 2021 9:16:05 下午 com.sun.jna.Native extractFromResourcePath
資訊: Found library resource at jar:file:/Users/flydean/.m2/repository/net/java/dev/jna/jna/5.10.0/jna-5.10.0.jar!/com/sun/jna/darwin-aarch64/libjnidispatch.jnilib
12月 24, 2021 9:16:05 下午 com.sun.jna.Native extractFromResourcePath
資訊: Extracting library to /Users/flydean/Library/Caches/JNA/temp/jna17752159487359796115.tmp
12月 24, 2021 9:16:05 下午 com.sun.jna.NativeLibrary loadLibrary
資訊: Looking for library 'c'
12月 24, 2021 9:16:05 下午 com.sun.jna.NativeLibrary loadLibrary
資訊: Adding paths from jna.library.path: null
12月 24, 2021 9:16:05 下午 com.sun.jna.NativeLibrary loadLibrary
資訊: Trying libc.dylib
12月 24, 2021 9:16:05 下午 com.sun.jna.NativeLibrary loadLibrary
資訊: Found library 'c' at libc.dylib
Hello, World

仔細觀察上面的輸出結果,我們可以大概瞭解JNA的工作流程。JNA的工作流程可以分為兩部分,第一部分是Library Loading,第二部分是Native Library Loading。

兩個部分分別對應的類是com.sun.jna.Native和com.sun.jna.NativeLibrary。

第一部分的Library Loading意思是將jnidispatch這個共享的lib檔案載入到System中,載入的順序是這樣的:

  1. jna.boot.library.path.
  2. 使用System.loadLibrary(java.lang.String)從系統的library path中查詢。如果不想從系統libary path中查詢,則可以設定jna.nosys=true。
  3. 如果從上述路徑中沒有找到,則會呼叫loadNativeDispatchLibrary將jna.jar中的jnidispatch解壓到本地,然後進行載入。如果不想從classpath中查詢,則可以設定jna.noclasspath=true。 如果不想從jna.jar檔案中解壓,則可以設定jna.nounpack=true。
  4. 如果你的系統對於從jar檔案中解壓檔案有安全方面的限制,比如SELinux,那麼你需要手動將jnidispatch安裝在一個可以訪問的地址,然後使用1或者2的方式來設定載入方式和路徑。

當jnidispatch被載入之後,會設定系統變數 jna.loaded=true,表示jna的lib已經載入完畢。

預設情況下我們載入的lib檔名字叫jnidispatch,你也可以通過設定jna.boot.library.name來對他進行修改。

我們看一下loadNativeDispatchLibrary的核心程式碼:

String libName = "/com/sun/jna/" + Platform.RESOURCE_PREFIX + "/" + mappedName;
            File lib = extractFromResourcePath(libName, Native.class.getClassLoader());
            if (lib == null) {
                if (lib == null) {
                    throw new UnsatisfiedLinkError("Could not find JNA native support");
                }
            }

            LOG.log(DEBUG_JNA_LOAD_LEVEL, "Trying {0}", lib.getAbsolutePath());
            System.setProperty("jnidispatch.path", lib.getAbsolutePath());
            System.load(lib.getAbsolutePath());
            jnidispatchPath = lib.getAbsolutePath();

首先是查詢stub lib檔案:/com/sun/jna/darwin-aarch64/libjnidispatch.jnilib, 預設情況下這個lib檔案是在jna.jar包中的,所以需要呼叫extractFromResourcePath方法將jar包中的lib檔案拷貝到臨時檔案中,然後呼叫System.load方法將其載入。

第二部分就是呼叫com.sun.jna.NativeLibrary中的loadLibrary方法來載入JAVA程式碼中要載入的lib。

在loadLibrary的時候有一些搜尋路徑的規則如下:

  1. jna.library.path,使用者自定義的jna lib的路徑,優先從使用者自定義的路徑中開始查詢。
  2. jna.platform.library.path, 和platform相關的lib路徑。
  3. 如果是在OSX作業系統上,則會去搜尋 ~/Library/Frameworks, /Library/Frameworks, 和 /System/Library/Frameworks ,去查詢對應的Frameworks。
  4. 最後會去查詢Context class loader classpath(classpath或者resource path),具體的格式是${os-prefix}/LIBRARY_FILENAME。如果內容是在jar包中,則會將檔案解壓縮至 jna.tmpdir,然後進行載入。

所有的搜尋邏輯都放在NativeLibrary的方法loadLibrary中實現的,方法體太長了,這裡就不一一列舉了,感興趣的朋友可以自行去探索。

本地方法中的結構體引數

如果本地方法傳入的引數是基本型別的話,在JNA中定義該native方法就用基本型別即可。

但是有時候,本地方法本身的引數是一個結構體型別,這種情況下我們該如何進行處理呢?

以Windows中的kernel32 library為例,這個lib中有一個GetSystemTime方法,傳入的是一個time結構體。

我們通過繼承Structure來定義引數的結構體:

@FieldOrder({ "wYear", "wMonth", "wDayOfWeek", "wDay", "wHour", "wMinute", "wSecond", "wMilliseconds" })
public static class SYSTEMTIME extends Structure {
    public short wYear;
    public short wMonth;
    public short wDayOfWeek;
    public short wDay;
    public short wHour;
    public short wMinute;
    public short wSecond;
    public short wMilliseconds;
}

然後定義一個Kernel32的interface:

public interface Kernel32 extends StdCallLibrary { 
Kernel32 INSTANCE = (Kernel32)
    Native.load("kernel32", Kernel32.class);
Kernel32 SYNC_INSTANCE = (Kernel32)
    Native.synchronizedLibrary(INSTANCE);

void GetSystemTime(SYSTEMTIME result);
}

最後這樣呼叫:

Kernel32 lib = Kernel32.INSTANCE;
SYSTEMTIME time = new SYSTEMTIME();
lib.GetSystemTime(time);

System.out.println("Today's integer value is " + time.wDay);

總結

以上就是JNA的基本使用,有關JNA根據深入的使用,敬請期待後續的文章。

本文的程式碼:https://github.com/ddean2009/learn-java-base-9-to-20.git

本文已收錄於 http://www.flydean.com/02-jna-overview/

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

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

相關文章