使用OpenJDK的JEP 454从Java访问本机C函数:外部函数和内存 API

Java 的稳健性和跨平台功能使其成为企业应用程序的主力。不过,在有些情况下,Java 应用程序需要与 C 或 C++ 等语言编写的本地库进行交互。Java 本地接口(JNI)一直是满足此类需求的传统解决方案,但它也有自身的复杂性和性能开销。OpenJDK 的 JEP 454 旨在提供一种更直接、更高效的替代方案。

这个 API 是 Panama 项目的一部分,旨在提高 Java 与本地代码交互的能力。在本文中,我们将通过一个简单的示例来演示如何使用 JEP 454 从 Java 程序中调用 C 函数。

需要注意的是,该 JEP 仍处于预览模式。要试用它,您需要 JDK 22。您可以使用 SDKMan 在系统中轻松安装 JDK 22。

JEP 454 有哪些内容?
Java 增强提案 (JEP) 454 旨在引入外来函数与内存 API,简化 Java 与本地代码之间的交互。API 由两个主要部分组成:

  • 外来函数接口(FFI):使 Java 程序能够轻松调用本地函数,抽象出 JNI 所需的大量模板代码。
  • 内存访问 API:提供一套与本地内存交互的工具,包括内存分配、取消分配和本地数据结构操作等功能。

JEP 454 的设计具有很高的性能,并包含各种安全检查,以防止缓冲区溢出等常见隐患。在大多数使用情况下,它将取代 JNI,为访问本地库和管理本地内存提供更高效、更安全的方法。

创建 C 语言库
首先,让我们创建一个包含两个整数相加的简单函数的 C 程序。代码如下

include <stdio.h>

int add(int a, int b) {
    return a + b;
}

编写代码后,将其保存在名为 addition.c 的文件中。要将其编译为共享库,请导航到文件所在目录并运行以下命令:

gcc -shared -o libaddition.so -fPIC addition.c

该命令将 C 代码编译成名为 libaddition.so 的共享库,以便 Java 程序访问。

编写 Java 程序
接下来,让我们编写一个 Java 程序,使用外来函数和内存 API 调用 C 库中的 add 函数。Java 代码如下:

import java.lang.foreign.*;
import java.nio.file.Path;

public class Main {
    void main() {

        try (var arena = Arena.ofConfined()) {
            var lib = SymbolLookup.libraryLookup(Path.of("libaddition.so"), arena);
            var linker = Linker.nativeLinker();
            var fd = FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.JAVA_INT);
            var addFunc = lib.find(
"add").get();

            var methodHandle = linker.downcallHandle(addFunc, fd);
            var sum = methodHandle.invoke(1, 2);
            System.out.println(
"sum = " + sum);
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }
}

将 "libaddition.so "替换为已编译 C 语言库的实际路径。

运行 Java 程序
要编译并运行 Java 程序,请使用以下命令:
java --source 22 --enable-preview Main.java

结果:
sum = 3

结论
OpenJDK 的 JEP 454 为 JNI 与本地代码的交互提供了一个前景广阔的替代方案。通过提供更简单、更安全、更高效的应用程序接口,它有可能彻底改变 Java 开发人员与本地库和内存的工作方式。