Project Panama and jextract

原文はこちら。
The original article was written by Sundar Athijegannathan (Consulting Member of Technical Staff at Oracle).
https://inside.java/2020/10/06/jextract/

Project PanamaのゴールはJava仮想マシンと、明確に定義されているけれども’foreign’、つまり非Java APIとの接続を補強するというものです。

Project Panama: Interconnecting JVM and native code
https://openjdk.java.net/projects/panama/

ハイレベルで、ネイティブAPIにJavaからアクセスする際に対処すべきことが2つあります。一つは外部メモリ、もう一つは外部コードの呼び出しです。

これまで、Javaからネイティブメモリにアクセスするには、さまざまなアプローチがありました。例えば、ByteBuffer.allocateDirectを使用できます。このアプローチの問題点の一つは、allocateDirect経由で割り当てられたネイティブメモリは、ByteBufferがガベージコレクションされたときにのみ解放されるということです。もう一つの「解決策」として、文書化されておらず、サポートされていないUnsafeクラスに頼るという方法もありますが、これは不安定でお勧めできません。

ByteBuffer::allocateDirect
https://docs.oracle.com/javase/jp/15/docs/api/java.base/java/nio/ByteBuffer.html#allocateDirect(int)
https://docs.oracle.com/en/java/javase/15/docs/api/java.base/java/nio/ByteBuffer.html#allocateDirect(int)

(CやC++などの)ネイティブコードをJavaから呼び出すには、常にJava Native Interface (JNI) がデファクトのソリューションでしたが、JNIは面倒です。

Java Native Interface Specification Contents
https://docs.oracle.com/javase/jp/15/docs/specs/jni/index.html
https://docs.oracle.com/en/java/javase/15/docs/specs/jni/index.html

Panamaは、ネイティブコードをJavaから呼び出す効率的かつ安全でサポートされた方法を導入することでこれらの問題を解決します。パナマには基本的なAPIが2個あり、一つは現在 JDK15でインキュベート中のForeign-Memory Access APIと、Foreign Linker API (JEP 389)です。この記事では、Panamaの2つの側面について説明します:Foreign Linker APIとjextractツールです。

JEP 393: Foreign-Memory Access API (Third Incubator)
https://openjdk.java.net/jeps/393
JEP 389: Foreign Linker API (Incubator)
https://openjdk.java.net/jeps/389

JNI AKA the “old way”

まずJNIを使う現在の状況を見てみましょう。以下の例はgetpidというCの関数をJavaから呼び出す方法を説明したものです。

1. Write the Java class

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

  private static native int getpid();
}

2. Compile the class, and generate the corresponding header file

$ 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. Implement the C function

以下のように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. Compile the C code into a dynamic library so that JVM can load it

# 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. Load the dynamic library from within the Java program

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. Run the program

$ java Main.java 
my process id: 86733

この基本的な例は、JNIを使ってJavaからネイティブ関数を呼び出すのに必要なものを示しています。一言で言うと、以下のようにする必要があります。

  1. Javaクラスでネイティブメソッドを宣言する
  2. Javaクラスを-hフラグを付けてコンパイルし、Cのヘッダーファイルを生成する
  3. 生成されたCの宣言を実装する
  4. 動的ロードライブラリをコンパイルするCompile a dynamically loaded library
  5. この動的ライブラリをSystem.loadLibraryを使ってロードする

System::loadLibrary
https://docs.oracle.com/javase/jp/15/docs/api/java.base/java/lang/System.html#loadLibrary(java.lang.String)
https://docs.oracle.com/en/java/javase/15/docs/api/java.base/java/lang/System.html#loadLibrary(java.lang.String)

というわけで、中間的なネイティブコードのラッパーを実装してオリジナルのネイティブ関数を呼び出さなければなりませんでした。言い換えれば、既存のネイティブライブラリを呼び出せるように、ネイティブコードを書いてコンパイルしなければならなかったのです。これは、贔屓目に見ても面倒です。

Panama Foreign Linker API

この例はPanamaのForeign Linker 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
  • Javaからネイティブメソッドハンドルを許可するためには、-Dforeign.restricted=permit が必要です。
  • Panama APIが現時点でincubatorプロジェクトなので、--add-modulesが必要です。
  • この例はJEP 330を使ってコンパイルし、1個のステップでこのクラスを実行しています。

JEP 330: Launch Single-File Source-Code Programs
https://openjdk.java.net/jeps/330

ご覧の通り、PanamaのForeign Linker APIはとても簡単です。JNIのようにネイティブの中間ラッパーを書く必要がありません。この例を試すには、最新のPanamaの早期アクセスビルドをインストールしてください。

Project Panama Early-Access Builds
https://jdk.java.net/panama/

jextract

先ほどの例では、ネイティブコードのラッパーを書かずにgetpidをJavaからの呼び出すことができました。しかし、メソッドハンドルやFunctionDescriptor、メソッドハンドル型、Cシンボル名などを取り扱う必要がありました。簡単なCのAPIを呼び出すだけなのに。。。

MethodHandle
https://docs.oracle.com/javase/jp/15/docs/api/java.base/java/lang/invoke/MethodHandle.html
https://docs.oracle.com/en/java/javase/15/docs/api/java.base/java/lang/invoke/MethodHandle.html

ここで jextract の出番です。これは、CのヘッダーファイルからJavaクラスを生成するPanamaのツールです。jextractで生成されたクラスは、ネイティブシンボルのルックアップを取り扱ったり、C宣言のメソッドハンドル作成から関数記述子を計算したり、そして、基礎となるC関数を呼び出すためのよりシンプルなJavaの静的メソッドを提示したりします。要するに、jextractはPanama Foreign Linker 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 を使ってターゲットのパッケージを指定します。

では新たなMainクラス (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

とテクノロジーと組み合わせたより興味深い例は以下にあります。

Panama jextract samples
https://github.com/sundararajana/panama-jextract-samples

Conclusion

この記事では旧来のJNIを使う方法、そして新たなPanama Foreign Linker APIを使う方法の2方法を使ってJavaからネイティブのgetpid関数を呼び出しました。Foreign Linker APIはシンプルかつ簡単で、中間のネイティブコードラッパーを取り扱う必要がないことがわかりました。それ以上に、jextractはPanamaのツールで、Cのヘッダーファイルを解析して、基礎となるC関数を呼び出すためのよりシンプルなJava静的メソッドを提示するJavaクラスを生成してくれるので、さらに簡単になります。

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト /  変更 )

Google フォト

Google アカウントを使ってコメントしています。 ログアウト /  変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト /  変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト /  変更 )

%s と連携中