S-JIS[2024-09-24/2024-09-25] 変更履歴

Java jextract

Java外部関数およびメモリーAPI用のクラスを生成するjextractツールのメモ。

  • Rustのcbindgen(RustでC言語のヘッダーファイルを生成するツール)

概要

jextractは、C言語のヘッダーファイルを元に、外部関数およびメモリーAPIで構造体や関数等を扱うためのクラス(MemoryLayoutMethodHandle等および構造体のメンバーへのsetter/getterメソッドを定義したクラス)を生成してくれるツール。


jextractのインストール

jextractは以下の手順でインストールする。

  1. ダウンロードサイトを開く。
  2. 使用したい環境のtar.gzをダウンロードする。(Windows用でもtar.gz形式で提供されている^^;)
  3. ダウンロードしたtar.gzファイルを適当な場所に解凍する。
    $ tar xf openjdk-22-jextract+5-33_windows-x64_bin.tar.gz

これにより「jextract-22/bin」というディレクトリーが作られ、その下に実行ファイル(Linux用シェルのjextractやWindows用のjextract.bat)が置かれている。


jextractの実行

jextractコマンドを実行して、C言語のヘッダーファイルからJavaのソースコードを生成する。

$ cd 〜/src/generated/java/
$ 〜/jextract-22/bin/jextract \
    -t com.example.jextract.ffi \
    --output 〜/src/generated/java/ \
    〜/ffi-example-rust/ffi-example.h

-t--target-package)で、生成されるクラスのパッケージ名を指定する。
これを指定しないと、出力先ディレクトリー直下に複数のjavaファイルが置かれてしまうので注意。

--outputは出力先のディレクトリー。これが指定されていないと、カレントディレクトリーに生成される。
パッケージ名が指定されている場合は、それを元にしたサブディレクトリーが作られる。

最後に、読み込むヘッダーファイルのパスを指定する。

jextractを実行したときに発生したエラー


かなり色々なソースファイルが生成されるが、重要なのは2種類。


生成されたソースファイルの例

読み込んだヘッダーファイル(ffi_example.h)


src/generated/java/com/example/jextract/ffi/ffi_example_h.java

    /**
     * {@snippet lang=c :
     * int32_t add(int32_t left, int32_t right)
     * }
     */
    public static int add(int left, int right) {
        〜
    }

src/generated/java/com/example/jextract/ffi/MyStruct1.java

    private static final GroupLayout $LAYOUT = MemoryLayout.structLayout(
        ffi_example_h.C_INT.withName("value1"),
        ffi_example_h.C_INT.withName("value2")
    ).withName("MyStruct1");

    /**
     * The layout of this struct
     */
    public static final GroupLayout layout() {
        return $LAYOUT;
    }
    /**
     * Allocate a segment of size {@code layout().byteSize()} using {@code allocator}
     */
    public static MemorySegment allocate(SegmentAllocator allocator) {
        return allocator.allocate(layout());
    }
    /**
     * Getter for field:
     * {@snippet lang=c :
     * int32_t value1
     * }
     */
    public static int value1(MemorySegment struct) {
        return struct.get(value1$LAYOUT, value1$OFFSET);
    }

    /**
     * Setter for field:
     * {@snippet lang=c :
     * int32_t value1
     * }
     */
    public static void value1(MemorySegment struct, int fieldValue) {
        struct.set(value1$LAYOUT, value1$OFFSET, fieldValue);
    }
生成されたMyStruct1.javaの主なメソッド
メソッド 説明
GroupLayout layout() MyStruct1構造体のレイアウト(MemoryLayout)を取得する。
MemorySegment allocate(SegmentAllocator allocator) MyStruct1構造体のメモリーを確保する。
引数にはArenaを渡す。
int value1(MemorySegment struct) MyStruct1構造体のメンバーvalue1の値を取得する。
void value1(MemorySegment struct, int fieldValue) MyStruct1構造体のメンバーvalue1に値を設定する。

jextractで生成したクラスを使う例

jextractで生成されたクラスでは、SymbolLookup.loaderLookup()を使ってライブラリーを読み込む。
したがって、System.loadLibrary()やSystem.load()によってライブラリーを指定する必要がある。

    static {
//      System.loadLibrary("ffi_example_rust");
        System.load("C:/example/ffi/ffi-example-rust/target/release/ffi_example_rust.dll");
    }

add関数を呼び出す例。

import com.example.jextract.ffi.ffi_example_h;
    int r = ffi_example_h.add(100, 2);

MyStruct1構造体を扱う例。

import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;

import com.example.jextract.ffi.MyStruct1;
    try (var arena = Arena.ofConfined()) {
        MemorySegment myStruct1Memory = MyStruct1.allocate(arena);	// MyStruct1構造体のメモリーを確保
        MyStruct1.value1(myStruct1Memory, 123);	// value1のsetter
        MyStruct1.value2(myStruct1Memory, 456);	// value2のsetter

        int value1 = MyStruct1.value1(myStruct1Memory);	// value1のgetter
        int value2 = MyStruct1.value2(myStruct1Memory);	// value2のgetter
        System.out.println(value1);
        System.out.println(value2);
    }

