Java的Panama项目与JNI以及外部函数接口FFI


微服务和快速启动的Dockers容器普及的大背景下,Java还在卯足劲纠结于本地环境的交互上,从JNI、JNA到JNR再到Panama,真是有钱很任性,如同当年在J2me、JavaFX撒欢一样:
Project Panama 是最新的 Java 项目,旨在简化和改进 Java 中的 FFI,作为其中的一部分,目前正在孵化许多提案。

外来函数接口FFI是指从另一种编程语言中调用以一种编程语言编写的函数或程序的能力。
FFI的大多数用例都是围绕着与传统应用程序的互动和访问主机操作系统功能或本地库。但最近机器学习和高级算术的激增使得FFI变得更加必要(banq:机器学习可选择库很多,Java前面排队很多呢)。如今,我们将外部分函数用于一系列用例,其中一些是。

  • 与遗留的应用程序互动
  • 访问语言中没有的功能
  • 使用本地库
  • 访问主机操作系统上的函数或程序
  • 多精度算术,矩阵乘法
  • GPU和CPU卸载(Cuda、OpenCL、OpenGL、Vulcan、DirectX等)
  • 深度学习(Tensorflow、cuDNN、Blas等)
  • OpenSSL,V8,以及更多

Java Native Interface (JNI)
长期以来,Java中FFI的标准一直是Java Native Interface(JNI),它以缓慢和不安全而闻名。如果你习惯于其他语言,如Rust、Go或Python,你可能知道在它们中使用FFI是多么容易和直观,而这在Java中还有待改进。即使是使用JNI做一个小的本地调用,你也要做相当多的工作,而且还可能出错,最终成为应用程序的安全问题。

JNI的主要问题是其使用的复杂性和需要手动编写C桥代码。这些问题会导致不安全的代码和安全风险。在某些情况下,这也会导致性能开销。JNI代码的性能和内存安全取决于开发者,因此可靠性会有所不同。

优点

  • C/C++/Assembly的本地接口访问
  • 在Java中是最快的解决方案
缺点
  • 使用起来很复杂,而且很脆弱
  • 不太安全,可能导致内存安全问题
  • 有可能出现开销和性能损失
  • 难以调试
  • 依赖于Java开发人员手动编写安全的C语言绑定代码
  • 你需要为每个目标平台编译和发送C代码

Java Native Access (JNA)
JNI的复杂性催生了一些社区驱动的库,使在Java中进行FFI变得更加简单。Java Native Access(JNA)就是其中之一。它建立在JNI之上,至少使FFI更容易使用,特别是它消除了手动编写任何C绑定代码的需要,减少了内存安全问题的机会。不过,它还是有一些基于JNI的缺点,在很多情况下比JNI稍慢。然而,JNA被广泛使用并经过了实战检验,所以绝对是比直接使用JNI更好的选择。

优点

  • 对C/C++/Assembly的本地接口访问
  • 与JNI相比,使用起来更简单
  • 动态绑定,不需要手动编写任何C绑定代码
  • 广泛使用和成熟的库
  • 更好的跨平台支持
缺点
  • 使用反射
  • 建立在JNI之上
  • 有性能开销,可能比JNI更慢
  • 难以调试

Java Native Runtime (JNR)
另一个流行的选择是Java Native Runtime(JNR)。虽然没有像JNA那样广泛使用或成熟,但对于大多数使用情况来说,它更现代,性能比JNA更好。然而,在某些情况下,JNA可能表现得更好。

优点

  • C/C++/Assembly的本地接口访问
  • 易于使用
  • 动态绑定,不需要手动编写任何C绑定代码
  • 现代API
  • 与JNI的性能相当
  • 更好的跨平台支持
缺点
  • 构建在JNI之上
  • 难以调试


外存访问 API
第一个难题是外部内存访问 API。它最初是在 JDK 14 中孵化的,经过 3 次孵化后,一个新的JEP将它结合到 Foreign Function & Memory API 中。

  • 用于安全有效地访问 Java 堆之外的外部内存的 API
    • 针对不同类型内存的一致 API
    • 不损害 JVM 内存安全
    • 显式内存释放
    • 与不同的内存资源交互,包括堆外或本机内存
  • JEP-370 - JDK 14 中的第一个孵化器
  • JEP-383 - JDK 15 中的第二个孵化器
  • JEP-393 - JDK 16 中的第三个孵化器


外部链接器 API
使 FFI 成为可能的另一个重要部分是 Foreign Linker API。这首先在 JDK 16 中孵化,并在下一个修订版中合并到 Foreign Function & Memory API。

  • 用于静态类型、纯 Java 访问本机代码的 API
    • 专注于易用性、灵活性和性能
    • 对 C 互操作的初始支持
    • .dll在、.so或中调用本机代码.dylib
    • 创建一个指向 Java 方法的本地函数指针,该方法可以传递给本地库中的代码
  • JEP-389 - JDK 16 中的第一个孵化器

向量 API
接下来是向量 API,它对 FFI 至关重要,尤其是在机器学习和高级计算方面。

  • 用于可靠和高性能矢量计算的 API
    • 平台无关
    • 简洁明了的 API
    • 可靠的运行时编译和性能
    • 优雅的降级
  • JEP-338 - JDK 16 中的第一个孵化器
  • JEP-414 - JDK 17 中的第二个孵化器
  • JEP-417 - JDK 18 中的第三个孵化器

外部函数和内存 API
最后,Foreign Linker API 和 Foreign-Memory Access API 一起发展成为 Foreign Function & Memory API。它首先在 JDK 17 中孵化。

  • Foreign-Memory Access API 和 Foreign Linker API 的演变
    • 与前两个相同的目标和功能(易用性、安全性、性能、通用性)
  • JEP-412 - JDK 17 中的第一个孵化器
  • JEP-419 - JDK 18 中的第二个孵化器
  • JEP-424 - JDK 19 中的第一个预览版

Panama API
使用新的 Panama API,您可以通过两种不同的方式执行相同的操作,即手动查找和加载本机函数或使用 jextract 工具。
在第一种情况下,您只需使用 CLinker API 编写一些 Java 代码。您查找本机方法并调用它;就这么简单。您还可以做更复杂的事情,例如使用本机内存等。通过这种方法,您可以直接使用 Foreign Linker API 和 Foreign Memory API 进行本机调用和管理本机内存。这不是最有效的方法,因为这需要您编写大量样板代码,并且在使用大型 C 头文件时扩展性不强。
第二种选择是使用 jextract。使用 jextract,上面的整个过程可以变成一行代码。使用 jextract,您可以获得用于本机程序的纯 Java API,并且您无需编写任何本机代码或接触任何头文件。jextract 使用外部链接器和外部内存 API 生成所有内容。是不是很厉害!这就是你在 Go 和 Rust 等语言中获得的那种 FFI 体验。
对于简单的本地调用,您可以使用第一种方法,但对于复杂的调用,第二种方法更好且可扩展。

详细点击标题