2025年1月18日 星期六

JAVA 7 to 17 JNI

一、前言

一、前言

返回目錄

在 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 程式庫的使用主要依賴於動態載入機制,這通常涉及以下幾個步驟:

  1. 編寫原生程式碼: 使用 C 或 C++ 等語言編寫原生程式碼,並編譯成動態連結程式庫 (例如: .dll .so .dylib )。
  2. 定義 Java 介面: 使用 native 關鍵字在 Java 程式碼中宣告對應的原生方法。
  3. 呼叫 System.loadLibrary() 在 Java 程式碼中,使用 System.loadLibrary() System.load() 方法,在執行時期載入原生程式庫。
  4. 使用原生方法: 透過定義好的 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 程式庫的主要優勢 (期望):

  1. 編譯時期檢查: 編譯器能在編譯時檢查原生程式庫是否存在,減少執行時期錯誤。
  2. 簡化部署: 可以直接打包單一的可執行檔案 (例如:使用 jpackage 打包),無需單獨部署原生程式庫。
  3. 提升效能: 可以減少動態載入程式庫的效能損耗。
  4. 更好的程式碼管理: 可以更好地管理程式碼依賴,並追蹤原生程式庫的版本。

在 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++ 的原生程式碼:

  1. Java 程式碼 ( JNI17.java ):

    • 先建立 JNI17.java 檔案,並定義 native 方法。
  2. 編譯 Java 程式碼,並產生 C/C++ 標頭檔:

    • 在包含 JNI17.java 的資料夾下,執行以下命令來編譯 Java 程式碼,並產生 C/C++ 的標頭檔。
    • 這個指令會先編譯 Java 程式碼,並在當前目錄生成 JNI17.h 這個標頭檔。
  3. C/C++ 標頭檔 ( JNI17.h ):

    • 產出出來的標頭檔如下:

    C 函數的命名規則為 Java_{package_and_classname}_{function_name}(JNI_arguments) ,package中的 . (dot)將被替換成底線(underscore)。

  4. C/C++ 原始碼 ( JNI17.c ):

    • 根據標頭檔定義,建立 C/C++ 原始碼檔案 JNI17.c
  5. 使用 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函數。
  6. 執行 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 應用程式的功能和效能。

沒有留言:

張貼留言