Project Panama 和 jextract

Project Panama 的目标是丰富 Java 虚拟机与明确定义但“外部”(即非 Java API)之间的连接。

从 Java 访问本机 API 时,从高层面来看,需要处理两件事,即访问外部内存和调用外部代码。

到目前为止,有不同的方法可以从 Java 访问本机内存。例如,可以使用 ByteBuffer.allocateDirect。此方法存在的一个问题是,仅在垃圾回收 ByteBuffer 时才会释放通过 allocateDirect 分配的本机内存!另一个“解决方案”是依赖于未记录且不受支持的 Unsafe 类,但这很脆弱,不建议使用!

要从 Java 调用本机代码(例如 C、C++ 等),Java Native Interface (JNI) 一直是事实上的解决方案,但 JNI 非常繁琐。

Panama 通过引入一种受支持、高效且安全的方式从 Java 调用本机代码来解决这些问题。Panama 有 2 个基本 API,Foreign-Memory Access API(当前在 JDK 15 中孵化)和 Foreign Linker API(候选 JEP)。这篇文章讨论了 Panama 的两个方面:Foreign Linker API 以及 jextract 工具。

~

JNI 又称“旧方法”

让我们首先了解 JNI 的当前情况。以下示例说明了如何从 Java 调用 getpid C 函数。

1. 编写 Java 类

class Main {
  public static void main(String[] args) {
    System.out.println("my process id: " + getpid());
  }

  private static native int getpid();
}

2. 编译类,并生成相应的头文件

$ javac -h . Main.java

-h 标志指示 javac 与编译后的类一起生成 C 头文件。生成的这个头文件 (Main.h) 如下所示。

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class Main */

#ifndef _Included_Main
#define _Included_Main
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     Main
 * Method:    getpid
 * Signature: ()I
 */
JNIEXPORT jint JNICALL Java_Main_getpid
  (JNIEnv *, jclass);

#ifdef __cplusplus
}
#endif
#endif

3. 实现 C 函数

Main.c

#include <unistd.h>
#include "Main.h"

JNIEXPORT jint JNICALL Java_Main_getpid
  (JNIEnv *env, jclass cls) {
  // call the actual C function to get the process id!
  return getpid();
}

4. 将 C 代码编译成动态库,以便 JVM 可以加载它

# Note: JAVA_HOME is the directory where your JDK is installed
# Following is the macOS command to compile it into a dynamic library
# This step is OS and compiler dependent!

$ cc -shared -o libmain.dylib -I $JAVA_HOME/include -I $JAVA_HOME/include/darwin Main.c

5. 从 Java 程序中加载动态库

class Main {
  public static void main(String[] args) {
    System.loadLibrary("main"); // <--- load dynamic library
    System.out.println("my process id: " + getpid());
  }

  private static native int getpid();
}

6. 运行程序

$ java Main.java 
my process id: 86733

这个基本示例展示了使用 JNI 从 Java 调用本机函数所必需的内容。简而言之,我们必须

  1. 在 Java 类中声明本机方法
  2. 使用 -h 标志编译 Java 类以生成 C 头
  3. 实现生成的 C 声明
  4. 编译动态加载的库
  5. 使用 System.loadLibrary 加载此动态库

因此,我们必须实现一个中间本机代码包装器来调用原始本机函数!换句话说,我们必须编写并编译本机代码才能调用现有的本机库!这充其量是繁琐的!

~

Panama 外部链接器 API

此示例使用 Panama 的外部链接器 API 来调用相同的本机函数。

import java.lang.invoke.*;
import jdk.incubator.foreign.*;

class PanamaMain {
  public static void main(String[] args) throws Throwable {
    // get System linker
    var linker = CLinker.getInstance();
    var lookup = LibraryLookup.ofDefault();

    // get a native method handle for 'getpid' function
    var getpid = linker.downcallHandle(
           lookup.lookup("getpid").get(),
           MethodType.methodType(int.class),
           FunctionDescriptor.of(CLinker.C_INT));

    // invoke it!
    System.out.println((int)getpid.invokeExact());
  }
}

使用以下命令编译并运行此 Java 程序。

$ java -Dforeign.restricted=permit --add-modules jdk.incubator.foreign  PanamaMain.java
WARNING: Using incubator modules: jdk.incubator.foreign
WARNING: using incubating module(s): jdk.incubator.foreign
1 warning
87543

💡 -Dforeign.restricted=permit 是必需的,以便允许 Java 中的本机方法句柄

💡 --add-modules 是必需的,因为 Panama API 仍在孵化中

💡 此示例使用 JEP 330 在一个步骤中编译并运行类

正如我们所看到的,Panama 的外部链接器 API 非常简单,因为它不需要像 JNI 那样编写(和编译!)中间本机包装器!要尝试此示例,只需安装最新的 Panama 早期访问版本

~

jextract

在前面的示例中,我们设法从 Java 调用 getpid,而无需编写任何本机代码包装器。但是,我们不得不处理 方法句柄、函数描述符、方法句柄类型、C 符号名称,… 只需调用一个简单的 C API。

这就是 jextract 的用武之地!它是一个 Panama 工具,可从 C 头文件中生成 Java 类。这些生成的类处理本机符号查找,从 C 声明方法句柄创建中计算函数描述符,并提供更简单的 Java 静态方法来调用底层 C 函数。简而言之,jextract 隐藏了 Panama 外部链接器 API 的一些底层细节。

让我们采用相同的 getpid 示例,但使用 jextract

// simple header file that contains C declaration
// you can extract arbitrary C header file btw.
int getpid();

以下命令为上述 C 头文件提取一个 Java 接口。

$ jextract -t com.unix getpid.h
WARNING: Using incubator modules: jdk.incubator.foreign, jdk.incubator.jextract

💡 -t com.unix 用于指定目标包。

现在,让我们从一个新的主类(Main2.java)中使用 com.unix.*

import static com.unix.getpid_h.*;

class Main2 {
   public static void main(String[] args) {
      System.out.println(getpid());
  }
}

没有方法句柄查找,没有 invokeExact 等。它再简单不过了!

以下命令将运行示例。

$ java -Dforeign.restricted=permit --add-modules jdk.incubator.foreign  Main2.java
WARNING: Using incubator modules: jdk.incubator.foreign
warning: using incubating module(s): jdk.incubator.foreign
1 warning
87716

getpid 是一个基本示例,可以在 此处 找到将 jextract 与以下技术相结合的更有趣的示例。

  • Python
  • SQLite
  • OpenGL
  • TensorFlow
  • LAPACK
  • BLAS
  • libgit2
~

结论

此文章使用两种方法从 Java 调用本机 getpid 函数,使用旧的 JNI 方法和新的 Panama 外部链接器 API。我们可以看到,外部链接器 API 非常简单,因为它不需要处理中间本机代码包装器。此外,jextract 是一个 Panama 工具,它通过解析 C 头文件来生成一个 Java 类,该类提供了一个更简单的 Java 静态方法来调用底层 C 函数,从而进一步简化了事情。