S-JIS[2024-09-23] 変更履歴
Javaの外部関数およびメモリーAPIのMemoryLayoutのメモ。
|
MemoryLayoutは、メモリーを構造体などの形式で扱えるようにするインターフェース。
import java.lang.foreign.MemoryLayout;
「構造体を返す関数」を呼び出す例。
struct MyStruct1 {
int value1; // 32bit
int value2; // 32bit
}
// MyStructを返す関数 MyStruct1 create_my_struct1(int, int);
まず、構造体をJavaで扱う為に、MemoryLayoutを使って構造を定義する。
import static java.lang.foreign.ValueLayout.JAVA_INT; import java.lang.foreign.MemoryLayout; import java.lang.foreign.MemoryLayout.PathElement; import java.lang.foreign.StructLayout; import java.lang.invoke.VarHandle;
/** MyStruct1 */
static final StructLayout MY_STRUCT1_LAYOUT = MemoryLayout.structLayout( //
JAVA_INT.withName("value1"), //
JAVA_INT.withName("value2") //
).withName("MyStruct1");
// 構造体のメンバーにアクセスする為のハンドル
static final VarHandle VALUE1_HANDLE = MY_STRUCT1_LAYOUT.varHandle(PathElement.groupElement("value1"));
static final VarHandle VALUE2_HANDLE = MY_STRUCT1_LAYOUT.varHandle(PathElement.groupElement("value2"));
varHandleメソッドに構造体のメンバーを指定してVarHandleを取得する。
もし構造体がネストしている場合は、varHandleメソッドは可変引数なので、ネストしている個数分のPathElementを渡すのだと思う。
では、MyStruct1を返す関数を呼び出してみる。
import java.lang.foreign.MemorySegment; import java.lang.invoke.MethodHandle;
try (var arena = Arena.ofConfined()) {
// 関数を取得
MemorySegment methodName = symbolLookup.findOrThrow("create_my_struct1");
MethodHandle createMyStruct1 = linker.downcallHandle(methodName, FunctionDescriptor.of(MY_STRUCT1_LAYOUT, JAVA_INT, JAVA_INT));
// System.out.println("create_my_struct1=" + createMyStruct1);
// 関数呼び出し
var myStruct1memory = (MemorySegment) createMyStruct1.invoke(arena, 123, 456);
// 構造体のメンバーの値を確認
// System.out.println("VALUE1_HANDLE=" + VALUE1_HANDLE);
// System.out.println("VALUE2_HANDLE=" + VALUE2_HANDLE);
int value1 = (int) VALUE1_HANDLE.get(myStruct1memory, 0);
int value2 = (int) VALUE2_HANDLE.get(myStruct1memory, 0);
System.out.println(value1);
System.out.println(value2);
}
FunctionDescriptor.of()で、呼び出す関数の返り値と引数の型を定義する。
第1引数が呼び出す関数の返り値の型なので、MyStruct1構造体の定義であるMY_STRUCT1_LAYOUTを渡している。
第2引数以降は呼び出す関数の引数。
MethodHandleのinvokeで実際に関数を呼び出す。
invokeの引数には、通常は、呼び出す関数の引数だけ渡せばいいのだが、
構造体が返る場合は、その構造体の分のメモリーを確保するオブジェクト(arena)を渡す必要があるようだ。
arenaが必要かどうかは、MethodHandleインスタンスをデバッグ出力して見ると分かる。
create_my_struct1=MethodHandle(SegmentAllocator,int,int)MemorySegment
MethodHandle()の丸括弧内がinvokeに渡す引数を表しているようだ。
SegmentAllocatorはメモリーを確保する為のインターフェースで、ArenaはSegmentAllocatorを継承しているので、invoke時にArenaを渡せばいい。
なお、MethodHandle()の丸括弧の後ろは、invokeしたときに返ってくる型を表していると思われる。
最後に、VarHandle(VALUE1_HANDLEやVALUE2_HANDLE)を使って構造体のメンバーから値を取得する。
getメソッドの第1引数には構造体の実体を渡す。
第2引数はよく分からないけどindexっぽい気がするので、0を指定しておく。
VarHandleもデバッグ出力すると、getメソッドに渡す型が分かる。
VALUE1_HANDLE=VarHandle[varType=int, coord=[interface java.lang.foreign.MemorySegment, long]] VALUE2_HANDLE=VarHandle[varType=int, coord=[interface java.lang.foreign.MemorySegment, long]]
varTypeがメンバー(データ)の型で、coordがget時に指定する引数の型だと思う。
「構造体のポインターを受け取って構造体のメンバーに値をセットする関数」を呼び出す例。
struct MyStruct1 {
int value1; // 32bit
int value2; // 32bit
}
void set_my_struct1(&MyStruct1, int, int);
構造体のJava側の定義は先の例と同じ。
import static java.lang.foreign.ValueLayout.JAVA_INT; import java.lang.foreign.Arena; import java.lang.foreign.FunctionDescriptor; import java.lang.foreign.MemorySegment; import java.lang.foreign.ValueLayout; import java.lang.invoke.MethodHandle; import java.lang.invoke.VarHandle;
try (var arena = Arena.ofConfined()) {
// 関数を取得
MemorySegment methodName = symbolLookup.findOrThrow("set_my_struct1");
MethodHandle setMyStruct1 = linker.downcallHandle(methodName, FunctionDescriptor.ofVoid(ValueLayout.ADDRESS, JAVA_INT, JAVA_INT));
// System.out.println("set_my_struct1=" + setMyStruct1);
// Java側で構造体のメモリーを確保
MemorySegment myStruct1memory = arena.allocate(MY_STRUCT1_LAYOUT);
// 関数呼び出し
setMyStruct1.invoke(myStruct1memory, 12, 34);
// 構造体のメンバーにセットされた値を確認
int value1 = (int) VALUE1_HANDLE.get(myStruct1memory, 0);
int value2 = (int) VALUE2_HANDLE.get(myStruct1memory, 0);
System.out.println(value1);
System.out.println(value2);
}
返り値が無い(返り値の型がvoid)の関数を定義する際は、FunctionDescriptor.ofVoid()を使う。
そして、ポインターを渡すときはValueLayout.ADDRESSを指定する。
invokeの方は、ポインターを渡す箇所で素直にMemorySegmentを渡せばいい。