Java 平台自 Java 19(作为孵化模块)以来引入的外部函数与内存 API(Foreign Function and Memory API,简称 FFM API,JEP 442 已在 Java 22 中定稿)彻底改变了 Java 与原生代码的交互方式。它提供了一种安全、高效且纯 Java 的方法来调用 C 库函数和操作原生内存,有效地取代了复杂且危险的 JNI。
本文将通过一个简单的 C 语言求和示例,展示如何使用 FFM API 完成整个调用过程。
准备工作:创建 C 语言共享库
首先,我们需要一个简单的 C 函数,将其编译成动态链接库(例如 Linux/macOS 的 .so 文件,或 Windows 的 .dll 文件)。
1. C 源代码 (native_sum.c)
#include <stdio.h>
// 一个简单的求和函数
int sum_two_ints(int a, int b) {
printf("C: Calculating %d + %d\n", a, b);
return a + b;
}
2. 编译共享库
使用 GCC 编译为共享库。请确保在你的操作系统上路径正确。
# Linux/macOS
gcc -shared -o libnative_sum.so native_sum.c
# Windows (使用 MinGW)
gcc -shared -o native_sum.dll native_sum.c
使用 Java FFM API 调用 C 函数
FFM API 的核心在于三个步骤:查找链接器、定义函数签名、执行方法句柄 (MethodHandle)。
3. Java 代码 (FfmApiDemo.java)
import java.lang.foreign.FunctionDescriptor;
import java.lang.foreign.Linker;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.SymbolLookup;
import java.lang.invoke.MethodHandle;
import java.lang.foreign.ValueLayout;
public class FfmApiDemo {
public static void main(String[] args) throws Throwable {
// 1. 获取本地平台链接器 (Native Linker)
Linker linker = Linker.nativeLinker();
// 2. 查找动态库。这里假设库文件位于当前执行路径下。
// 注意:在 Windows 上,文件名可能是 native_sum.dll。
String libraryName = "libnative_sum.so";
SymbolLookup loader = SymbolLookup.libraryLookup(libraryName, linker);
// 3. 查找 C 函数符号 (sum_two_ints)
MemorySegment sumSymbol = loader.find("sum_two_ints")
.orElseThrow(() -> new IllegalStateException("C function 'sum_two_ints' not found"));
// 4. 定义函数签名 (FunctionDescriptor)
// C 签名: int sum_two_ints(int a, int b)
// Java 签名: (int, int) -> int
FunctionDescriptor sumDescriptor = FunctionDescriptor.of(
ValueLayout.JAVA_INT, // 返回值类型
ValueLayout.JAVA_INT, // 参数 a (int)
ValueLayout.JAVA_INT // 参数 b (int)
);
// 5. 准备 MethodHandle (执行 Downcall)
// linker.downcallHandle 将 C 函数地址和签名转换为 Java 可调用的方法句柄
MethodHandle sumHandle = linker.downcallHandle(sumSymbol, sumDescriptor);
// 6. 调用 C 函数
int a = 100;
int b = 23;
// invokeExact 用于严格匹配签名调用
int result = (int) sumHandle.invokeExact(a, b);
System.out.println("Java: Native call completed.");
System.out.println("Result of " + a + " + " + b + " is: " + result);
}
}
运行 Java 程序
由于 FFM API 涉及本地内存和函数访问,我们需要在运行时启用对非导出模块的本地访问权限。
# 编译
javac FfmApiDemo.java
# 运行 (使用 --enable-native-access 允许访问 C 库)
# 确保 libnative_sum.so 在当前路径或 java.library.path 中
java --enable-native-access=ALL-UNNAMED FfmApiDemo
预期输出:
C: Calculating 100 + 23
Java: Native call completed.
Result of 100 + 23 is: 123
FFM API 关键概念总结
- Linker (链接器): Linker.nativeLinker() 是访问原生环境的入口点,负责实际的函数调用转换(Downcall)。
- SymbolLookup (符号查找): 用于在当前进程或特定动态库中查找原生函数或全局变量的内存地址(即 MemorySegment)。
- ValueLayout (值布局): 定义了 Java 类型与 C 类型之间的映射关系,例如 ValueLayout.JAVA_INT 对应 C 的 int。
- FunctionDescriptor (函数描述符): 精确描述了 C 函数的参数类型序列和返回值类型,这是确保安全调用的关键。
- MemorySegment (内存段): FFM API 中用于抽象和管理原生内存区域的核心概念。无论是函数地址、结构体还是缓冲区,都通过 MemorySegment 表示。它具有明确的生命周期(通过 Arena 管理)和边界检查,大大提高了内存操作的安全性,避免了 JNI 时代常见的内存泄漏和越界访问问题。
汤不热吧