簡介
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中,載入的順序是這樣的:
- jna.boot.library.path.
- 使用System.loadLibrary(java.lang.String)從系統的library path中查詢。如果不想從系統libary path中查詢,則可以設定jna.nosys=true。
- 如果從上述路徑中沒有找到,則會呼叫loadNativeDispatchLibrary將jna.jar中的jnidispatch解壓到本地,然後進行載入。如果不想從classpath中查詢,則可以設定jna.noclasspath=true。 如果不想從jna.jar檔案中解壓,則可以設定jna.nounpack=true。
- 如果你的系統對於從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的時候有一些搜尋路徑的規則如下:
- jna.library.path,使用者自定義的jna lib的路徑,優先從使用者自定義的路徑中開始查詢。
- jna.platform.library.path, 和platform相關的lib路徑。
- 如果是在OSX作業系統上,則會去搜尋 ~/Library/Frameworks, /Library/Frameworks, 和 /System/Library/Frameworks ,去查詢對應的Frameworks。
- 最後會去查詢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/
最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!
歡迎關注我的公眾號:「程式那些事」,懂技術,更懂你!