使用JNA訪問本機動態庫

1.概述

在本教程中,我們將看到如何使用Java Native Access庫(簡稱JNA)來訪問本機庫,而無需編寫任何JNI(Java本機接口)代碼。

2.為什麼選擇JNA?

多年來,Java和其他基於JVM的語言在很大程度上實現了“一次編寫,隨處運行”的座右銘。但是,有時我們需要使用本機代碼來實現某些功能

  • 重用用C / C ++或任何其他能夠創建本機代碼的語言編寫的遺留代碼
  • 訪問標準Java運行時中不可用的特定於系統的功能
  • 為給定應用程序的特定部分優化速度和/或內存使用率。

最初,這種要求意味著我們不得不採用JNI – Java Native Interface。這種方法雖然有效,但也有其缺點,由於一些問題,通常可以避免使用:

  • 要求開發人員編寫C / C ++“膠水代碼”以橋接Java和本機代碼
  • 需要每個目標系統都可用的完整編譯和鏈接工具鏈
  • 與JVM來回編組和解組值是一項繁瑣且容易出錯的任務
  • 混合Java庫和本機庫時的法律和支持問題

JNA解決了與使用JNI相關的大多數複雜性。特別是,無需創建任何JNI代碼即可使用位於動態庫中的本機代碼,這使整個過程變得更加容易。

當然,需要權衡以下幾點:

  • 我們不能直接使用靜態庫
  • 與手工製作的JNI代碼相比更慢

但是,對於大多數應用程序而言,JNA的簡單性優勢遠遠超過了這些劣勢。因此,可以說,除非我們有非常具體的要求,否則今天的JNA可能是從Java(或任何其他基於JVM的語言)訪問本機代碼的最佳選擇。

3.JNA項目設置

使用JNA要做的第一件事是將其依賴項添加到我們項目的pom.xml

<dependency>

 <groupId>net.java.dev.jna</groupId>

 <artifactId>jna-platform</artifactId>

 <version>5.6.0</version>

 </dependency>

可以從Maven Central下載最新版本的jna-platform

4.使用JNA

使用JNA分為兩個步驟:

  • 首先,我們創建一個Java接口,該接口擴展了JNA的Library接口,以描述調用目標本機代碼時使用的方法和類型。
  • 接下來,我們將此接口傳遞給JNA,JNA返回此接口的具體實現,用於調用本機方法

4.1 C標準庫中的調用方法

對於我們的第一個示例,讓我們使用JNA cosh函數,該函數在大多數係統中都可用。此方法採用double精度參數併計算其雙曲餘弦值。 AC程序只需包含<math.h>頭文件即可使用此功能:

#include <math.h>

 #include <stdio.h>

 int main(int argc, char** argv) {

 double v = cosh(0.0);

 printf("Result: %f\n", v);

 }

讓我們創建調用此方法所需的Java接口:

public interface CMath extends Library {

 double cosh(double value);

 }

接下來,我們使用JNA的Native類創建此接口的具體實現,因此我們可以調用我們的API:

CMath lib = Native.load(Platform.isWindows()?"msvcrt":"c", CMath.class);

 double result = lib.cosh(0);

這裡真正有趣的部分是對load()方法的調用。它有兩個參數:動態庫名稱和描述我們將使用的方法的Java接口。它返回此接口的具體實現,允許我們調用其任何方法。

現在,動態庫名稱通常取決於系統,C標準庫也不例外:在大多數基於Linux的系統中, libc.so在Windows中為msvcrt.dll這就是為什麼我們使用Platform helper類來檢查我們在哪個平台上運行並選擇正確的庫名稱的原因。

請注意,我們不必添加.so.dll擴展名(暗含)。同樣,對於基於Linux的系統,我們不需要指定共享庫標準的“ lib”前綴。

因為從Java角度來看,動態庫的行為類似於Singletons,所以一種常見的做法是聲明INSTANCE字段作為接口聲明的一部分:

public interface CMath extends Library {

 CMath INSTANCE = Native.load(Platform.isWindows() ? "msvcrt" : "c", CMath.class);

 double cosh(double value);

 }

4.2 基本類型映射

在我們最初的示例中,被調用方法僅將原始類型用作其參數和返回值。從C類型映射時,JNA通常使用其自然的Java對應物自動處理這些情況:

  • char => byte
  • short => short
  • wchar_t => char
  • int => int
  • long => com.sun.jna.NativeLong
  • long long => long
  • float => float
  • double => double
  • char * => String

一種看起來很奇怪的映射是用於本機long類型的映射。這是因為在C / C ++中, long類型可能表示32位或64位值,具體取決於我們是在32位還是64位系統上運行。

為了解決此問題,JNA提供了NativeLong類型,該類型根據系統的體系結構使用適當的類型。

4.3 struct和union

另一個常見的情況是處理本機代碼API,這些API需要指向某種structunion類型.創建Java接口以訪問它時,相應的參數或返回值必須是分別Structure or Union

例如,給定此C結構:

struct foo_t {

 int field1;

 int field2;

 char *field3;

 };

它的Java對等類為:

@FieldOrder({"field1","field2","field3"})

 public class FooType extends Structure {

 int field1;

 int field2;

 String field3;

 };

