前言
只要你用過了 JNA (java native access) , 那你可能就再也不想用 JNI 了
實際上, JNA 搞定了 JNI 中最麻煩的數據類型映射, 可以讓我們進行高效的開發, 不用再去寫各種的轉換接口.
- char*
- string
- 數組
- 結構體
上面的數據類型它都支持
可能有人會問 JNA 能完全代替 JNI 么? 不能, JNA只能實現Java訪問C函數,如果你想實現C語言調用 Java 代碼, 你還是需要使用 JNI 技術。
但其實在很大程度上已經夠用了, 因為在很多應用領域都是由 C++算法工程師提供庫 (.so/.dll) , java 工程師只需要負責調用就可以了
JNA 有什么用? 吹一波?
就一個 簡化 jni 開發
想想當年寫 jni 的時候, 下面這樣的代碼要寫幾百行, 你就知道我有多痛苦了
當然最痛苦的還不是這個
在開發中, 作為一個JAVA 程序員你會遇到各種各樣奇葩的問題
還包含一部分你無法理解的C++問題
- 類型轉換
- linux windows多環境編譯
- 內存泄漏
- 異常處理
- 各種找不到原因的報錯
- debug 困難
- ...
而如果使用 JNA, 你可能只需要這樣:
你不需要通過 javah 生成頭文件, 不需要給它寫實現
不需要在 windows/linux 環境各自編譯成 .dll/.so 來調用真正的函數
只需要聲明一個接口, 其他的事情讓 JNA 做好就行
public class HelloWorld {
public interface CLibrary extends Library {
CLibrary INSTANCE = 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");
}
}
// 該源碼來自 https://github.com/java-native-access/jna/blob/master/www/GettingStarted.md
哪些項目使用了 JNA
- Apache Cassandra:大型NoSQL數據存儲
- JetBrains的IntelliJ IDEA
- NetBeans IDE
...
每天都在用的 IDEA 都用了 JNA =.= 你還怕什么?
數據類型映射
了解一下 C -> java 的數據類型映射, 幫助你更好的完成之后的練習
Native Type | Size | Java Type | Common Windows Types |
---|---|---|---|
char | 8-bit integer | byte | BYTE, TCHAR |
short | 16-bit integer | short | WORD |
wchar_t | 16/32-bit character | char | TCHAR |
int | 32-bit integer | int | DWORD |
int | boolean value | boolean | BOOL |
long | 32/64-bit integer | NativeLong | LONG |
long long | 64-bit integer | long | __int64 |
float | 32-bit FP | float | |
double | 64-bit FP | double | |
char* | C string | String | LPCSTR |
void* | pointer | Pointer | LPVOID, HANDLE, LPXXX |
from JNA - Default Type Mappings
你可能會發現表中沒有 native 的 boolean 類型, 默認上 boolean = true
會被默認映射成 -1(int)
, 在 C 中打印出來是 255. 所以我一般在定義接口時, 會避免使用 bool . 如果想自定義 boolean 映射, 可以參考 JNA maps Java boolean to -1 integer?
代碼示例
本代碼示例基于 64位 win10 + vs 2019
如果遇到 dll 依賴問題( UnsatisfiedLinkError ), 請下載 micro soft vc 運行庫 或者使用 Dependency walker 查看 dll 依賴缺失情況
可以 clone 一下我的項目:
java 部分: https://github.com/giraffe-tree/jna-func
c++ 部分(vs 項目): https://github.com/giraffe-tree/jna-c
max
先來個簡單的例子
先將下面的依賴加入你的 java maven 項目
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
<version>5.3.1</version>
</dependency>
// c++ 需要在 vs 中編譯成 dll
int max(int num1, int num2) {
return num1 > num2 ? num1 : num2;
}
// java
public interface JnaLibrary extends Library {
// JNA 為 dll 名稱
JnaLibrary INSTANCE = Native.load("JNA", JnaLibrary.class);
int max(int a, int b);
}
將編譯好的 JNA.dll
放入 resources 文件夾下的 win32-x86-64
目錄中 (我使用的是 64位 windows), JNA 會自動到 win32-x86-64
中去找 JNA.dll
運行 main
函數即可
public static void main(String[] args) {
int max = JnaLibrary.INSTANCE.max(100, 200);
// out: 200
System.out.println(max);
}
primitive array
// java
public interface JnaLibrary extends Library {
// JNA 為 dll 名稱
JnaLibrary INSTANCE = Native.load("JNA", JnaLibrary.class);
void testArray(short[] vals, int len);
}
// c++
void testArray(uint16_t* vals, int len) {
for (int j = 0; j < len; j++) {
printf("vals[%d]: %d \n", j, vals[j]);
}
}
JnaLibrary.INSTANCE.testArray(new short[]{1, 2, 3, 4}, 4);
// out:
vals[0]: 1
vals[1]: 2
vals[2]: 3
vals[3]: 4
值傳遞與引用傳遞
通過值傳遞對象的時候需要注意
- 對象需要繼承
Structure
, 且它的屬性必須為 public- Structure fields corresponding to native struct fields must be public. If your structure is to have no fields of its own, it must be declared abstract.
- JNA 有時候會判斷錯誤, 導致原本的值傳遞, 變成引用傳遞, 從而報出
Invalid memory access
的異常, 這時候最好實現一下Structure.ByValue
接口
- FieldOrder 需要按順序寫, 否則會報出
Invalid memory access
// java
public interface JnaLibrary extends Library {
// JNA 為 dll 名稱
JnaLibrary INSTANCE = Native.load("JNA", JnaLibrary.class);
// 實際測試下來 void printUser(User.ByValue user); 也是可以的
void printUser(User user);
void printUserRef(User user);
@Structure.FieldOrder({"name", "height", "weight"})
public static class User extends Structure {
public static class UserValue extends User implements Structure.ByValue {
public UserValue(String name, int height, double weight) {
super(name, height, weight);
}
}
public User(String name, int height, double weight) {
this.name = name;
this.height = height;
this.weight = weight;
}
public String name;
public int height;
public double weight;
}
}
// .h
struct User {
char* name;
int height;
double weight;
};
void printUser(User user);
void printUserRef(User& user);
// cpp
void printUser(User user) {
printf("printUser user: %s height: %d weight: %.2f \n", user.name, user.height, user.weight);
}
void printUserRef(User& user) {
printf("printUserRef user: %s height: %d weight: %.2f \n", user.name, user.height, user.weight);
}
JnaLibrary.User.UserValue user1 = new JnaLibrary.User.UserValue("user1", 186, 65.2);
JnaLibrary.INSTANCE.printUserRef(user1);
JnaLibrary.INSTANCE.printUser(user1);
// out:
printUserRef user: user1 height: 186 weight: 65.20
printUser user: user1 height: 186 weight: 65.20
Pointer
// java
public interface JnaLibrary extends Library {
// JNA 為 dll 名稱
JnaLibrary INSTANCE = Native.load("JNA", JnaLibrary.class);
void testStruct(ArrInfo arrInfo);
@Structure.FieldOrder({"vals", "len"})
public static class ArrInfo extends Structure {
public Pointer vals;
public int len;
public ArrInfo(Pointer vals, int len) {
this.vals = vals;
this.len = len;
}
}
}
// .h
struct ArrInfo
{
uint16_t* vals;
int len;
};
void testStruct(ArrInfo arrInfo);
// cpp
void testStruct(ArrInfo arrInfo) {
for (int j = 0; j < arrInfo.len; j++) {
printf("arrInfo[%d]: %d \n", j, arrInfo.vals[j]);
}
}
// java main test
int len = 3;
int shortSize = Native.getNativeSize(Short.class);
Pointer pointer = new Memory(len * shortSize);
for (int i = 0; i < len; i++) {
pointer.setShort(shortSize * i, (short) i);
}
JnaLibrary.ArrInfo arrInfo = new JnaLibrary.ArrInfo(pointer, len);
JnaLibrary.INSTANCE.testStruct(arrInfo);
// out
arrInfo[0]: 0
arrInfo[1]: 1
arrInfo[2]: 2
關于 JNA 調試
雖然 JNA 相比于 JNI 好用很多, 但我在使用的過程中還是遇到一些"坑"
這些 bug 常常很難直接找出, JNA 統一都報了一個 Invalid memory access
, 導致我們找不到真正錯誤的原因. 這時候就需要調試了
我使用的開發環境是 IDEA + jdk8 + VS2019
步驟
先簡單講下步驟, 具體圖文可以看下面的實戰
- 通過 vs 編譯 debug 版本的 dll , 放入 java 項目指定的目錄下 (我這里是
resources/win32-x86-64/JNA.dll
) - 啟動 java 程序, 停止在你指定的斷點上
-
jps -l
找到 java 程序的 pid - 在 vs 中
ctrl+alt+p
附加到進程, 選擇剛剛找到的 pid, 點擊附加- 其實一般是最上面的那個 java 進程, 就是你剛剛運行起來的那個
- 點擊
j
可以快速查找
- 在 vs 中設置一個斷點
- 在 idea 中繼續debug, 它會跳到 vs 的debug 界面中
- 在 vs 中點擊繼續就可以接著調試啦
實戰
在 idea 中進入調試
在下圖中, jps -l
找到 java 程序的 pid = 10560
在 vs 中 ctrl+alt+p
附加到進程, 選擇剛剛找到的 pid, 點擊附加
此時斷點不會命中
接著在 idea 中點擊繼續, 然后回到 vs 界面, 我們發現 斷點被觸發了
查看局部變量
yeah, 調試完成 =.=
其他問題
C++ 中 printf 控制臺打印缺失
目前測試下來, 可能存在 java 主線程停止, 但 C++ print 緩存區未被清空/不輸出的情況, 導致控制臺打印內容缺失
在這種情況下, 請延長 java 主線程運行的時間.
在C++調試時, 遇到未加載 jvm.pdb
目前測試下來, 在 vs 中 debug 繼續后會報出一個 未加載 jvm.pdb
, 我確實沒有找到這個文件
但還可以繼續調試, 沒啥大問題
不過什么時候來個調試 openjdk 想想應該蠻有趣的 哈哈
Invalid memory access
這個問題怎么說呢, JNA 好多地方都能報出這個錯誤, 我遇到這個錯誤時, 大部分都是我的java 參數 -> C++ 參數的映射問題
包括參數類型寫錯, 順序寫錯等, 仔細檢查映射關系就能解決
dll 兼容性問題
java.lang.UnsatisfiedLinkError: 找不到指定的模塊。
這個錯誤我遇到過的情況分兩種
- 我調用的 dll 沒有放進指定目錄
- 比如我的平臺是
win32-x86-64
, 但是沒有放入這個目錄中 - 這種情況下, jna 會提示找不到指定目錄的資源文件比較好解決
- 比如我的平臺是
- 我調用的 dll 依賴的其他 dll 沒有找到
- 這個情況, 我在 win10 下編譯, 然后在 windower server 2012 上運行時遇到過
- 通過解決方案是這樣
- 通過 Dependency walker 查看 dll 依賴缺失情況
- 補全缺失的 dll
- 當時我的情況是 缺失了 vc140, 然后我下載 micro soft vc 運行庫 就解決了這個問題
個人網站文章鏈接: https://giraffetree.me/2019/10/28/jna_tutorial/ 歡迎留言~