S-JIS[2024-09-24/2024-09-25] 変更履歴
Javaの外部関数およびメモリーAPI用のクラスを生成するjextractツールのメモ。
|
|
|
jextractは、C言語のヘッダーファイルを元に、外部関数およびメモリーAPIで構造体や関数等を扱うためのクラス(MemoryLayoutやMethodHandle等および構造体のメンバーへのsetter/getterメソッドを定義したクラス)を生成してくれるツール。
jextractは以下の手順でインストールする。
$ tar xf openjdk-22-jextract+5-33_windows-x64_bin.tar.gz
これにより「jextract-22/bin」というディレクトリーが作られ、その下に実行ファイル(Linux用シェルのjextract
やWindows用のjextract.bat
)が置かれている。
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は出力先のディレクトリー。これが指定されていないと、カレントディレクトリーに生成される。
パッケージ名が指定されている場合は、それを元にしたサブディレクトリーが作られる。
最後に、読み込むヘッダーファイルのパスを指定する。
かなり色々なソースファイルが生成されるが、重要なのは2種類。
ffi_example_h.java
」
標準的な命名ルールを全く無視^^;)MyStruct1.java
」)/** * {@snippet lang=c : * int32_t add(int32_t left, int32_t right) * } */ public static int add(int left, int right) { 〜 }
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); }
メソッド | 説明 |
---|---|
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で生成されたクラスでは、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]
〜 #define MY_CONST1 123 〜
↓生成
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]
typedef enum MyEnum1 { MY_ENUM1_DEFAULT, MY_ENUM1_INTERRUPT, MY_ENUM1_WAIT, MY_ENUM1_INTERRUPT_EXCLUDE, MY_ENUM1_WAIT_EXCLUDE, } MyEnum1;
↓生成
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]
void my_println(const char *s);
↓生成
/** * {@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]
const char *my_to_string(int32_t n); void free_string(char *s);
↓生成
/** * {@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()という関数が提供されている。
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言語用に生成するオプションを加えて再生成した)