【FFI】 Java 侧演化——从 JNI 到 Panama

14 阅读 1796 字 · 约 6 分钟

Java 从 JDK 1.1 就能调 C,但这条路走了二十多年,一直在"方便"和"快"之间做取舍。四个方案串起来看,就是 Java FFI 的演化史。

JNI:最底层,也最麻烦

JNI(Java Native Interface)是 Java 调 C 的标准姿势。Java 侧写 native 方法,javac -h 生成 C 头文件,手写 C 实现:

// Java 侧
public class NativeLib {
    static { System.loadLibrary("mylib"); }
    public static native int fib(int n);
}
// C 侧,按 JNI 规范生成的函数名
JNIEXPORT jint JNICALL
Java_com_example_NativeLib_fib(JNIEnv *env, jclass cls, jint n) {
    return n <= 1 ? n : fib(n - 1) + fib(n - 2);
}

C 函数名是 Java_<包名>_<类名>_<方法名>,参数里多了一个 JNIEnv*(JVM 的指针,用来操作 Java 对象)和 jclass(调用类的 class 对象)。JNI 是静态绑定:编译期生成桩,调用开销低,但写起来极其繁琐——改个方法名要重新生成头文件、改个参数类型要重新编译 C 代码。

JNA:站在 libffi 上,无需编译

JNA(Java Native Access)把 JNI 的编译期拉到了运行时。它站在 libffi 上,不需要生成头文件:

import com.sun.jna.Library;
import com.sun.jna.Native;

public interface MyLib extends Library {
    MyLib INSTANCE = Native.load("mylib", MyLib.class);
    int fib(int n);
}

// 直接调
MyLib.INSTANCE.fib(40);

你定义一个 Java interface 映射 C 函数签名,JNA 运行时用 libffi 构造调用栈。不用写 C、不用 javac -h、不用编译 .so。代价是每次调用都要解析签名、marshal 参数,比 JNI 慢一截。适合探索性开发和胶水脚本。

JNR:JNA 的继任者

JNR(Java Native Runtime)是 JNA 的下一代。同样是动态绑定,但用了更激进的技术——运行时直接生成 JIT 编译的 native stub,不用 libffi 的通用栈帧构造,性能接近 JNI。

代码风格和 JNA 类似,但生态没跟上,用户量远不如 JNA。现在基本是 JNA 和 Panama 二分天下。

Project Panama:官方下场,一统江湖

Panama 是 JDK 16 开始孵化的官方方案,JDK 19 正式发布,目标就是替代 JNI。

核心理念:jextract 工具从 C 头文件自动生成 Java 绑定,不用手写任何 C 代码。

# 从 C 头文件生成 Java 绑定
jextract mylib.h --output target/classes

生成的 Java 代码用 MemorySegmentMethodHandle 直接操作 native 内存:

// Panama 生成的安全内存访问
MemorySegment segment = Arena.ofAuto().allocate(4);
segment.set(ValueLayout.JAVA_INT, 0, 42);
int val = segment.get(ValueLayout.JAVA_INT, 0);

MemorySegment 是 Panama 最大的区别——它提供了安全的 native 内存访问。JNI 里一个 void* 指针随时可能变成野指针,Panama 用 Arena.ofAuto() 自动管理生命周期:Arena 关掉,所有从它分配的内存一起释放。不用手写 free,不用怕 double free。

演进逻辑

方案绑定时机胶水代码性能内存安全
JNI编译期手写 C 头文件全靠人肉
JNA运行时(libffi)只写 Java全靠人肉
Panama编译期(jextract)自动生成Arena 自动管理

从 JNI 到 Panama,Java FFI 走了二十多年,核心演进方向只有一个:把"手写 C"这件事干掉。JNI 让你手写 C,JNA 让你只写 Java,Panama 让你什么都不写——jextract.h 文件直接生成,自动处理类型映射、内存安全、调用约定。和 PyO3 异曲同工:编译期把事情做完,运行时只管调用。