(給ImportNew加星標,提高Java技能)
隨着 JDK 19 在未來幾周*內發布,是時候討論巴拿馬(Panama)項目了,更具體地說,是新的外部函數和內存 API,它簡化了 Java 和本機代碼之間的互操作性。編註:2022年9月20日 JDK 19 已正式發布。本文使用一個簡單的基於 Java 的「Hello World」應用程序調用一些 C 本機代碼來介紹外部函數和內存 API。準備
要使用 Foreign Function & Memory API 和示例代碼,請先下載 JDK 19(build 24 或更高版本)。
項目概述
巴拿馬項目旨在為 JVM 和用其他語言(如 C/C++)編寫的本機代碼之間搭建橋樑。包含以下 3 個部分:
內存段及其地址:一組 API 類,用於處理本機內存和指向它的指針;內存布局和描述符:用於模擬外部類型(結構、原語)和函數描述符的 API;鏈接器和符號查找:一組用於執行向下和向上調用的 API 類;段分配器:一種用於在內存會話中分配內存段的 API。
Hello World 程序
對巴拿馬了解得越深,就越會發現擁有一個好的介紹是至關重要的,這樣就不會錯過重要的概念、技術和方法。
本文將介紹鏈接器(Linker),並簡要介紹 SymbolLookup 方法和本機內存管理 ( MemorySession )。上面描述的這三個主要組件是構建塊,用於更深入地開發由 Java 和本機代碼組成的程序。
鏈接器
從技術角度來看,鏈接器是兩個二進制接口之間的橋樑:JVM 和 C/C++ 本機代碼,也稱為 C ABI。
JDK 19 為所有流行的平台提供了一組 C ABI 實現:public static Linker getSystemLinker() { return switch (CABI.current()) { case Win64 - > Windowsx64Linker.getInstance(); case SysV - > SysVx64Linker.getInstance(); case LinuxAArch64 - > LinuxAArch64Linker.getInstance(); case MacOsAArch64 - > MacOsAArch64Linker.getInstance(); };}
在 JDK 術語中,鏈接器是特定於平台的 C ABI 實現的一個實例。鏈接器提供一組方法來執行向下調用和向上調用,其中:
downcall 是從高級子系統發起的事件。在我們的例子中是 JVM 到較低級別的子系統,如操作系統內核或者一些 Java 代碼調用一些本機代碼。稍後將通過外部函數和內存 API 說明這一點。
upcall 例如一些本機代碼調用一些 Java 代碼。
雖然鏈接器就像電話一樣,想打電話給誰,只需撥入正確的電話號碼即可。符號查找方法就像通訊錄,只需提供要打電話的人正確的信息即可。
要執行向下調用,需要提供調用的(本機)函數的描述符、通過符號查找分配的本機地址,以及用於創建調用本機函數的方法句柄對應的鏈接器。
從 Java 實現經典的 C 風格的 Hello World:
int printf(const char * __restrict, ...)Java 中的 C語言風格的「Hello World」
要編寫使用本機 printf 函數的基於 Java 的「Hello World」應用程序,我們需要:
1. 找到 native 函數的地址
首先,我們需要搜索 printf 函數的本機內存地址:Linker linker = Linker.nativeLinker();SymbolLookup linkerLookup = linker.defaultLookup();SymbolLookup systemLookup = SymbolLookup.loaderLookup();SymbolLookup symbolLookup = name -> systemLookup.lookup(name).or(() -> linkerLookup.lookup(name));Optional<MemorySegment> printfMemorySegment = symbolLookup.lookup("printf");
從技術上講,查找可能會失敗,因此需要提供適當的錯誤處理。
2. 構建正在調用的函數的描述符
一旦知道了 C printf 所在的位置,就需要定義由結果類型和接受的參數組成的 printf 描述符。值得一提的是,像 printf 這樣的本機函數稱為可變參數函數。在 Java 中,接受可變參數集的方法稱為具有可變參數的方法。
為了簡化,我們可以為 printf 定義 FunctionDescriptor 的簡化版本:FunctionDescriptorprintfDescriptor=FunctionDescriptor.of(JAVA_INT,ADDRESS);
注意:從 Java 運行時的角度來看,C 指針背後的值類型無關緊要,因為 C 指針的內存布局不保存類型,而是平台固定的 32/64 位值。
一個描述符定義了一個返回值類型為 int 的函數,它的參數是一個指針。假設一個描述符幾乎對應於它在 stdio.h 中的 C 定義,因為它定義了一個標準函數,而 printf 是一個可變參數函數。
通過值布局(Value Layout)在 Java 中對 C 類型建模
在 Java 中,值布局用於對與基本數據類型的值關聯的內存布局建模,例如整數類型(有符號或無符號)和浮點類型。JAVA_INT 和 ADDRESS 都是對應的 C 類型的值布局。
JAVA_INT:
// ValueLayout.OfInt.classOfInt JAVA_INT = new OfInt(ByteOrder.nativeOrder()).withBitAlignment(32);這是值布局的一個實例,它的載體是 int.class。通過這種布局,鏈接器被指示在 C int32和具有運營商類 int.class 的相應 Java int 類型之間創建橋樑。// ValueLayout.OfAddress.classOfAddress ADDRESS = new OfAddress(ByteOrder.nativeOrder()) .withBitAlignment(ValueLayout.ADDRESS_SIZE_BITS);ADDRESS是一個值布局,其中對應的 C 類型是一個指向變量的指針,載體是MemoryAddress.class。3. 從函數的本機內存地址構建方法句柄
使用 C printf 本機地址及其函數描述符,我們現在可以為 C printf 創建一個方法句柄:MethodHandle printfMethodHandle = symbolLookup.lookup("printf").map( addr - > linker.downcallHandle(addr, printfDescriptor)).orElse(null);
上面的代碼創建了 C print 的可執行引用,簡而言之:一個方法句柄,來自 printf 的本機內存地址及其函數描述符。
注意:方法句柄是對底層方法、構造函數、字段或類似低級操作的類型化、可執行引用,具有參數或返回值的可選轉換。
現在已經解釋了必要的概念,我們可以擴展 downcalls 和 upcalls 的定義:
downcall 是通過由本機函數地址及其 Java 版本的函數描述符形成的 MethodHandle調用本機函數。upcall 是通過 MethodHandle 調用一些用 Java 編寫的代碼,該 MethodHandle 轉換為本機內存段,然後可以將其作為函數指針傳遞給本機函數。4. 分配本機內存
我們需要以某種方式將 Java 對象綁定到本機內存段,以確保 C printf 可以訪問它們。C 中的內存分配和釋放內存都很痛苦,因為開發人員可能會忘記分配或釋放內存,這會導致程序泄漏或因分段錯誤而崩潰。
另一方面,Java 依靠垃圾收集器來分配和釋放內存。但是巴拿馬的外部函數和內存 API 是在堆外分配內存,有助於分配堆外內存,這是任何本機互操作的關鍵部分!
外部函數和內存 API允許開發人員分配和訪問內存段、它們的地址以及位於堆上或堆外的連續內存區域的形狀。所有分配的內存段都綁定到特定的內存會話 ( MemorySession )。內存會話的實例提供一組 API 來分配本機內存段。考慮一個內存會話,就像一個統一的內存分配工具,比如 C malloc。MemorySession 實現了 AutoClosable 接口,它使用 try-with-resources 結構極大地簡化了取消分配。
外部函數和內存 API提供了不止一種分配內存段的正確方法。一種可能的本機內存分配方法是 SegmentAllocator,它類似於 MemorySession:try (var memorySession = MemorySession.openConfined()) { SegmentAllocator allocator = SegmentAllocator.newNativeArena(memorySession); var cStringFromAllocator = allocator.allocateUtf8String("Hello World" + "\n"); var cStringFromSession = memorySession.allocateUtf8String("Hello World" + "\n");}
簡單起見,這個「Hello World」應用程序將使用 MemorySession 作為內存段分配工具。
最後,要調用 C printf,我們需要使用 MemorySession 在內存會話中分配 const char * 內存段,並將其傳遞給 C printf 函數:
MemorySegment cString = memorySession.allocateUtf8String(str + "\n");
private static int printf(String str, MemorySession memorySession) throws Throwable { Objects.requireNonNull(printfMethodHandle); var cString = memorySession.allocateUtf8String(str + "\n"); return (int) printfMethodHandle.invoke(cString);}public static void main(String[] args) throws Throwable { var str = "Hello World"; try (var memorySession = MemorySession.openConfined()) { System.out.println(printf(str, memorySession)); }}
5. 小結到目前為止,我們了解到內存會話 ( MemorySession ) 或段分配器 ( SegmentAllocator ) 是執行內存分配的關鍵 API。應使用 try-with-resources 聲明內存會話以實現隱式內存釋放。分配內存段有多種選擇——通過段分配器或直接通過內存會話。鏈接器、符號查找對象、值和內存布局以及方法句柄都是靜態對象。
本文概述了外部函數和內存 API,並研究了如何從 Java 調用簡單的 C 函數。好消息是開發人員可以依靠 jextract 工具來處理大部分外部函數和內存機制。
使用外部函數和內存 API 從 Java 調用本機代碼時需要解決幾個問題:
在 Java 中構建函數描述符 ( FunctionDescriptor )。創建一個相關的方法句柄並確認它已經正確創建(例如,如果本機庫不在系統路徑中,查找將失敗並且返回一個方法句柄將為空)。決定應用程序將如何分配內存段:通過段分配器或內存會話。確保內存分配技術在應用程序的整個代碼庫中保持一致。代碼清單
https://github.com/denismakogon/openjdk-project-samples/blob/master/Panama.md#openjdk-panama-part-1
package com.java_devrel.samples.panama.part_1;import java.lang.foreign.*;import java.lang.invoke.MethodHandle;import java.util.Objects;import static java.lang.foreign.ValueLayout.ADDRESS;import static java.lang.foreign.ValueLayout.JAVA_INT;public class PrintfSimplified { private static final Linker linker = Linker.nativeLinker(); private static final SymbolLookup linkerLookup = linker.defaultLookup(); private static final SymbolLookup systemLookup = SymbolLookup.loaderLookup(); private static final SymbolLookup symbolLookup = name - > systemLookup.lookup(name).or(() - > linkerLookup.lookup(name)); private static final FunctionDescriptor printfDescriptor = FunctionDescriptor.of(JAVA_INT.withBitAlignment(32), ADDRESS.withBitAlignment(64)); private static final MethodHandle printfMethodHandle = symbolLookup.lookup("printf").map(addr - > linker.downcallHandle(addr, printfDescriptor)).orElse(null); private static int printf(String str, MemorySession memorySession) throws Throwable { Objects.requireNonNull(printfMethodHandle); var cString = memorySession.allocateUtf8String(str + "\n"); return (int) printfMethodHandle.invoke(cString); } public static void main(String[] args) throws Throwable { var str = "hello world"; try (var memorySession = MemorySession.openConfined()) { System.out.println(printf(str, memorySession)); } }}轉自:Denys Makogon,
鏈接:denismakogon.github.io/openjdk/panama/2022/05/31/introduction-to-project-panama-part-1.html
- EOF -
1、JVM問題分析調優經驗
2、JDK 19 / Java 19正式GA
3、JVM 解剖公園(12):本地內存跟蹤
看完本文有收穫?請轉發分享給更多人
關注「ImportNew」,提升Java技能

點讚和在看就是最大的支持❤️