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,我们必须在启用预览功能的情况下编译和运行代码,即通过向 javajavac 添加 –enable-preview 标志。

4. Foreign Function and Memory API

Foreign Function and Memory API 帮助 Java 程序与 Java 运行时之外的代码和数据互操作。

它通过高效调用外部函数(即 JVM 外部的代码)和安全访问外部内存(即 JVM 不管理的内存)来实现这一点。

它结合了两个早期的孵化 API:Foreign-Memory Access APIForeign Linker API

该 API 提供了一组类和接口来实现这些功能:

  • 使用 MemorySegmentMemoryAddressSegmentAllocator 分配外部内存
  • 通过 Arena(从 JDK20 开始,MemorySession 被拆分为 Arena 和 SegmentScope) 控制外部内存的分配和释放
  • 使用 MemoryLayout 操作结构化的外部内存
  • 通过 VarHandles 访问结构化的外部内存
  • 借助 LinkerFunctionDescriptorSymbolLookup 调用外部函数

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,该点具有 xy 坐标:

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,并提供三个主要类:LinkerFunctionDescriptorSymbolLookup

让我们考虑通过调用 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_INTADDRESS 分别对应 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 上获取。


原始标题:Guide to Java Project Panama | Baeldung