资讯动态

告别JNI:Java 22 FFM API实战指南,安全高效调用C函数

发布时间:2026/10/18 5:58:16 来源:尧图企业网站定制
“调 C 函数”这个词在过去很长一段时间里就等于“写 JNI”javac -h生成头文件手写 C 包装函数再处理jstring、jintArray这类类型搬运最后还要祈祷运行时不崩。我早期做项目时凌晨三点抱着 gdb对着一个SIGSEGV查 Java 和 C 内存布局的画面到现在都记忆犹新。所以 Java 22 把 Foreign Function Memory APIFFM从孵化器里转正之后我的态度很直接JNI 能不用就尽量不用了。这篇不是 API 文档翻译更像是我把几个项目从 JNI 迁到 FFM 之后的一份实操笔记。我会从“为什么值得换”讲起把Linker、SymbolLookup、FunctionDescriptor、Arena、MemorySegment这几个核心角色拆开再给可以直接抄的示例和我在实际使用中踩过的坑。适合两类人已经被 JNI 折腾过的老伙计以及刚入行、想直接走新路的新手。1. 为什么是 FFMJNI 的麻烦你不必再忍1.1 JNI 到底烦在哪JNI 的问题不是“能不能用”而是“成本全藏在你没注意的地方”。表面上看流程很标准Java 里声明native方法javac -h生成 C 头文件再用 C 实现这个函数编译成.so或.dll最后通过System.loadLibrary加载。但只要你多调几个函数马上就会发现这套流程的笨重。第一层麻烦是胶水代码。每暴露一个函数给 C就要写一套Java_xxx_xxx命名的导出函数。如果这个函数要传字符串、数组、结构体你还要在 Java 数据类型和 C 数据类型之间来回做转换。比如传一个字符串JNI 层拿到的jstring不能直接被 C 使用得调用GetStringUTFChars用完记得ReleaseStringUTFChars。如果忘了释放内存就慢慢漏。一个项目里的 JNI 代码多了以后光这些“管家式”的搬运逻辑就占了大半。第二层麻烦是编译和依赖。JNI 代码不能只写一次你得维护每个平台的头文件路径、编译器参数、链接参数。换一个操作系统或者换一个 C 编译器可能又要重新配一遍构建脚本。更难受的是如果调用的库本身有更新JNI 层的签名也要同步改改了之后还要重新编译链路很长。第三层麻烦才是真正让运维头疼的东西运行时安全。C 代码里一个指针传错JVM 直接崩掉连 Java 的try-catch都帮不上忙。JNI 的互操作层缺乏“作用域控制”和“边界检查”的概念你拿到的地址是裸地址C 函数越界写坏内存Java 侧完全感知不到。段错误、堆损坏很多时候只能在事后看hs_err_pid日志定位。1.2 FFM 是什么从 Panama 项目走出的正规军FFM 全称是 Foreign Function Memory API在 JDK 22 通过 JEP 454 转正包名从孵化期的jdk.incubator.foreign转正为java.lang.foreign。它的目标很明确让你在 Java 里以“声明式”的方式调用外部函数并安全地访问外部内存不需要写 C 包装层也不需要头文件。它其实是由 Panama 项目孵化多年的产物。这个项目解决的核心命题就是“跨语言互操作要像写 Java 一样自然”。你可以把它理解为 Java 和 C 之间的一位专业翻译你告诉它 C 函数长什么样签名它负责生成调用指令、处理 ABI 对齐、安排参数传递。你在 Java 侧看到的还是一个MethodHandle可以直接invoke调用。优势在于FFM 把“类型描述”和“内存管理”作为一等公民。FunctionDescriptor描述 C 函数签名MemoryLayout描述结构体内存排布Arena控制内存生命周期。这套体系比 JNI 的手工搬砖要严谨得多很多原本会直接段错误的操作在 Java 侧会因为作用域检查而抛异常至少你能知道错在哪。1.3 JNI vs FFM一张表看清前后变化对比项JNIFFM头文件需要javac -h生成并维护不需要C 胶水代码每个函数都要写 native 导出层不需要类型映射手工处理jstring、数组、结构体用MemoryLayout描述内存生命周期全手动容易泄漏Arena统一管理自动释放编译依赖要为每个平台编译 .so/.dll通常不编译 C 代码运行期错误各种SIGSEGVJVM 直接崩溃很多错误会抛异常ABI 适配自己处理不同平台有差异Linker.nativeLinker()按平台 ABI 适配调 C 函数指针复杂支持 upcall可让 C 调用 Java 回调这张表不是要否定 JNI 存在的价值。JNI 目前仍然是访问 JVM 内部机制的“官方通道”。但如果你只是想调用普通 C 函数、处理外部内存FFM 是现代 Java 环境下更顺手的选择。2. 核心概念理解 FFM 的四个关键角色2.1 Linker 与 SymbolLookup去动态库里“捞”函数第一次用 FFm 时我发现它的模型比 JNI 清晰得多。你不需要让 Java 编译器生成什么头文件也不用在 C 侧导出特定命名的函数。你只需要两样东西函数的运行时地址以及函数签名描述。Linker是这个流程的入口。Linker.nativeLinker()会返回一个与当前平台 ABI 匹配的链接器。这个链接器知道怎么生成函数调用指令也知道怎么处理结构体传参、寄存器分配等底层细节。SymbolLookup则负责“找符号”linker.defaultLookup()返回的查找器能直接查 libc 动态库里的符号。举个例子我要找strlen这个 C 函数代码很直接Linker linker Linker.nativeLinker(); SymbolLookup libc linker.defaultLookup(); MemorySegment strlenSymbol libc.find(strlen).orElseThrow();SymbolLookup.find()返回一个OptionalMemorySegment这个MemorySegment可以理解为一个“符号地址”。找不到符号时抛出异常而不是像 JNI 那样等到调用时才莫名其妙失败。如果你的库不在默认 libc 里可以用SymbolLookup.libraryLookup(libfoo.so, arena)打开特定动态库。这个方法需要传入一个Arena用于控制库句柄的关闭时机。2.2 FunctionDescriptor用 Java 声明 C 函数的“简历”拿到符号地址之后下一步是告诉Linker这个 C 函数长什么样。这就是FunctionDescriptor的职责。它描述两个东西返回值的类型布局以及参数列表的类型布局。比如strlen的 C 签名是size_t strlen(const char *s)。对应到 FFMFunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS);ValueLayout.JAVA_LONG描述返回值表示 C 层的size_t在大多数 64 位平台上对应 Java 的 8 字节长整型。ValueLayout.ADDRESS描述参数表示const char*是一个地址类型。这里需要理解“布局”这个词它不仅仅是“类型”还包含了字节宽度、对齐方式、字节序等信息。这也是 FFM 比简单类型映射更严谨的地方。FFM 中常用的ValueLayout常量有这些JAVA_BYTE、JAVA_SHORT、JAVA_INT、JAVA_LONG、JAVA_FLOAT、JAVA_DOUBLE、ADDRESS。它们分别对应 Java 的byte、short、int、long、float、double和内存地址。当 C 函数返回void时用FunctionDescriptor.ofVoid(...)。2.3 Arena 与 MemorySegment内存的分配、访问与回收JNI 时代最折磨人的是内存管理。外部内存谁分配、谁释放、生命周期持续到什么时候全靠自觉。FFM 用Arena概念把这块理顺了。Arena可以理解为一个“内存作用域容器”。你可以通过Arena.ofConfined()创建一个单线程作用域的 Arena也可以用Arena.ofShared()创建多线程共享的 Arena还有Arena.global()这种永不关闭的全局 Arena。最常用的写法是结合 try-with-resourcestry (Arena arena Arena.ofConfined()) { MemorySegment text arena.allocateFrom(Hello, FFM!); // 使用 text }当退出 try 块时这个 Arena 管理的所有内存都会被自动释放。如果你在释放之后还尝试访问对应的MemorySegmentFFM 会抛出IllegalStateException而不是给你一个神秘的段错误。如果你要分配一块指定大小的原生内存可以用arena.allocate(1024)。如果你需要按某个结构体布局分配可以用arena.allocate(MEMORY_LAYOUT)。MemorySegment是 FFM 对外部内存的统一抽象。它不像byte[]那样在 Java 堆里而是在堆外原生内存中。你可以通过segment.set(ValueLayout.JAVA_INT, offset, value)往指定位置写值通过segment.get(ValueLayout.JAVA_INT, offset)读值。这种“段 布局 偏移量”的方式让结构体的读写变得很自然。2.4 MethodHandle调用 Native 代码的入口FFM 调用 C 函数时返回的是MethodHandle。MethodHandle是 Java 里已有的高性能方法调用抽象这里被复用为“外部函数句柄”。把符号地址和FunctionDescriptor交给Linker.downcallHandle()就能得到一个可调用的MethodHandleMethodHandle strlen linker.downcallHandle( libc.find(strlen).orElseThrow(), FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS) );之后就可以像调用普通方法句柄一样invoke。MethodHandle在类型检查上非常严格参数类型不匹配会抛WrongMethodTypeException而不是到了 C 层才出问题。这也是迁移之后感觉“更踏实”的原因之一。需要注意的是每次创建MethodHandle都涉及符号查找、ABI 分析等开销所以实际项目中应该把句柄缓存起来而不是每次都创建。通常我会把它放在静态成员位置初始化一次后续复用。3. 实操演练三个经典场景快速上手3.1 第一个 C 函数调用 strlen 计算字符串长度先写一个可以直接运行的完整示例。这个例子不需要头文件不需要编译 C 代码只要你的 JDK 是 22 及以后版本代码可以直接跑import java.lang.foreign.*; import java.lang.invoke.MethodHandle; public class FfmStrlen { public static void main(String[] args) throws Throwable { Linker linker Linker.nativeLinker(); SymbolLookup libc linker.defaultLookup(); MethodHandle strlen linker.downcallHandle( libc.find(strlen).orElseThrow(), FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS) ); try (Arena arena Arena.ofConfined()) { MemorySegment text arena.allocateFrom(Hello, FFM!); long length (long) strlen.invoke(text); System.out.println(strlen length); } } }输出结果是strlen 11字符串Hello, FFM!正好 11 个字符。这里第一个关键点是arena.allocateFrom(Hello, FFM!)。这个方法会分配一块原生内存把 Java 字符串按 UTF-8 编码写入并在末尾自动补上\0终止符。strlen在 C 层看到的就是一个合法、完整的 C 字符串。第二个关键点是strlen.invoke(text)直接传入MemorySegment。当FunctionDescriptor中参数布局是ValueLayout.ADDRESS时对应的方法句柄参数类型就是MemorySegment所以你不需要去取“地址数值”直接把段传进去即可。这也是很多人初次接触时容易混淆的地方不要再主动调用segment.address()去转long除非你特意把布局改成JAVA_LONG否则类型对不上。3.2 与 C 互操作分配原生内存并调用 malloc/free调strlen这类只读函数还不过瘾我们通常需要自己分配内存、写入数据、再交给 C 库处理。JNI 时代这块是重灾区。FFM 下手动分配同样可行只是要注意“C 库分配的内存”和“Arena 分配的内存”不是一回事。import java.lang.foreign.*; import java.lang.invoke.MethodHandle; public class FfmMallocFree { public static void main(String[] args) throws Throwable { Linker linker Linker.nativeLinker(); SymbolLookup libc linker.defaultLookup(); MethodHandle malloc linker.downcallHandle( libc.find(malloc).orElseThrow(), FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.JAVA_LONG) ); MethodHandle free linker.downcallHandle( libc.find(free).orElseThrow(), FunctionDescriptor.ofVoid(ValueLayout.ADDRESS) ); try (Arena arena Arena.ofConfined()) { MemorySegment raw (MemorySegment) malloc.invoke(128L); if (raw.address() 0) { throw new OutOfMemoryError(malloc returned null); } // 这里 raw 只是一个“不知道大小”的段先用 reinterpret 按 128 字节重新解释 MemorySegment block raw.reinterpret(128L, arena, null); block.set(ValueLayout.JAVA_INT, 0, 42); System.out.println(block.get(ValueLayout.JAVA_INT, 0)); // C 库分配的内存要主动释放Arena 关闭不会帮你调 free free.invoke(raw); } } }这里特别说明一下raw.reinterpret(128L, arena, null)。通过malloc返回的MemorySegment是一个地址但 FFM 并不知道它具体指向多大范围的内存所以它默认是个“零长度段”。如果直接对它调用set或get会得到边界访问异常。reinterpret的作用就是告诉 FFM这块区域按多大尺寸看待同时可以绑定到一个Arena和清理动作。上述示例传入null作为 cleanup因为我们自己会手动free。实际上更省心的方式是让Arena关闭时自动调用freeMemorySegment block raw.reinterpret(128L, arena, segment - { try { free.invoke(segment); } catch (Throwable e) { throw new RuntimeException(e); } });这样只要try块结束free就会被自动调用。用不用这种写法取决于内存生命周期由谁掌控。我的习惯是如果这段内存的存活范围和一个Arena完全重合就让 Arena 负责如果需要跨作用域传递就自己管理并保证不会重复free。3.3 传递结构体数据用 MemoryLayout 映射 C struct结构体是 JNI 里最让人头大的部分。JNI 里没有直接映射 Cstruct的类型你得先把结构体拆成基本字段再把字段打包进 Java 对象最后传到 C 层还要重新组装。FFM 用MemoryLayout.structLayout(...)把这个过程简化了。假设 C 侧定义了一个二维坐标结构体typedef struct { int x; int y; } Point;Java 侧对应的布局描述MemoryLayout POINT_LAYOUT MemoryLayout.structLayout( ValueLayout.JAVA_INT.withName(x), ValueLayout.JAVA_INT.withName(y) );之后在一个 Arena 中分配一块符合这个布局的内存try (Arena arena Arena.ofConfined()) { MemorySegment point arena.allocate(POINT_LAYOUT); point.set(ValueLayout.JAVA_INT, 0, 10); point.set(ValueLayout.JAVA_INT, 4, 20); long x point.get(ValueLayout.JAVA_INT, 0); long y point.get(ValueLayout.JAVA_INT, 4); System.out.println(point ( x , y )); }注意字段偏移量POINT_LAYOUT中x占 4 字节y紧跟其后偏移就是 4。这类简单结构体没有对齐缝偏移可以手工算。如果字段里有double或者指针最好用layout.byteOffset(MemoryLayout.PathElement.groupElement(x))这种方式获取偏移不要手算因为结构体对齐规则有时会插入 padding手算容易翻车。如果要传给一个接受Point*的 C 函数只需要把point这个MemorySegment作为参数传进 downcall handle 即可。ADDRESS 布局的参数天然允许传入结构体段的地址。再进一步如果你的 C 库需要 Java 回调比如qsort的比较函数那就涉及 upcall。Linker.upcallStub()可以把一个 Java 的MethodHandle包装成 C 函数指针MethodHandle compare MethodHandles.lookup().findStatic( FfmQSort.class, compare, MethodType.methodType(int.class, MemorySegment.class, MemorySegment.class) ); MemorySegment compareStub linker.upcallStub( compare, FunctionDescriptor.of( ValueLayout.JAVA_INT, ValueLayout.ADDRESS.withTargetLayout(ValueLayout.JAVA_INT), ValueLayout.ADDRESS.withTargetLayout(ValueLayout.JAVA_INT) ), arena );这样 C 侧qsort执行到“比较两个元素”时会反向调用 Java 方法。JNI 时代这种回调玩法要写一堆静态 native 方法还要处理JNIEnv传递FFM 确实省掉了很多模板。3.4 调用可变参数函数时的“特殊处理”很多新手在第一次尝试 FFM 调printf时会碰壁FunctionDescriptor要求参数列表完整但 C 的printf是可变参数函数。FFM 本身没有专门声明“varargs”的语法实际处理方式是针对你想发起的某次调用把这次调用中真实会用到的额外参数补进FunctionDescriptor。比如调用printf(answer is %d\n, 42)那就按两个参数描述MethodHandle printf linker.downcallHandle( libc.find(printf).orElseThrow(), FunctionDescriptor.of( ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.JAVA_INT ) ); try (Arena arena Arena.ofConfined()) { MemorySegment format arena.allocateFrom(answer is %d\n); int result (int) printf.invoke(format, 42); }这种写法能不能成功取决于底层平台 ABI 下可变参数在寄存器/栈上的传递约定。好在Linker已经帮我们把 ABI 差异封装掉了你补充的参数只要类型完全匹配就好。如果参数类型给错了比如把JAVA_INT写成了JAVA_LONG运行结果很可能是一个毫无意义的输出甚至触发段错误。所以可变参数调用时务必让 descriptor 严格匹配实际入参。4. 细节与排查内存布局、生命周期和段错误的坑4.1 类型映射表Java 与 C 怎么对应我在实际使用中整理了一张常用映射表这不完全等同于 C 规范但在主流的 x86_64 Linux/macOS 环境里准确度足够C 类型FFM 布局方法句柄上的 Java 类型charJAVA_BYTEbyteshortJAVA_SHORTshortintJAVA_INTintlongJAVA_LONGlongsize_tJAVA_LONGlongfloatJAVA_FLOATfloatdoubleJAVA_DOUBLEdoublevoidFunctionDescriptor.ofVoidvoid指针 / 数组ADDRESSMemorySegmentstruct Point*ADDRESSwithTargetLayoutMemorySegment这里要特别强调“C long不等于 Javalong”这个细节。在 x86_64 Linux 上C 的long是 64 位和size_t宽度一致直接映射JAVA_LONG没问题。但 Windows 上 C 的long是 32 位如果你写死JAVA_LONG参数栈布局就会整体错位。所以我更推荐的做法是在代码注释里写明目标平台跨平台代码尽量用JAVA_INT对应 Cint用显式宽度的布局去处理指针和size_t。FFM 提供的VaList和AddressLayout.withTargetLayout等高级 API 也可以用于处理更复杂的指针语义。关于 ADDRESS 配合withTargetLayout它能补充指针指向的对象布局比如int*写成ValueLayout.ADDRESS.withTargetLayout(ValueLayout.JAVA_INT)在 upcall 场景尤其有用。4.2 内存生命周期Arena 的作用域与释放时机Arena 不是花架子它解决的是 JNI 时代最头大的问题“这些内存什么时候能释放”。我的建议是保持一个 Arena 只服务于一个短任务。try (Arena arena Arena.ofConfined()) { // 短任务分配、调用、读取 }这种模式下所有临时缓冲都随着任务结束一起释放不会有跨线程的悬垂指针。Arena.ofConfined()表示该 Arena 只能被创建它的线程访问简单场景下最安全、性能也最好。Arena.ofShared()适合多个线程共同使用同一块内存缓冲区代价是内部会增加同步开销。真正容易出问题的是把对象生命周期跨出了 Arena 作用域。比如你把一个MemorySegment存进了全局缓存然后关闭了创建它的 Arena之后再拿出来用就会得到异常。FFM 的设计就是故意用这种检查让悬垂指针问题在 Java 层暴露出来而不是留到 C 层变成段错误。如果你确实需要跨作用域长期保存一段 C 库返回的内存两个选择一是用Arena.global()但这意味着内存要“手动”管理因为全局 Arena 永不关闭二是用reinterpret(..., customArena, cleanup)让新的 Arena 接管生命周期并指明释放动作。我倾向于后者因为至少释放逻辑是显式的。4.3 段错误排查地址、越界与生命周期FFM 不可能彻底消灭段错误因为它给了你一把能直接访问底层内存的钥匙。就我的经验段错误集中在三种情况。第一种传给 C 函数的MemorySegment地址是有效的但 C 函数自身对缓冲区长度假设错误写越界了。比如你分配了 64 字节C 函数却按 4KB 的缓冲区来写。这种问题 FFM 帮不了你只能在调用前严格核对 C 库的 API 文档必要时多分配一些内存作为余量。第二种把一个已经关闭的 Arena 的段地址传给了 C 函数然后 C 函数去读写这块内存。此时 C 侧拿到的是一个“已失效”的指针。Java 侧会先尝试检查段作用域但如果你通过address()拿裸地址传过去Java 就无从保护了。我的底线是调用 C 函数时优先传MemorySegment而不是long地址让 FFM 至少保留检查能力。第三种C 库内部维护了一个指针等它下次使用时候Java 侧对应的 Arena 已经关闭。比如你调用某个库的set_callback_data(ptr)传了一个 Arena 分配的缓冲区之后这个 Arena 关了但 C 库还持有这个指针。之后库内部再访问轻则读到垃圾数据重则段错误。对策是确保 C 库持有引用期间对应的 Arena 一直存活。一旦真的崩了先把hs_err_pid*.log中Java frames和Native frames对照看确认崩在哪个库函数。再检查是不是最上面的三种情况之一。JNI 时代我只能靠 gdb 硬查FFM 下至少错误通常还能定位到参数交互层面。4.4 跨平台与 ABI 差异不是所有 long 都一样FFM 的Linker.nativeLinker()会自动选择当前平台对应的 ABI这已经比 JNI 手工编译省心很多但跨平台时仍要小心符号名差异。比如 Windows 的 C 运行库里printf这类函数在符号层面可能有下划线前缀或不同名称。Linker.defaultLookup()在不同平台检索的库也不一样不能假设所有 C 库函数都能直接找到。当你需要调用某个特定第三方库时要用SymbolLookup.libraryLookup(/path/to/libfoo.so, arena)显式加载。路径的处理最好走配置化不要写死否则项目换个部署环境就崩。Windows 下动态库后缀是.dllLinux 是.somacOS 是.dylib区分这些是基础功课。结构体对齐也要跨平台验证。C 编译器的默认对齐规则和 Java 的MemoryLayout.structLayout默认对齐规则在大多数平台是一致的但如果你用了#pragma pack这类“紧凑结构体”Java 侧就要手动给MemoryLayout加withByteAlignment或用填充字段手动补齐。我在一个项目里就遇到过结构体字段顺序一致、位数一致但 C 编译器按 4 字节对齐Java 默认按 8 字节对齐算出的字段偏移完全对不上。后来打印出两边sizeof/byteOffset一对比马上定位到对齐策略差异。5. 实战心得常见问题速查表与排错实例5.1 常见错误场景速查把这些错误整理成表方便大家快速对照现象可能原因解决方向SymbolNotFoundException默认库中找不到该符号确认符号名改用libraryLookup加载目标库调用时WrongMethodTypeExceptionFunctionDescriptor参数布局与实际 Java 参数类型不一致检查布局对应 Java 类型ADDRESS 对应MemorySegment调用后返回奇怪数据布局大小或对齐不对C 函数读取了错误字节核对结构体byteOffset和byteSizeIllegalStateException: segment already closed访问了已关闭 Arena 的内存检查对象生命周期避免跨 Arena 保存引用直接段错误JVM 崩溃C 库写入越界或持有了失效地址检查 C 库文档、缓冲区大小尽量传MemorySegment而非裸地址内存泄漏持续上涨Arena 未关闭或 C 库分配的内存未 free用 try-with-resourcesreinterpret绑定 free 清理动作找不到第三方库符号.so/.dll未加载或依赖缺失用libraryLookup指定路径检查库的依赖链5.2 一次真实排错过程之前帮某个项目组排查过一个“调用第三方图像处理库返回结果偶尔错误”的问题。那位开发者已经在用 FFM 了但一到并发场景部分图片的数据就会错乱。一开始怀疑是 C 库线程安全问题后来发现不是。问题的实质是C 库返回了一个指针指向库内部分配的内存开发者把它包装成MemorySegment后使用。但这个返回的段在 FFM 中被视为“零长度段”开发者虽然用reinterpret指定了大小却没有注意到reinterpret需要绑定正确的 Arena 生命周期。因为原始接口并没有把返回内存的释放职责交代清楚他们用一个短生命周期 Arena 去绑定结果图片处理异步任务还没结束Arena 就已经关闭。后续访问时Java 侧报异常倒还好问题最诡异的是某些路径直接通过address()拿出了原始地址绕过了 FFM 的检查于是拿到了已释放内存数据随机错乱。排查过程其实不长先跑单线程不出问题加并发后偶发把错误触发的图片反复跑发现稳定复现后往 FFM 的 Arena 生命周期上查打印关闭时间点和异常发生时间点一对照就明白了。解决办法是让承载图片数据的 Arena 生命周期覆盖整个异步任务并在任务结束时显式关闭。这种问题如果发生在 JNI 时代可能要排查数天FFM 下因为作用域是显式对象定位要快得多。5.3 什么情况下你还是得用 JNI说了这么多 FFM 的好处还是要保持客观。有些场景 FFM 现在仍不能替代 JNI。首先要访问 JVM 内部能力时比如开发 Java 调试工具、Agent 工具、需要调用 JVMTI 接口JNI 依然是官方通道。其次你的现有代码库已经积累了大量 JNI 函数全部重写成本很高短期内维护旧代码可能是更现实的选择。另外如果项目还停留在 JDK 21 或更早版本又不想用孵化器 API那也只能先用 JNI。FFM 在 JDK 22 前一直在jdk.incubator.foreign里API 会有调整生产环境用它需要一个明确的升级计划。最后说一点个人体会。把现有项目从 JNI 迁到 FFM 的时候我最大的感觉是写 C 的互操作终于回到了“写 Java”的思维类型描述、内存作用域、异常这些都在语言和 API 层有明确表达。头文件、javac -h、C 包装层、平台编译脚本这些旧时代的负担能不背就不背。我建议你可以先从一两个低频小函数开始比如从 C 库计算哈希、读系统时间这类无副作用操作试水跑通之后再逐步扩大范围。你会明显感觉到调试互操作代码的“心理成本”真的比 JNI 时代低了很多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