JNA簡介
JNA全稱Java Native Access,是一個建立在經典的JNI技術之上的Java開源框架(https://github.com/twall/jna)。JNA提供一組Java工具類用於在運行期動態訪問系統本地庫(native library:如Window的dll)而不需要編寫任何Native/JNI代碼。開發人員只要在一個java接口中描述目標native library的函數與結構,JNA將自動實現Java接口到native function的映射。
JNA包:
https://maven.java.net/content/repositories/releases/net/java/dev/jna/jna/4.0.0/jna-4.0.0.jar
JNA在線幫助文檔:http://twall.github.io/jna/4.0/javadoc/
JNA入門示例:https://github.com/twall/jna/blob/master/www/GettingStarted.md
1,dll和so是C函數的集合和容器,這與Java中的接口概念吻合,所以JNA把dll文件和so文件看成一個個接口。在JNA中定義一個接口就是相當於了定義一個DLL/SO文件的描述文件,該接口代表了動態鏈接庫中發布的所有函數。而且,對於程序不需要的函數,可以不在接口中聲明。
2,JNA定義的接口一般繼承com.sun.jna.Library接口,如果dll文件中的函數是以stdcall方式輸出函數,那么,該接口就應該繼承com.sun.jna.win32.StdCallLibrary接口。
3,Jna難點:編程語言之間的數據類型不一致。
Java和C的數據類型對照
Java和C的數據類型對照表
Java 類型 |
C 類型 |
原生表現 |
|
boolean |
int |
32位整數(可定制) |
|
byte |
char |
8位整數 |
|
char |
wchar_t |
平台依賴 |
|
short |
short |
16位整數 |
|
int |
int |
32位整數 |
|
long |
long long, __int64 |
64位整數 |
|
float |
float |
32位浮點數 |
|
double |
double |
64位浮點數 |
|
Buffer/Pointer |
pointer |
平台依賴(32或64位指針) |
|
<T>[] (基本類型的數組) |
pointer/array |
32或64位指針(參數/返回值) 鄰接內存(結構體成員) |
|
String |
char* |
/0結束的數組 (native encoding or jna.encoding) |
|
WString |
wchar_t* |
/0結束的數組(unicode) |
|
String[] |
char** |
/0結束的數組的數組 |
|
WString[] |
wchar_t** |
/0結束的寬字符數組的數組 |
|
Structure |
struct*/struct |
指向結構體的指針(參數或返回值) (或者明確指定是結構體指針)結構體(結構體的成員) (或者明確指定是結構體) |
|
Union |
union |
等同於結構體 |
|
Structure[] |
struct[] |
結構體的數組,鄰接內存 |
|
Callback |
<T> (*fp)() |
Java函數指針或原生函數指針 |
|
NativeMapped |
varies |
依賴於定義 |
|
NativeLong |
long |
平台依賴(32或64位整數) |
|
PointerType |
pointer |
和Pointer相同 |
通用入門案例
import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Platform;
/** Simple example of JNA interface mapping and usage. */
public class HelloWorld {
// This is the standard, stable way of mapping, which supports extensive
// customization and mapping of Java to native types.
public interface CLibrary extends Library {
CLibrary INSTANCE = (CLibrary)
Native.loadLibrary((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]);
}
}
}
運行程序,如果沒有帶參數則只打印出“Hello, World”,如果帶了參數,則會打印出所有的參數。
很簡單,不需要寫一行C代碼,就可以直接在Java中調用外部動態鏈接庫中的函數!
下面來解釋下這個程序。
(1)需要定義一個接口,繼承自Library
或StdCallLibrary
默認的是繼承Library
,如果動態鏈接庫里的函數是以stdcall方式輸出的,那么就繼承StdCallLibrary
,比如眾所周知的kernel32庫。比如上例中的接口定義:
public interface CLibrary extends Library {
}
(2)接口內部定義
接口內部需要一個公共靜態常量:INSTANCE,
通過這個常量,就可以獲得這個接口的實例,從而使用接口的方法,也就是調用外部dll/so的函數。
該常量通過Native.loadLibrary()這個API函數獲得,該函數有2個參數:
-
第一個參數是動態鏈接庫dll/so的名稱,但不帶.dll或.so這樣的后綴,這符合JNI的規范,因為帶了后綴名就不可以跨操作系統平台了。搜索動態鏈接庫路徑的順序是:先從當前類的當前文件夾找,如果沒有找到,再在工程當前文件夾下面找win32/win64文件夾,找到后搜索對應的dll文件,如果找不到再到WINDOWS下面去搜索,再找不到就會拋異常了。比如上例中printf函數在Windows平台下所在的dll庫名稱是msvcrt,而在其它平台如Linux下的so庫名稱是c。
-
第二個參數是本接口的Class類型。JNA通過這個Class類型,根據指定的.dll/.so文件,動態創建接口的實例。該實例由JNA通過反射自動生成。
CLibrary INSTANCE = (CLibrary)
Native.loadLibrary((Platform.isWindows() ? "msvcrt" : "c"),
CLibrary.class);
接口中只需要定義你要用到的函數或者公共變量,不需要的可以不定義,如上例只定義printf函數:
void printf(String format, Object... args);
注意參數和返回值的類型,應該和鏈接庫中的函數類型保持一致。
(3)調用鏈接庫中的函數
定義好接口后,就可以使用接口中的函數即相應dll/so中的函數了,前面說過調用方法就是通過接口中的實例進行調用,非常簡單,如上例中:
CLibrary.INSTANCE.printf("Hello, World\n");
for (int i=0;i < args.length;i++) {
CLibrary.INSTANCE.printf("Argument %d: %s\n", i, args[i]);
}
這就是JNA使用的簡單例子,可能有人認為這個例子太簡單了,因為使用的是系統自帶的動態鏈接庫,應該還給出一個自己實現的庫函數例子。其實我覺得這個完全沒有必要,這也是JNA的方便之處,不像JNI使用用戶自定義庫時還得定義一大堆配置信息,對於JNA來說,使用用戶自定義庫與使用系統自帶的庫是完全一樣的方法,不需要額外配置什么信息。比如我在Windows下建立一個動態庫程序:
#include "stdafx.h"
extern "C"_declspec(dllexport) int add(int a, int b);
int add(int a, int b) {
return a + b;
}
然后編譯成一個dll文件(比如CDLL.dll),放到當前目錄下,然后編寫JNA程序調用即可:
public class DllTest {
public interface CLibrary extends Library {
CLibrary INSTANCE = (CLibrary)Native.loadLibrary("CDLL", CLibrary.class);
int add(int a, int b);
}
public static void main(String[] args) {
int sum = CLibrary.INSTANCE.add(3, 6);
System.out.println(sum);
}
}
簡單案例
import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Platform;
interface HelloInter extends Library {
int toupper(int ch);
double pow(double x, double y);
void printf(String format, Object... args);
}
public class HelloWorld {
public static void main(String[] args) {
HelloInter INSTANCE = (HelloInter) Native.loadLibrary(Platform.isWindows() ? "msvcrt" : "c", HelloInter.class);
INSTANCE.printf("Hello, Worldn");
String[] strs = new String[] { "芙蓉", "如花", "鳳姐" };
for (int i = 0; i < strs.length; i++) {
INSTANCE.printf("人物 %d: %sn", i, strs[i]);
}
System.out.println("pow(2d,3d)==" + INSTANCE.pow(2d, 3d));
System.out.println("toupper('a')==" + (char) INSTANCE.toupper((int) 'a'));
}
}
顯示結果:
pow(2d,3d)==8.0
toupper('a')==A
Hello, Worldn人物 0: 芙蓉n人物 1: 如花n人物 2: 鳳姐n
說明:
HelloInter接口中定義的3個函數全是C語言函數庫中的函數,其定義格式如下:
int toupper(int ch)
double pow( double x, double y )
int printf(const char* format, ...)
C語言函數庫中有很多個函數,但是我們只用到了這3個函數,所以其他的函數不需要聲明在接口中。
JNA模擬結構體
例:使用 JNA調用使用 Struct的 C函數
假設我們現在有這樣一個C 語言結構體
struct UserStruct{
long id;
wchar_t* name;
int age;
};
使用上述結構體的函數
#define MYLIBAPI extern "C" __declspec( dllexport )
MYLIBAPI void sayUser(UserStruct* pUserStruct);
對應的Java 程序中,在例1 的接口中添加下列代碼:
public static class UserStruct extends Structure{
public NativeLong id;
public WString name;
public int age;
public static class ByReference extends UserStruct implements Structure.ByReference {}
public static class ByValue extends UserStruct implements Structure.ByValue {}
@Override
protected List getFieldOrder() {
return Arrays.asList(new String[] { "id", "name", "age"});
}
}
public void sayUser(UserStruct.ByReference struct);
Java中的代碼
UserStruct userStruct=new UserStruct ();
userStruct.id=new NativeLong(100);
userStruct.age=30;
userStruct.name=new WString("奧巴馬");
TestDll1.INSTANCE.sayUser(userStruct);
Structure說明
現在,我們就在Java 中實現了對C 語言的結構體的模擬。這里,我們繼承了Structure 類,用這個類來模擬C 語言的結構體。必須注意,Structure 子類中的公共字段的順序,必須與C 語言中的結構的順序一致。否則會報錯!因為,Java 調用動態鏈接庫中的C 函數,實際上就是一段內存作為函數的參數傳遞給C函數。動態鏈接庫以為這個參數就是C 語言傳過來的參數。同時,C 語言的結構體是一個嚴格的規范,它定義了內存的次序。因此,JNA 中模擬的結構體的變量順序絕對不能錯。
如果一個Struct 有2 個int 變量。Int a, int b如果JNA 中的次序和C 語言中的次序相反,那么不會報錯,但是數據將會被傳遞到錯誤的字段中去。
Structure 類代表了一個原生結構體。當Structure 對象作為一個函數的參數或者返回值傳遞時,它代表結構體指針。當它被用在另一個結構體內部作為一個字段時,它代表結構體本身。
另外,Structure 類有兩個內部接口Structure.ByReference 和Structure.ByValue。這兩個接口僅僅是標記,如果一個類實現Structure.ByReference 接口,就表示這個類代表結構體指針。
如果一個類實現Structure.ByValue 接口,就表示這個類代表結構體本身。使用這兩個接口的實現類,可以明確定義我們的Structure 實例表示的是結構體的指針還是結構體本身。上面的例子中,由於Structure 實例作為函數的參數使用,因此是結構體指針。所以這里直接使用了UserStruct userStruct=new UserStruct ();也可以使用UserStruct userStruct=new UserStruct.ByReference ();明確指出userStruct 對象是結構體指針而不是結構體本身。
JNA模擬復雜結構體C 語言最主要的數據類型就是結構體。結構體可以內部可以嵌套結構體,這使它可以擬任何類型的對象。JNA 也可以模擬這類復雜的結構體,結構體內部可以包含結構體對象的指針的數組
struct CompanyStruct{
long id;
wchar_t* name;
UserStruct users[100];
int count;
};
JNA 中可以這樣模擬:
public static class CompanyStruct extends Structure{
public NativeLong id;
public WString name;
public UserStruct.ByValue[] users=new UserStruct.ByValue[100];
public int count;
@Override
protected List getFieldOrder() {
return Arrays.asList(new String[] { "id", "name",,"users" "count"});
}
}
這里,必須給users 字段賦值,否則不會分配100 個UserStruct 結構體的內存,這樣JNA中的內存大小和原生代碼中結構體的內存大小不一致,調用就會失敗。
測試代碼:
CompanyStruct2.ByReference companyStruct2=new CompanyStruct2.ByReference();
companyStruct2.id=new NativeLong(2);
companyStruct2.name=new WString("Yahoo");
companyStruct2.count=10;
UserStruct.ByReference pUserStruct=new UserStruct.ByReference();
pUserStruct.id=new NativeLong(90);
pUserStruct.age=99;
pUserStruct.name=new WString("楊致遠");
// pUserStruct.write();
for(int i=0;i<companyStruct2.count;i++){
companyStruct2.users[i]=pUserStruct;
}
TestDll1.INSTANCE.sayCompany2(companyStruct2);
執行測試代碼,報錯了。這是怎么回事?
考察JNI 技術,我們發現Java 調用原生函數時,會把傳遞給原生函數的Java 數據固定在內存中,這樣原生函數才可以訪問這些Java 數據。對於沒有固定住的Java 對象,GC 可以刪除它,也可以移動它在內存中的位置,以使堆上的內存連續。如果原生函數訪問沒有被固定住的Java 對象,就會導致調用失敗。固定住哪些java 對象,是JVM 根據原生函數調用自動判斷的。而上面的CompanyStruct2結構體中的一個字段是UserStruct 對象指針的數組,因此,JVM 在執行時只是固定住了CompanyStruct2 對象的內存,而沒有固定住users 字段引用的UserStruct 數組。因此,造成了錯誤。我們需要把users 字段引用的UserStruct 數組的所有成員也全部固定住,禁止GC 移動或者刪除。如果我們執行了pUserStruct.write();這段代碼,那么就可以成功執行上述代碼。Structure 類的write()方法會把結構體的所有字段固定住,使原生函數可以訪問。
總結
使用JNA的過程中也不一定會一帆風順,比如會拋出”非法內存訪問”,這時候檢查一下變量是否==null。還有內存對齊的問題,當從內存中獲取圖片信息進行保存的時候,如果內存對齊處理不好,就會拋出很嚴重的異常,導致JVM異常退出,JNA提供了四種內存對齊的方式,分別是:ALIGN_DEFAULT、ALIGN_NONE、ALIGN_GNUC和ALIGN_MSVC。ALIGN_DEFAULT采用平台默認的對齊方式(推薦);ALIGN_NONE是不采用對齊方式;ALIGN_GNUC為針對linux/gcc操作系統的對齊方式。ALIGN_MSVC為針對win32/msvc架構的內存對齊方式。
JNA也提供了一種保護機制.比如防止JNA出現異常不會導致JVM異常退出,默認是開啟這個功能的,開啟方式為System.setProperty(“jna.protected”,”true”); 記得要在JNA加載dll文件之前調用,然后try {...} catch(Throwable e)異常,不過你也不要期望過高,不要以為加上這個就萬事大吉,出現”非法內存訪問”的時候還是會束手無策。JNA也提供了一種保護機制.比如防止JNA 出現異常不會導致JVM異常退出,默認是開啟這個功能的,開啟方式為 System.setProperty(“jna.protected”,”true”); 記得要在JNA加載dll文件之前調用,然后try {...} catch(Throwable e)異常,不過你也不要期望過高,不要以為加上這個就萬事大吉,出現”非法內存訪問”的時候還是會束手無策。