初步了解 Panama

Project Panama 是 Java 社区中一项重要的技术发展,旨在改善和丰富 JVM(Java虚拟机)与本地代码之间的互操作性。这项计划的核心目标是减少使用 JNI(Java Native Interface)时的复杂性和性能开销,从而使得 Java 应用程序能够更高效地利用本地库的功能 。

起源

Project Panama 的起源可以追溯到对现有 JNI 技术限制的认识。JNI 在 Java 程序需要调用 C/C++ 编写的本地代码时扮演了重要角色,但它的使用通常比较繁琐,并且存在一定的性能损耗。为了克服这些问题,Project Panama 提出了 Foreign Function & Memory API 作为解决方案的一部分,它允许 Java 应用程序直接调用和操作本地内存和函数 。

自2020年起,关于 Project Panama 的讨论和开发工作逐渐增多。到了2023年,JEP 417 和 JEP 419 为实现 Panama 项目提供了持续的支持,这些增强措施旨在改进 JVM 与非 Java API 之间的互操作性,特别是那些在 C 程序库中常用的接口 。

随着时间推移,Panama 项目不断成熟,并在 Java 20 中实现了显著的进展。例如,JEP 434 和 JEP 438 属于 Panama 项目的一部分,它们进一步增强了 Java 与外部 API 的交互能力 。此外,在 Java 21 中,我们可以看到更多来自 Amber、Loom、Panama 和 Valhalla 这四个主要 Java 项目的创新特性 。到了 JDK 22,Oracle 宣布了 GA(General Availability)版本的发布,其中包含了对外部函数与内存 API 的最终确定,这意味着从 Java 22 开始,FFM API 基本上不会有大的改动,开发者可以期待其稳定性和长期支持

Hello World

要实现在 Java 中调用 C 的 printf 方法,需要搜索 printf 函数的本机内存地址,之后调用该方法(环境: JDK 23

// 1. 获取系统链接器
val linker = Linker.nativeLinker()
// 2. 获取标准库的符号查找对象
val stdlibLookup = linker.defaultLookup()

// 3. 查找 printf 函数
val printfSymbol = stdlibLookup.find("printf").getOrNull()?:throw IllegalStateException("printf not found")

// 4. 构建 printf 函数的描述符
// 函数返回值是 int,参数是 指针地址
val printfDescriptor = FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS)

// 5. 创建 printf 方法句柄
val printfHandle = linker.downcallHandle(printfSymbol, printfDescriptor)


Arena.ofConfined().use { arena ->
    val cString = arena.allocateFrom("123")
    val result = printfHandle.invokeExact(cString) as Int
    println("printf result: $result")
}

在 Java 中通过 JNI 调用本地代码的传统方式较为繁琐,而 Project Panama 提供的 Foreign Function & Memory API(JEP 454)极大简化了这一过程。让我们通过示例代码解析关键组件:


1. Linker(链接器)

val linker = Linker.nativeLinker()
  • 作用:作为 Java 与本地代码之间的桥梁,负责处理调用约定(Calling Convention)、参数/返回值类型转换等底层细节
  • nativeLinker():获取与当前操作系统和 CPU 架构匹配的本地链接器
  • 特点:自动处理平台差异(如 x86_64 和 ARM 的不同调用规则)

2. Lookup(符号查找)

val stdlibLookup = linker.defaultLookup()
  • 作用:用于在本地库中查找函数/变量符号(Symbol)
  • defaultLookup():获取标准 C 库(如 Linux 的 libc、Windows 的 msvcrt.dll)的符号查找器
  • 自定义库查找
    linker.lookup("mylib.dll") // 加载自定义库
    

3. Symbol(函数符号)

val printfSymbol = stdlibLookup.find("printf").getOrNull()
  • 作用:代表本地函数在内存中的入口地址
  • 查找过程:通过符号名称(区分大小写)在加载的库中定位函数
  • 错误处理getOrNull() 确保找不到符号时不会抛出异常

4. FunctionDescriptor(函数描述符)

val printfDescriptor = FunctionDescriptor.of(
    ValueLayout.JAVA_INT,  // 返回值类型
    ValueLayout.ADDRESS    // 参数类型(指针)
)
  • 作用:声明本地函数的签名(参数类型 + 返回值类型)
  • 类型映射
    • JAVA_INT → C 的 int
    • ADDRESS → C 的指针类型(如 char*
    • 其他类型:JAVA_LONGlongJAVA_FLOATfloat
  • 可变参数处理:示例代码简化了 printf 的可变参数特性,实际使用时需要完整声明参数列表

5. Downcall Handle(调用句柄)

val printfHandle = linker.downcallHandle(printfSymbol, printfDescriptor)
  • 作用:将符号与函数描述符绑定,生成可调用的方法句柄
  • 类型安全:确保调用时参数类型与描述符声明一致
  • 性能优化:JVM 会生成高效的调用桩(Stub)

完整调用流程

Arena.ofConfined().use { arena ->  // 内存作用域管理
    val cString = arena.allocateFrom("123")  // 分配堆外内存
    val result = printfHandle.invokeExact(cString) as Int  // 精确调用
    println("printf result: $result")  // 输出返回值(写入字符数)
}
  1. 内存分配:通过 Arena 自动管理 C 字符串内存(等价于 malloc + free
  2. 类型转换:Java String → C 字符串(自动添加 NULL 终止符)
  3. 方法调用:通过 MethodHandle 调用本地函数
  4. 结果处理printf 返回写入的字符数(示例中应返回 3)

与传统 JNI 的对比

特性传统 JNIForeign Function API
代码复杂度需要编写 C 胶水代码纯 Java/Kotlin 实现
内存管理手动管理通过 Arena 自动管理
类型映射需要定义 JNI 类型签名通过 ValueLayout 声明
性能有调用开销接近原生性能(通过 Stub)

通过这套 API,开发者可以用类型安全的方式直接操作本地内存和调用本地函数,显著简化了 Java 与本地代码的互操作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值