1. 概述
本教程将带你全面了解 Project Panama 的核心组件。首先,我们将探索 Foreign Function and Memory API。接着,我们会看到 JExtract 工具如何简化其使用。
2. 什么是 Project Panama?
Project Panama 旨在简化 Java 与外部(非 Java)API(即用 C、C++ 等编写的本地代码)的交互。
在此之前,使用 Java Native Interface (JNI) 是从 Java 调用外部函数的解决方案。但 JNI 存在一些缺点,Project Panama 通过以下方式解决了这些问题:
- ✅ 消除了编写中间本地代码包装器的需求
- ✅ 用更面向未来的 Memory API 替换了 ByteBuffer API
- ✅ 引入了一种平台无关、安全且内存高效的方式,从 Java 调用本地代码
为了实现这些目标,Panama 包含一组 API 和工具:
- Foreign-Function and Memory API:用于分配和访问堆外内存,并直接从 Java 代码调用外部函数
- Vector API:使高级开发者能够在 Java 中表达复杂的数据并行算法
- JExtract:一个工具,用于从一组本地头文件自动生成 Java 绑定
3. 前置条件
要使用 Foreign Function and Memory API,请下载 Project Panama Early-Access Build。本文撰写时,我们使用的是 *Build 19-panama+1-13 (2022/1/18)*。接下来,根据使用的系统设置 JAVA_HOME。
⚠️ 由于 Foreign Function and Memory API 是一个预览 API,我们必须在启用预览功能的情况下编译和运行代码,即通过向 java 和 javac 添加 –enable-preview 标志。
4. Foreign Function and Memory API
Foreign Function and Memory API 帮助 Java 程序与 Java 运行时之外的代码和数据互操作。
它通过高效调用外部函数(即 JVM 外部的代码)和安全访问外部内存(即 JVM 不管理的内存)来实现这一点。
它结合了两个早期的孵化 API:Foreign-Memory Access API 和 Foreign Linker API。
该 API 提供了一组类和接口来实现这些功能:
- 使用 MemorySegment、MemoryAddress 和 SegmentAllocator 分配外部内存
- 通过 Arena(从 JDK20 开始,MemorySession 被拆分为 Arena 和 SegmentScope) 控制外部内存的分配和释放
- 使用 MemoryLayout 操作结构化的外部内存
- 通过 VarHandles 访问结构化的外部内存
- 借助 Linker、FunctionDescriptor 和 SymbolLookup 调用外部函数
4.1. 外部内存分配
首先,我们探索内存分配。这里的主要抽象是 MemorySegment。它模拟一个连续的内存区域,可以位于堆外或堆上。MemoryAddress 是段内的偏移量。简单来说,内存段由内存地址组成,一个内存段可以包含其他内存段。
此外,内存段绑定到其封装的 Arena,并在不再需要时释放。Arena 管理段的生命周期,并确保在多线程访问时正确释放。
让我们在内存段中连续偏移处创建四个 bytes,然后设置一个浮点值 6:
try (Arena memorySession = Arena.ofConfined()) {
int byteSize = 5;
int index = 3;
float value = 6;
try(Arena arena = Arena.ofAuto()) {
MemorySegment segment = arena.allocate(byteSize);
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" };
try(Arena arena = Arena.ofAuto()) {
MemorySegment offHeapSegment = arena.allocateArray(ValueLayout.ADDRESS, greetingStrings.length);
for (int i = 0; i < greetingStrings.length; i++) {
// 在堆外分配一个字符串,然后存储指向它的指针
MemorySegment cString = arena.allocateUtf8String(greetingStrings[i]);
offHeapSegment.setAtIndex(ValueLayout.ADDRESS, i, cString);
}
}
4.2. 外部内存操作
接下来,我们深入探讨使用内存布局进行内存操作。MemoryLayout 描述了段的内容。它对于操作本地代码的高级数据结构(如 struct、指针和指向 struct 的指针)非常有用。
让我们使用 GroupLayout 在堆外分配一个表示点的 C struct,该点具有 x 和 y 坐标:
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 (Arena memorySession = Arena.ofConfined()) {
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 开发者使用任何本地库,而无需依赖第三方包装器。它严重依赖 Method Handles,并提供三个主要类: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";
Linker 接口支持向下调用(从 Java 代码到本地代码)和向上调用(从本地代码回调 Java 代码)。最后,我们调用本地函数:
try (Arena memorySession = Arena.ofConfined()) {
MemorySegment greetingSegment = memorySession.allocateUtf8String(greeting);
methodHandle.invoke(greetingSegment);
}
5. JExtract
使用 JExtract,无需直接使用大部分 Foreign Function & Memory 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 (Arena memorySession = Arena.ofConfined()) {
MemorySegment greetingSegment = memorySession.allocateUtf8String(greeting);
// 在取消注释之前生成 JExtract 绑定
// printf(greetingSegment);
}
}
}
运行代码会将问候语打印到控制台:
java --enable-preview --source 21 .\src\main\java\com\baeldung\java\panama\jextract\Greetings.java
6. 结论
在本文中,我们学习了 Project Panama 的关键特性。
- ✅ 首先使用 Foreign Function and Memory API 探索了本地内存管理
- ✅ 然后使用 MethodHandles 调用了外部函数
- ✅ 最后使用 JExtract 工具隐藏了 Foreign Function and Memory API 的复杂性
Project Panama 还有更多值得探索的内容,特别是从本地代码调用 Java、调用第三方库以及 Vector API。
一如既往,示例代码可在 GitHub 上获取。