一、前言
在 Java 的世界中,Java Native Interface (JNI) 允許 Java 程式碼呼叫以其他程式語言 (例如 C、C++) 編寫的程式庫,這在需要使用作業系統底層功能,或需要使用效能更好的原生程式碼時非常有用。傳統上,JNI 程式庫都是在執行時期動態載入的,這可能會導致一些問題,例如:載入失敗、版本衝突、效能損耗等。本文將梳理 Java 7 至 Java 17 中關於 JNI 程式庫的新特性,並已知 Java 19 後
jextract
工具帶來的便利性 (Java 19 才開始有完整支援),但本文先不介紹此工具,現階段以 Java 17 為主。
目錄
二、Java 7 及之前的 JNI 動態載入
在 Java 7 及更早的版本中,JNI 程式庫的使用主要依賴於動態載入機制,這通常涉及以下幾個步驟:
-
編寫原生程式碼:
使用 C 或 C++ 等語言編寫原生程式碼,並編譯成動態連結程式庫 (例如:
.dll
、.so
、.dylib
)。 -
定義 Java 介面:
使用
native
關鍵字在 Java 程式碼中宣告對應的原生方法。 -
呼叫
System.loadLibrary()
: 在 Java 程式碼中,使用System.loadLibrary()
或System.load()
方法,在執行時期載入原生程式庫。 - 使用原生方法: 透過定義好的 Java 介面,呼叫原生程式庫中的方法。
這種動態載入機制存在一些缺點:
- 執行時期錯誤: 如果程式庫檔案遺失、或版本不符、或有其他錯誤導致無法載入,將會在執行時期拋出例外。
- 部署複雜性: 需要在部署環境中確保原生程式庫和 Java 程式碼都能正確部署。
- 效能損耗: 動態載入程式庫需要耗費額外的時間和資源。
- 平台相依性: 需要為不同的作業系統編譯不同的原生程式庫。
三、Java 8:JNI 的靜態連結概念
Java 8 開始有靜態連結 JNI 程式庫的概念,目的是希望在編譯時期就將原生程式庫與 Java 應用程式連結在一起,以減少動態載入的缺點。 然而,在 Java 8 仍然需要使用
System.loadLibrary()
才能進行呼叫。
block-beta columns 5 A[".java源碼"] space B[".h標頭檔"] space C[".c源碼"] space space space space space D[".calss字節碼"] space space E[".dll文件"] space space space space space space F["執行結果"] space space space space classDef java stroke:#696; classDef native stroke:#969; class A,D,F java class B,C,E native A -- "javac -h" --> B B -- "實作" --> C A -- "javac" --> D D -- "調用" --> E D -- "執行" --> F B -- "編譯" --> E C -- "編譯" --> E
靜態連結 JNI 程式庫的主要優勢 (期望):
- 編譯時期檢查: 編譯器能在編譯時檢查原生程式庫是否存在,減少執行時期錯誤。
- 簡化部署: 可以直接打包單一的可執行檔案 (例如:使用 jpackage 打包),無需單獨部署原生程式庫。
- 提升效能: 可以減少動態載入程式庫的效能損耗。
- 更好的程式碼管理: 可以更好地管理程式碼依賴,並追蹤原生程式庫的版本。
在 Java 8 要實作靜態連結 JNI ,需要額外的工具或編譯器設定,並無法像 Java 19 那樣使用
jextract
工具直接達成。
Java 8 靜態連結 JNI 的主要限制
- 需要原生程式碼 必須擁有原生程式碼的原始碼或標頭檔,才能夠進行編譯時期的連結。
- 並非所有原生程式碼皆可使用 部份原生程式碼可能因為平台的差異或是有複雜的相依性,導致無法正確靜態連結。
- 編譯複雜度 需要考慮跨平台編譯的議題,並且必須管理不同平台的原生程式庫。
- 工具資源缺乏: Java 8 並未提供可產生靜態連結 JNI 程式碼的工具。
四、JNI 的持續改進
從 Java 9 到 Java 17,JNI 本身沒有新增主要的 API 變更,重點是在編譯工具、效能以及和其他特性的整合上有所改進:
-
jextract
工具的引入 (+19):jextract
工具是在 Java 14 中提及(現在已找不到相關資源與說明),明確版本為 Java 19 的 Foreign Function & Memory API 搭配才能發揮作用。jextract
工具能將 C/C++ 標頭檔自動產生 Java JNI 介面程式碼,大幅簡化 JNI 開發流程,後續有更新長期支援版本時在陸續介紹。 -
Foreign Function & Memory API 的引入 (+19):
Foreign Function & Memory API 從 Java 19 開始進入預覽階段,搭配
jextract
工具, 可以更有效率、更安全地存取原生程式碼。 - JNI 的效能優化: JDK 在每個版本都會對 JNI 的效能進行優化,例如降低 JNI 呼叫的損耗、改進參數傳遞機制等。
- GraalVM Native Image 的支持: GraalVM Native Image 可以將 Java 應用程式和相關的 JNI 程式庫編譯為獨立的可執行檔案,進一步提升效能。
五、JNI 的應用範例
以下是一個簡單的 JNI 動態連結使用範例 (Java 17 版本),並說明如何在 Windows 環境下使用 MinGW 編譯 C/C++ 的原生程式碼:
-
Java 程式碼 (
JNI17.java
):-
先建立
JNI17.java
檔案,並定義native
方法。
-
先建立
-
編譯 Java 程式碼,並產生 C/C++ 標頭檔:
-
在包含
JNI17.java
的資料夾下,執行以下命令來編譯 Java 程式碼,並產生 C/C++ 的標頭檔。
-
這個指令會先編譯 Java 程式碼,並在當前目錄生成
JNI17.h
這個標頭檔。
-
在包含
-
C/C++ 標頭檔 (
JNI17.h
):- 產出出來的標頭檔如下:
C 函數的命名規則為
Java_{package_and_classname}_{function_name}(JNI_arguments)
,package中的.
(dot)將被替換成底線(underscore)。 -
C/C++ 原始碼 (
JNI17.c
):-
根據標頭檔定義,建立 C/C++ 原始碼檔案
JNI17.c
。
-
根據標頭檔定義,建立 C/C++ 原始碼檔案
-
使用 MinGW 編譯 C/C++ 原始碼 (Windows):
-
安裝 MinGW:
- 下載 MinGW 的安裝程式,並依照提示進行安裝。
-
確保 MinGW 的
bin
資料夾路徑 (例如:C:\mingw64\bin
) 已加入系統的PATH
環境變數。 Using GCC with MinGW in VSCODE 。
-
編譯
JNI17.c
:- 開啟命令提示字元 (cmd.exe), PowerShell 或 bash。
-
導航到包含
JNI17.c
和JNI17.h
檔案的資料夾。 -
使用以下命令編譯
JNI17.c
,生成動態連結程式庫hellojni17.dll
:- (cmd.exe), PowerShell
- bash
-
-shared
選項表示生成動態連結程式庫。 -
-o hellojni17.dll
選項指定輸出檔案名稱為hellojni17.dll
。 -
-I"$JAVA_HOME\include" -I"$JAVA_HOME\include\win32"
選項表示使用Java下的jni標頭檔。
-
如果您使用 bash ,可以使用
file
檢查 dll 檔案。 -
如果您使用 bash ,可以使用
nm
查看add函數。
-
安裝 MinGW:
-
執行 Java 程式碼:
-
確保
hellojni17.dll
檔案與JNI17.class
在同一個資料夾,或者將hellojni17.dll
的路徑加入java.library.path
環境變數。 -
使用以下命令執行
JNI17
類別:
-
確保
程式碼說明:
-
javac -h . JNI17.java
: 表示使用 java 工具產生JNI17.h
這個檔案。 -
gcc
: 使用 GNU Compiler Collection (GCC) 編譯器來編譯 C/C++ 程式碼。 -
-I
: 表示編譯時搜尋標頭檔的路徑。 -
-shared
: 指示gcc
編譯器產生動態連結程式庫。 -
-o hellojni17.dll
: 將編譯結果輸出為hellojni17.dll
檔案。 -
System.loadLibrary("hellojni17")
: Java 程式碼使用此方法在執行時期載入hellojni17.dll
程式庫。
注意事項:
-
MinGW 安裝:
你必須先安裝 MinGW,並確保
bin
資料夾已加入PATH
環境變數。 -
DLL 檔案:
請務必將生成的
my_native.dll
檔案放在與 Java 程式碼相同的位置,或透過java.library.path
指定它的位置。 - 執行環境: 確保你的執行環境可以找到編譯好的 dll 檔案
- 跨平台編譯: 如果你需要跨平台使用 JNI,則需要在不同的作業系統上編譯不同的動態連結程式庫。
- 避免靜態連結: 在 Java 17 中, 靜態連結需要更複雜的步驟, 因此建議使用動態連結。
六、總結
Java 的 JNI 從 Java 7 及之前的動態載入,逐漸發展,在 Java 8 出現靜態連結 JNI 的概念,但實際上在 Java 8 仍然需要使用動態載入, 然後在 Java 9~17 持續改進 JNI 效能與相關工具鏈。 在 Java 17 中,仍然主要透過動態載入來使用 JNI 程式庫。 總而言之,透過瞭解 JNI 與其在不同版本的改進,可以更好地利用原生程式碼,來擴展 Java 應用程式的功能和效能。
沒有留言:
張貼留言