Java的Panama專案與JNI以及外部函式介面FFI

banq發表於2022-04-14

微服務和快速啟動的Dockers容器普及的大背景下,Java還在卯足勁糾結於本地環境的互動上,從JNI、JNA到JNR再到Panama,真是有錢很任性,如同當年在J2me、JavaFX撒歡一樣:
Project Panama 是最新的 Java 專案,旨在簡化和改進 Java 中的 FFI,作為其中的一部分,目前正在孵化許多提案。

外來函式介面FFI是指從另一種程式語言中呼叫以一種程式語言編寫的函式或程式的能力。
FFI的大多數用例都是圍繞著與傳統應用程式的互動和訪問主機作業系統功能或本地庫。但最近機器學習和高階算術的激增使得FFI變得更加必要(banq:機器學習可選擇庫很多,Java前面排隊很多呢)。如今,我們將外部分函式用於一系列用例,其中一些是。
  • 與遺留的應用程式互動
  • 訪問語言中沒有的功能
  • 使用本地庫
  • 訪問主機作業系統上的函式或程式
  • 多精度算術,矩陣乘法
  • GPU和CPU解除安裝(Cuda、OpenCL、OpenGL、Vulcan、DirectX等)
  • 深度學習(Tensorflow、cuDNN、Blas等)
  • OpenSSL,V8,以及更多


Java Native Interface (JNI)
長期以來,Java中FFI的標準一直是Java Native Interface(JNI),它以緩慢和不安全而聞名。如果你習慣於其他語言,如Rust、Go或Python,你可能知道在它們中使用FFI是多麼容易和直觀,而這在Java中還有待改進。即使是使用JNI做一個小的本地呼叫,你也要做相當多的工作,而且還可能出錯,最終成為應用程式的安全問題。

JNI的主要問題是其使用的複雜性和需要手動編寫C橋程式碼。這些問題會導致不安全的程式碼和安全風險。在某些情況下,這也會導致效能開銷。JNI程式碼的效能和記憶體安全取決於開發者,因此可靠性會有所不同。

優點

  • C/C++/Assembly的本地介面訪問
  • 在Java中是最快的解決方案

缺點
  • 使用起來很複雜,而且很脆弱
  • 不太安全,可能導致記憶體安全問題
  • 有可能出現開銷和效能損失
  • 難以除錯
  • 依賴於Java開發人員手動編寫安全的C語言繫結程式碼
  • 你需要為每個目標平臺編譯和傳送C程式碼


Java Native Access (JNA)
JNI的複雜性催生了一些社群驅動的庫,使在Java中進行FFI變得更加簡單。Java Native Access(JNA)就是其中之一。它建立在JNI之上,至少使FFI更容易使用,特別是它消除了手動編寫任何C繫結程式碼的需要,減少了記憶體安全問題的機會。不過,它還是有一些基於JNI的缺點,在很多情況下比JNI稍慢。然而,JNA被廣泛使用並經過了實戰檢驗,所以絕對是比直接使用JNI更好的選擇。

優點

  • 對C/C++/Assembly的本地介面訪問
  • 與JNI相比,使用起來更簡單
  • 動態繫結,不需要手動編寫任何C繫結程式碼
  • 廣泛使用和成熟的庫
  • 更好的跨平臺支援

缺點
  • 使用反射
  • 建立在JNI之上
  • 有效能開銷,可能比JNI更慢
  • 難以除錯


Java Native Runtime (JNR)
另一個流行的選擇是Java Native Runtime(JNR)。雖然沒有像JNA那樣廣泛使用或成熟,但對於大多數使用情況來說,它更現代,效能比JNA更好。然而,在某些情況下,JNA可能表現得更好。

優點

  • C/C++/Assembly的本地介面訪問
  • 易於使用
  • 動態繫結,不需要手動編寫任何C繫結程式碼
  • 現代API
  • 與JNI的效能相當
  • 更好的跨平臺支援

缺點
  • 構建在JNI之上
  • 難以除錯



外存訪問 API
第一個難題是外部記憶體訪問 API。它最初是在 JDK 14 中孵化的,經過 3 次孵化後,一個新的JEP將它結合到 Foreign Function & Memory API 中。

  • 用於安全有效地訪問 Java 堆之外的外部記憶體的 API
    • 針對不同型別記憶體的一致 API
    • 不損害 JVM 記憶體安全
    • 顯式記憶體釋放
    • 與不同的記憶體資源互動,包括堆外或本機記憶體
  • JEP-370 - JDK 14 中的第一個孵化器
  • JEP-383 - JDK 15 中的第二個孵化器
  • JEP-393 - JDK 16 中的第三個孵化器



外部連結器 API
使 FFI 成為可能的另一個重要部分是 Foreign Linker API。這首先在 JDK 16 中孵化,並在下一個修訂版中合併到 Foreign Function & Memory API。

  • 用於靜態型別、純 Java 訪問本機程式碼的 API
    • 專注於易用性、靈活性和效能
    • 對 C 互操作的初始支援
    • .dll在、.so或中呼叫本機程式碼.dylib
    • 建立一個指向 Java 方法的本地函式指標,該方法可以傳遞給本地庫中的程式碼
  • JEP-389 - JDK 16 中的第一個孵化器


向量 API
接下來是向量 API,它對 FFI 至關重要,尤其是在機器學習和高階計算方面。

  • 用於可靠和高效能向量計算的 API
    • 平臺無關
    • 簡潔明瞭的 API
    • 可靠的執行時編譯和效能
    • 優雅的降級
  • JEP-338 - JDK 16 中的第一個孵化器
  • JEP-414 - JDK 17 中的第二個孵化器
  • JEP-417 - JDK 18 中的第三個孵化器


外部函式和記憶體 API
最後,Foreign Linker API 和 Foreign-Memory Access API 一起發展成為 Foreign Function & Memory API。它首先在 JDK 17 中孵化。

  • Foreign-Memory Access API 和 Foreign Linker API 的演變
    • 與前兩個相同的目標和功能(易用性、安全性、效能、通用性)
  • JEP-412 - JDK 17 中的第一個孵化器
  • JEP-419 - JDK 18 中的第二個孵化器
  • JEP-424 - JDK 19 中的第一個預覽版


Panama API
使用新的 Panama API,您可以透過兩種不同的方式執行相同的操作,即手動查詢和載入本機函式或使用 jextract 工具。
在第一種情況下,您只需使用 CLinker API 編寫一些 Java 程式碼。您查詢本機方法並呼叫它;就這麼簡單。您還可以做更復雜的事情,例如使用本機記憶體等。透過這種方法,您可以直接使用 Foreign Linker API 和 Foreign Memory API 進行本機呼叫和管理本機記憶體。這不是最有效的方法,因為這需要您編寫大量樣板程式碼,並且在使用大型 C 標頭檔案時擴充套件性不強。
第二種選擇是使用 jextract。使用 jextract,上面的整個過程可以變成一行程式碼。使用 jextract,您可以獲得用於本機程式的純 Java API,並且您無需編寫任何本機程式碼或接觸任何標頭檔案。jextract 使用外部連結器和外部記憶體 API 生成所有內容。是不是很厲害!這就是你在 Go 和 Rust 等語言中獲得的那種 FFI 體驗。
對於簡單的本地呼叫,您可以使用第一種方法,但對於複雜的呼叫,第二種方法更好且可擴充套件。

詳細點選標題

相關文章