Java 項目巴拿馬指南
一、概述
在本教程中,我們將瀏覽Project Panama組件。我們將首先探索Foreign Function 和 Memory API 。然後,我們將了解JExtract工具如何促進其使用。
2. 什麼是巴拿馬項目?
巴拿馬項目旨在簡化 Java 與外部(非 Java)API 之間的交互,即用 C、C++ 等編寫的本機代碼。
到目前為止,使用 Java 本機接口 (JNI) 是從 Java 調用外部函數的解決方案。但是 JNI 有一些缺點,巴拿馬項目通過以下方式解決了這些缺點:
- 無需在 Java 中編寫中間本機代碼包裝器
- 用更面向未來的 Memory API 替換 ByteBuffer API
- 引入一種與平台無關、安全且內存高效的方法來從 Java 調用本機代碼
為實現其目標,Panama 提供了一組 API 和工具:
- Foreign-Function and Memory API :用於分配和訪問堆外內存以及直接從 Java 代碼調用外部函數
- Vector API :使高級開發人員能夠用 Java 表達複雜的數據並行算法
- JExtract :一種從一組本機標頭中機械地派生 Java 綁定的工具
3.先決條件
要使用外部函數和內存 API,讓我們下載Project Panama Early-Access Build 。在撰寫本文時,我們使用的是Build 19-panama+1-13 (2022/1/18)
。接下來,我們根據使用的系統設置JAVA_HOME
。
由於 Foreign Function 和 Memory API 是預覽 API ,我們必須在啟用預覽功能的情況下編譯和運行我們的代碼,即通過將–enable-preview
標誌添加到java
和javac.
4. 外部函數和內存API
外部函數和內存 API 幫助 Java 程序與 Java 運行時之外的代碼和數據進行互操作。
它通過有效地調用外部函數(即 JVM 外部的代碼)和安全地訪問外部內存(即不受 JVM 管理的內存)來實現這一點。
它結合了兩個早期的孵化 API:外部內存訪問 API和外部鏈接器 API 。
API 提供了一組類和接口來執行這些操作:
- 使用
MemorySegment
、MemoryAddress
和SegmentAllocator
分配外部內存 - 通過
MemorySession
控制外來內存的分配和釋放 - 使用
MemoryLayout
操作結構化外部內存 - 通過 VarHandles 訪問結構化外部內存
- 借助
Linker
、FunctionDescriptor
和SymbolLookup
調用外部函數
4.1.外部內存分配
首先,讓我們探討一下內存分配。在這裡,主要的抽像是MemorySegment
。它模擬位於堆外或堆上的連續內存區域。 MemoryAddress
是段內的偏移量。簡單的說,一個內存段是由內存地址組成的,一個內存段可以包含其他的內存段。
此外,內存段綁定到它們封裝的MemorySession
並在不再需要時釋放。 MemorySession
管理段的生命週期並確保它們在被多個線程訪問時被正確釋放。
讓我們在內存段中的連續偏移處創建四個字節,然後將浮點值設置為6
:
try (MemorySession memorySession = MemorySession.openConfined()) {
int byteSize = 4;
int index = 0;
float value = 6;
MemorySegment segment = MemorySegment.allocateNative(byteSize, memorySession);
segment.setAtIndex(JAVA_FLOAT, index, value);
float result = segment.getAtIndex(JAVA_FLOAT, index);
System.out.println("Float value is:" + result);
}
在我們上面的代碼中, confined
內存會話限制對創建會話的線程的訪問,而shared
內存會話允許從任何線程進行訪問。
此外, JAVA_FLOAT ValueLayout
指定取消引用操作的屬性:類型映射的正確性和要取消引用的字節數.
SegmentAllocator
抽象定義了有用的操作來分配和初始化內存段。當我們的代碼管理大量堆外段時,它會非常有用:
String[] greetingStrings = {"hello", "world", "panama", "baeldung"};
SegmentAllocator allocator = SegmentAllocator.implicitAllocator();
MemorySegment offHeapSegment = allocator.allocateArray(ValueLayout.ADDRESS, greetingStrings.length);
for (int i = 0; i < greetingStrings.length; i++) {
// Allocate a string off-heap, then store a pointer to it
MemorySegment cString = allocator.allocateUtf8String(greetingStrings[i]);
offHeapSegment.setAtIndex(ValueLayout.ADDRESS, i, cString);
}
4.2.外部記憶操作
接下來,我們深入研究內存佈局的內存操作。 MemoryLayout
描述段的內容。它對操作本機代碼的高級數據結構很有用,例如struct
s、指針和指向struct
s 的指針。
讓我們使用GroupLayout
在堆外分配一個表示具有x
和y
坐標的點的C struct
:
GroupLayout pointLayout = structLayout(
JAVA_DOUBLE.withName("x"),
JAVA_DOUBLE.withName("y")
);
VarHandle xvarHandle = pointLayout.varHandle(PathElement.groupElement("x"));
VarHandle yvarHandle = pointLayout.varHandle(PathElement.groupElement("y"));
try (MemorySession memorySession = MemorySession.openConfined()) {
MemorySegment pointSegment = memorySession.allocate(pointLayout);
xvarHandle.set(pointSegment, 3d);
yvarHandle.set(pointSegment, 4d);
System.out.println(pointSegment.toString());
}
值得注意的是,不需要計算偏移量,因為不同的VarHandle
用於初始化每個點坐標。
我們還可以使用SequenceLayout.
以下是獲取五點列表的方法:
SequenceLayout ptsLayout = sequenceLayout(5, pointLayout);
4.3.來自 Java 的本機函數調用
Foreign Function API 允許 Java 開發人員在不依賴第三方包裝器的情況下使用任何本機庫。它嚴重依賴方法句柄並提供三個主要類: Linker
、 FunctionDescriptor
和SymbolLookup
。
讓我們考慮通過調用 C printf()
函數來打印“ Hello world
”消息:
#include <stdio.h>
int main() {
printf("Hello World from Project Panama Baeldung Article");
return 0;
}
首先,我們在標準庫的類加載器中查找函數:
Linker nativeLinker = Linker.nativeLinker();
SymbolLookup stdlibLookup = nativeLinker.defaultLookup();
SymbolLookup loaderLookup = SymbolLookup.loaderLookup();
Linker
是兩個二進制接口之間的橋樑:JVM 和 C/C++ 本機代碼,也稱為C ABI 。
下面,我們需要描述函數原型:
FunctionDescriptor printfDescriptor = FunctionDescriptor.of(JAVA_INT, ADDRESS);
值佈局JAVA_INT
和ADDRESS,
分別對應 C printf()
函數的返回類型和輸入:
int printf(const char * __restrict, ...)
接下來,我們獲取方法句柄:
String symbolName = "printf";
String greeting = "Hello World from Project Panama Baeldung Article";
鏈接器接口支持向下調用(從 Java 代碼調用本機代碼)和向上調用(從本機代碼調用回 Java 代碼)。最後,我們調用本機函數:
try (MemorySession memorySession = MemorySession.openConfined()) {
MemorySegment greetingSegment = memorySession.allocateUtf8String(greeting);
methodHandle.invoke(greetingSegment);
}
5.J提取
使用 JExtract,無需直接使用大部分外部函數和內存 API 抽象。讓我們重新打印上面的“ Hello World”
示例。
首先,我們需要從標準庫頭文件生成 Java 類:
jextract --source --output src/main -t foreign.c -I c:\mingw\include c:\mingw\include\stdio.h
stdio
的路徑必須更新為目標操作系統中的路徑。接下來,我們可以簡單地從 Java 中import
本機printf()
函數:
import static foreign.c.stdio_h.printf;
public class Greetings {
public static void main(String[] args) {
String greeting = "Hello World from Project Panama Baeldung Article, using JExtract!";
try (MemorySession memorySession = MemorySession.openConfined()) {
MemorySegment greetingSegment = memorySession.allocateUtf8String(greeting);
printf(greetingSegment);
}
}
}
運行代碼將問候語打印到控制台:
java --enable-native-access=ALL-UNNAMED --enable-preview --source 19 .\src\main\java\com\baeldung\java\panama\jextract\Greetings.java
六,結論
在本文中,我們了解了 Project Panama 的主要功能。
首先,我們探索了使用外部函數和內存 API 進行本機內存管理。然後我們使用MethodHandles
調用外部函數。最後,我們使用 JExtract 工具來隱藏 Foreign Function 和 Memory API 的複雜性。
Project Panama 還有很多值得探索的地方,特別是從本機代碼調用 Java、調用第三方庫和Vector API 。
與往常一樣,這些示例的代碼在 GitHub 上可用。