外部函数与内存API - Java 22 - 未记录

作者:API传播员 · 2025-10-30 · 阅读时间:7分钟

Foreign Function and Memory API(外部函数与内存API)是 Panama 项目的一部分,该项目旨在简化 Java 与外部(非 Java)API 的交互,例如用 C、C++ 或汇编语言编写的本地代码。由于许多原生库并非用 Java 编写,因此需要这种功能。以下是一些常见的原生库示例:

  1. OpenGL(图形处理)
  2. TensorFlow 和 ONNX(机器学习框架)
  3. OpenSSL(安全通信)
  4. CUDA(GPU 通用计算)

过去,Java 使用 JNI(Java Native Interface) 来实现与这些库的交互。JNI 允许 Java 类定义本机方法,这些方法的实现由 C 或 C++ 等本地语言编写。然而,这种方法存在以下缺点:

  1. 跨平台问题:Java 的“一次编写,到处运行”特性被破坏。支持多个平台(如 Windows、MacOS 和 Linux)需要为每个平台和架构单独构建库。
  2. 维护成本高:构建、维护和部署这些库的成本显著增加。
  3. 通信开销:Java 和本地代码之间的数据交换需要进行双向转换,这可能导致性能瓶颈。

为了解决这些问题,Panama 项目引入了多种工具和 API,包括:

  1. Foreign Function and Memory API:用于分配和访问堆外内存,并直接从 Java 调用外部函数。
  2. 矢量 API:在 Java 中执行矢量计算,提升性能。
  3. JExtract:自动生成 Java 与本地代码绑定的工具。

本文将重点介绍 Foreign Function and Memory API(简称 FFM API)。


外部存储器访问

在 Java 中,通过 new 关键字创建的对象存储在 JVM 的堆内存中,由垃圾回收器管理。然而,垃圾回收可能带来不可预测的性能开销,因此许多性能关键的库更倾向于使用堆外内存,由本地语言自行管理分配和释放。

Java 过去通过 ByteBuffer APIsun.misc.Unsafe 提供堆外内存访问,但它们各有局限性:

  • ByteBuffer API:内存区域大小限制为 2GB,且内存释放由垃圾回收器控制,而非开发者。
  • sun.misc.Unsafe:提供了过于细粒度的控制,容易导致悬空指针等错误。

FFM API 提供了一种更平衡的方式来访问堆外内存,同时保证安全性和灵活性。


外部内存的分配与释放

内存段

内存段是连续内存块的抽象表示,可以位于堆外或堆上。通过定义内存地址范围(空间边界)和生命周期(时间边界),内存段确保了内存访问的安全性。内存段分为以下几种类型:

  1. 本机段:分配在堆外内存中(类似于 malloc)。
  2. 映射段:使用映射文件的堆外内存区域(类似于 mmap)。
  3. 堆上段:基于 Java 数组或字节缓冲区的内存段。

竞技场

竞技场(Arena)定义了它分配的内存段的边界。例如:

MemorySegment segment = Arena.global().allocate(100);

上述代码分配了 100 字节的内存,地址范围为 bb+99,其中 b 是基址。竞技场有多种类型,具体如下表所示:

FFM API 提供了 SegmentAllocator 接口,用于抽象内存段的分配和初始化。Arena 类实现了该接口,支持分配本机内存段。


操纵和访问结构化外部存储器

内存布局

MemoryLayout 用于声明性地描述内存段的内容。以下是几种常见的内存布局:

  1. ValueLayout:用于建模基本数据类型,定义了大小、对齐方式和字节顺序等。
  2. SequenceLayout:表示元素布局的重复序列。
  3. GroupLayout:表示多个不同成员布局的组合,分为:
    • 结构布局:成员按顺序排列。
    • 联合布局:成员共享相同的起始偏移量。
  4. PaddingLayout:用于对齐成员布局,内容可忽略。

调用外部函数

符号查找

调用外部函数的第一步是找到目标函数或全局变量的地址。FFM API 提供了 SymbolLookup 接口,用于在本地库中查找符号。例如:

SymbolLookup lookup = SymbolLookup.libraryLookup(Paths.get(pathToLibrary), arena);
MemorySegment testSymbol = lookup.find("test")
    .orElseThrow(() -> new RuntimeException("找不到符号 'test'"));

FFM API 提供了以下三种 SymbolLookup 对象:

  1. libraryLookup(String, Arena):定位用户指定库中的符号。
  2. loaderLookup():查找当前类加载器加载的本地库中的符号。
  3. defaultLookup():查找常用本地库中的符号。

链接器

Linker 接口用于实现 Java 与本地代码的互操作,支持下调用(调用本地代码)和上调用(本地代码调用 Java)。通过 Linker.nativeLinker(),可以获取与当前平台关联的链接器。

下调用

通过 MethodHandle.downcallHandle,可以将外部函数的地址与 Java 方法句柄绑定,从而在 Java 中调用本地函数。例如:

MethodHandle addFunctionHandle = linker.downcallHandle(addSymbol, addDescriptor);

上调用

通过 MemorySegment.upcallStub,可以将 Java 方法转换为函数指针,供本地代码调用。

函数描述符

FunctionDescriptor 用于描述外部函数的签名,包括参数类型和返回类型。例如:

FunctionDescriptor descriptor = FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.JAVA_INT);

示例:调用 C 函数

以下是一个完整示例,展示如何通过 FFM API 调用用 C 编写的函数。

C 程序

创建一个名为 addition.c 的文件,内容如下:

#include 

int add(int a, int b) {
    return a + b;
}

编译为共享库(以 macOS 为例):

gcc -shared -o addition.dylib -fPIC addition.c

Java 程序

以下是 Java 程序代码:

import java.lang.foreign.*;
import java.lang.invoke.*;
import java.nio.file.*;

public class MemoryAccessExample {
    public static void main(String[] args) throws Throwable {
        FunctionDescriptor addDescriptor = FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.JAVA_INT);

        String libraryPath = System.getProperty("user.home") + "/Downloads/addition.dylib";
        try (Arena arena = Arena.ofConfined()) {
            SymbolLookup lookup = SymbolLookup.libraryLookup(Paths.get(libraryPath), arena);
            MemorySegment addSymbol = lookup.find("add")
                .orElseThrow(() -> new RuntimeException("找不到函数 'add'"));

            Linker linker = Linker.nativeLinker();
            MethodHandle addFunctionHandle = linker.downcallHandle(addSymbol, addDescriptor);

            int num1 = 25;
            int num2 = 10;

            try (Arena offHeap = Arena.ofConfined()) {
                MemorySegment intMemory = offHeap.allocate(ValueLayout.JAVA_INT, 2);
                intMemory.set(ValueLayout.JAVA_INT, 0, num1);
                intMemory.set(ValueLayout.JAVA_INT, 4, num2);

                int result = (int) addFunctionHandle.invoke(num1, num2);
                System.out.println("总和为:" + result);
            }
        }
    }
}

运行程序时需启用预览功能:

java --enable-preview MemoryAccessExample

输出结果为:

总和为:35

使用 FFM 的优势

FFM API 提供了一种安全、简洁且纯 Java 的方式来替代 JNI 和 sun.misc.Unsafe,同时保证了与它们相当的性能。通过统一的 API,开发者可以避免 JNI 的复杂性和潜在漏洞,从而更高效地与本地代码交互。

如需了解更多信息,请参考官方文档。

原文链接: https://www.unlogged.io/post/foreign-function-and-memory-api---java-22