java高級用法之:調用本地方法的利器JNA

簡介

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*的庫文件。

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

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/

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

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章