定数の例

C言語の#defineで定義された定数の例。[2024-09-25]

ffi_example.h:

〜
#define MY_CONST1 123
〜

↓生成

src/generated/java/com/example/jextract/ffi/ffi_example_h.java:

    private static final int MY_CONST1 = (int)123L;
    /**
     * {@snippet lang=c :
     * #define MY_CONST1 123
     * }
     */
    public static int MY_CONST1() {
        return MY_CONST1;
    }

グローバルな関数と同様に、「ヘッダーファイル名.java」内にstaticメソッドとして定義されている。


列挙型の例

C言語のenumの例。[2024-09-25]

ffi_example.h:

typedef enum MyEnum1 {
  MY_ENUM1_DEFAULT,
  MY_ENUM1_INTERRUPT,
  MY_ENUM1_WAIT,
  MY_ENUM1_INTERRUPT_EXCLUDE,
  MY_ENUM1_WAIT_EXCLUDE,
} MyEnum1;

Rustのcbindgenで列挙型を出力する例

↓生成

src/generated/java/com/example/jextract/ffi/ffi_example_h.java:

    private static final int MY_ENUM1_DEFAULT = (int)0L;
    /**
     * {@snippet lang=c :
     * enum MyEnum1.MY_ENUM1_DEFAULT = 0
     * }
     */
    public static int MY_ENUM1_DEFAULT() {
        return MY_ENUM1_DEFAULT;
    }

    private static final int MY_ENUM1_INTERRUPT = (int)1L;
    /**
     * {@snippet lang=c :
     * enum MyEnum1.MY_ENUM1_INTERRUPT = 1
     * }
     */
    public static int MY_ENUM1_INTERRUPT() {
        return MY_ENUM1_INTERRUPT;
    }
〜

「構造体と同様にMyEnum1.javaが作られるかなー」と思いきや、グローバル関数と同じjavaファイルに出力されている。

C言語では、enumによって定義された列挙子は、グローバル変数(定数)と同様に使用できる。(「列挙型名.列挙子」のように書く必要が無い)
jextractでもそれを踏襲したということらしい。


関数に文字列を渡す例

関数の引数として文字列(UTF-8で'\0'終端)を渡す例。[2024-09-25]

ffi_example.h:

void my_println(const char *s);

↓生成

src/generated/java/com/example/jextract/ffi/ffi_example_h.java:

    /**
     * {@snippet lang=c :
     * void my_println(const char *s)
     * }
     */
    public static void my_println(MemorySegment s) {
        〜
    }

    // nullを渡す例
    ffi_example_h.my_println(MemorySegment.NULL);

    try (var arena = Arena.ofConfined()) {
        // 文字列を渡す例
        MemorySegment s = arena.allocateFrom("文字列 from Java");
        ffi_example_h.my_println(s);
    }

関数から文字列が返される例

関数の戻り値として文字列(UTF-8で'\0'終端)を受け取る例。[2024-09-25]

ffi_example.h:

const char *my_to_string(int32_t n);

void free_string(char *s);

↓生成

src/generated/java/com/example/jextract/ffi/ffi_example_h.java:

    /**
     * {@snippet lang=c :
     * const char *my_to_string(int32_t n)
     * }
     */
    public static MemorySegment my_to_string(int n) {
        〜
    }

    /**
     * {@snippet lang=c :
     * void free_string(char *s)
     * }
     */
    public static void free_string(MemorySegment s) {
        〜
    }

    MemorySegment rs = ffi_example_h.my_to_string(123);	// 整数を文字列に変換する関数
    try {
        String s = rs.getString(0);
        System.out.println("java ffm: " + s);
    } finally {
        ffi_example_h.free_string(rs);	// 渡された文字列のメモリーを解放する
    }

関数が文字列を返すということは、その関数の中で文字列の分のメモリーを確保しているということ。
なので、そのメモリーは、その関数が属しているライブラリーの中で解放してもらう必要がある。
上記の例では、そのためにfree_string()という関数が提供されている。


jextract実行時のエラー

Windows上でjextractコマンドを実行したときに、以下のエラーが出た。

$ 〜/jextract-22/bin/jextract.bat -t com.example.fextract.ffi ffi-example-rust/ffi-example.h
ERROR: C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Tools\MSVC\14.41.34120\include\yvals_core.h:23:2: error: error STL1003: Unexpected compiler, expected C++ compiler.

このときは、ヘッダーファイルの中身がC++用だった。

ここで使われたVisual Studio Build ToolsのコンパイラーはC言語用らしく、C++はコンパイルできないようだ。

という訳で、ヘッダーファイルをC言語用にしたら通った。
(このときはRustのcbindgenを使ってヘッダーファイルを生成していた。デフォルトオプション はC++用だったので、C言語用に生成するオプションを加えて再生成した)


外部関数およびメモリーAPIへ戻る / Java目次へ戻る / 新機能へ戻る / 技術メモへ戻る
メールの送信先:ひしだま