外部函数与内存API - Java 22 - 未记录
Foreign Function and Memory 外部函数与内存API)是 Panama 项目的一部分,该项目旨在简化 Java 与外部(非 Java)API 的交互,例如用 C、C++ 或汇编语言编写的本地代码。由于许多原生库并非用 Java 编写,因此需要这种功能。以下是一些常见的原生库示例:
- OpenGL(图形处理)
 - TensorFlow 和 ONNX(机器学习框架)
 - OpenSSL(安全通信)
 - CUDA(GPU 通用计算)
 
过去,Java 使用 JNI(Java Native Interface) 来实现与这些库的交互。JNI 允许 Java 类定义本机方法,这些方法的实现由 C 或 C++ 等本地语言编写。然而,这种方法存在以下缺点:
- 跨平台问题:Java 的“一次编写,到处运行”特性被破坏。支持多个平台(如 Windows、MacOS 和 Linux)需要为每个平台和架构单独构建库。
 - 维护成本高:构建、维护和部署这些库的成本显著增加。
 - 通信开销:Java 和本地代码之间的数据交换需要进行双向转换,这可能导致性能瓶颈。
 
为了解决这些问题,Panama 项目引入了多种工具和 API,包括:
- Foreign Function and Memory API:用于分配和访问堆外内存,并直接从 Java 调用外部函数。
 - 矢量 API:在 Java 中执行矢量计算,提升性能。
 - JExtract:自动生成 Java 与本地代码绑定的工具。
 
本文将重点介绍 Foreign Function and Memory API(简称 FFM API)。
外部存储器访问
在 Java 中,通过 new 关键字创建的对象存储在 JVM 的堆内存中,由垃圾回收器管理。然而,垃圾回收可能带来不可预测的性能开销,因此许多性能关键的库更倾向于使用堆外内存,由本地语言自行管理分配和释放。
Java 过去通过 ByteBuffer API 和 sun.misc.Unsafe 提供堆外内存访问,但它们各有局限性:
- ByteBuffer API:内存区域大小限制为 2GB,且内存释放由垃圾回收器控制,而非开发者。
 - sun.misc.Unsafe:提供了过于细粒度的控制,容易导致悬空指针等错误。
 
FFM API 提供了一种更平衡的方式来访问堆外内存,同时保证安全性和灵活性。

外部内存的分配与释放
内存段
内存段是连续内存块的抽象表示,可以位于堆外或堆上。通过定义内存地址范围(空间边界)和生命周期(时间边界),内存段确保了内存访问的安全性。内存段分为以下几种类型:
- 本机段:分配在堆外内存中(类似于 
malloc)。 - 映射段:使用映射文件的堆外内存区域(类似于 
mmap)。 - 堆上段:基于 Java 数组或字节缓冲区的内存段。
 
竞技场
竞技场(Arena)定义了它分配的内存段的边界。例如:
MemorySegment segment = Arena.global().allocate(100);
上述代码分配了 100 字节的内存,地址范围为 b 到 b+99,其中 b 是基址。竞技场有多种类型,具体如下表所示:

FFM API 提供了 SegmentAllocator 接口,用于抽象内存段的分配和初始化。Arena 类实现了该接口,支持分配本机内存段。
操纵和访问结构化外部存储器
内存布局
MemoryLayout 用于声明性地描述内存段的内容。以下是几种常见的内存布局:
- ValueLayout:用于建模基本数据类型,定义了大小、对齐方式和字节顺序等。
 - SequenceLayout:表示元素布局的重复序列。
 - GroupLayout:表示多个不同成员布局的组合,分为:
- 结构布局:成员按顺序排列。
 - 联合布局:成员共享相同的起始偏移量。
 
 - PaddingLayout:用于对齐成员布局,内容可忽略。
 
调用外部函数
符号查找
调用外部函数的第一步是找到目标函数或全局变量的地址。FFM API 提供了 SymbolLookup 接口,用于在本地库中查找符号。例如:
SymbolLookup lookup = SymbolLookup.libraryLookup(Paths.get(pathToLibrary), arena);
MemorySegment testSymbol = lookup.find("test")
    .orElseThrow(() -> new RuntimeException("找不到符号 'test'"));
FFM API 提供了以下三种 SymbolLookup 对象:
libraryLookup(String, Arena):定位用户指定库中的符号。loaderLookup():查找当前类加载器加载的本地库中的符号。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
     热门API
- 1. AI文本生成
 - 2. AI图片生成_文生图
 - 3. AI图片生成_图生图
 - 4. AI图像编辑
 - 5. AI视频生成_文生视频
 - 6. AI视频生成_图生视频
 - 7. AI语音合成_文生语音
 - 8. AI文本生成(中国)
 
最新文章
- API协议设计的10种技术
 - ComfyUI API是什么:深入探索ComfyUI的API接口与应用
 - 从架构设计侧剖析: MCP vs A2A 是朋友还是对手?
 - Kimi Chat API入门指南:从注册到实现智能对话
 - 免费查询公司注册信息API的使用指南
 - 防御 API 攻击:保护您的 API 和数据的策略
 - 香港支付宝实名认证:是什么?怎么用?
 - 如何获取 Coze开放平台 API 密钥(分步指南)
 - 如何保护您的API免受自动化机器人和攻击 | Zuplo博客
 - ASP.NET Core Minimal APIs 入门指南 – JetBrains 博客
 - 什么是 OpenReview
 - Vue中使用echarts@4.x中国地图及AMap相关API的使用