JNA需要@FieldOrder批註,因此它可以在將數據用作目標方法的參數之前正確地將數據序列化到內存緩衝區中。

另外,我們可以重寫getFieldOrder()方法以達到相同的效果。當針對單個體系結構/平台時,前一種方法通常就足夠了。我們可以使用後者來處理跨平台的對齊問題,有時需要添加一些額外的填充字段。

Unions工作方式類似,除了以下幾點:

  • 無需使用@FieldOrder批註或實現getFieldOrder()
  • 我們必須在調用本機方法之前調用setType()

讓我們來看一個簡單的例子:

public class MyUnion extends Union {

 public String foo;

 public double bar;

 };

現在,讓我們將MyUnion與一個假設的庫一起使用:

MyUnion u = new MyUnion();

 u.foo = "test";

 u.setType(String.class);

 lib.some_method(u);

如果foobar的類型相同,則必須改用字段名:

u.foo = "test";

 u.setType("foo");

 lib.some_method(u);

4.4 使用指針

JNA提供了一個Pointer抽象,可以幫助處理用無類型指針聲明的API,通常是void *此類提供的方法允許對底層本機內存緩衝區進行讀寫訪問,這有明顯的風險。

在開始使用此類之前,我們必須確保我們清楚地了解每次是誰“擁有”了所引用的內存。否則,可能會產生難以調試的錯誤,這些錯誤與內存洩漏和/或無效訪問有關。

假設我們知道自己在做什麼(一如既往),讓我們看看如何將著名的malloc()free()函數與JNA一起使用,用於分配和釋放內存緩衝區。首先,讓我們再次創建包裝器接口:

public interface StdC extends Library {

 StdC INSTANCE = // ... instance creation omitted

 Pointer malloc(long n);

 void free(Pointer p);

 }

現在,讓我們使用它來分配緩衝區並使用它:

StdC lib = StdC.INSTANCE;

 Pointer p = lib.malloc(1024);

 p.setMemory(0l, 1024l, (byte) 0);

 lib.free(p);

setMemory()方法只是用一個恆定的字節值(在這種情況下為零)填充基礎緩衝區。注意, Pointer實例並不知道它指向的對象,更不用說它的大小了。這意味著我們可以很容易地使用堆的方法破壞堆。

稍後我們將看到如何使用JNA的崩潰保護功能減輕此類錯誤。

4.5 處理錯誤

標準C庫的舊版本使用全局errno變量來存儲特定調用失敗的原因。例如,這是典型的open()調用如何在C中使用此全局變量的方式:

int fd = open("some path", O_RDONLY);

 if (fd < 0) {

 printf("Open failed: errno=%d\n", errno);

 exit(1);

 }

當然,在現代的多線程程序中,此代碼將不起作用,對嗎?好吧,多虧了C的預處理程序,開發人員仍然可以編寫這樣的代碼,並且可以正常工作。事實證明,如今, errno是一個擴展為函數調用的宏:

// ... excerpt from bits/errno.h on Linux

 #define errno (*__errno_location ())



 // ... excerpt from <errno.h> from Visual Studio

 #define errno (*_errno())

現在,這種方法在編譯源代碼時很好用,但是在使用JNA時就沒有這種東西了。我們可以在包裝器接口中聲明擴展功能並顯式調用它,但是JNA提供了更好的替代方法: LastErrorException

在包裝器接口中聲明的所有帶有throws LastErrorException都將在本機調用之後自動包括錯誤檢查。如果報告錯誤,則JNA將拋出LastErrorException ,其中包括原始錯誤代碼。

讓我們在以前用來顯示此功能StdC包裝器接口中添加一些方法:

public interface StdC extends Library {

 // ... other methods omitted

 int open(String path, int flags) throws LastErrorException;

 int close(int fd) throws LastErrorException;

 }

現在,我們可以在try / catch子句中open()

StdC lib = StdC.INSTANCE;

 int fd = 0;

 try {

 fd = lib.open("/some/path",0);

 // ... use fd

 }

 catch (LastErrorException err) {

 // ... error handling

 }

 finally {

 if (fd > 0) {

 lib.close(fd);

 }

 }

catch塊中,我們可以使用LastErrorException.getErrorCode()獲取原始errno值,並將其用作錯誤處理邏輯的一部分。

4.6 處理訪問衝突

如前所述,JNA不能防止我們濫用給定的API,尤其是在處理來回傳遞本機代碼的內存緩衝區時。在正常情況下,此類錯誤會導致訪問衝突並終止JVM。

JNA在某種程度上支持允許Java代碼處理訪問衝突錯誤的方法。有兩種激活它的方法:

  • jna.protected系統屬性true
  • 調用Native.setProtected(true)

激活此保護模式後,JNA將捕獲通常會導致崩潰的訪問衝突錯誤,並引發java.lang.Error異常。 Pointer並嘗試向其中寫入一些數據來驗證此方法是否有效:

Native.setProtected(true);

 Pointer p = new Pointer(0l);

 try {

 p.setMemory(0, 100*1024, (byte) 0);

 }

 catch (Error err) {

 // ... error handling omitted

 }

但是,如文檔所述,此功能僅應用於調試/開發目的。

5.結論

在本文中,我們展示了與JNI相比,如何使用JNA輕鬆訪問本機代碼。