S-JIS[2024-09-23] 変更履歴

Java外部関数およびメモリーAPI MemoryLayout

Java外部関数およびメモリーAPIのMemoryLayoutのメモ。


概要

MemoryLayoutは、メモリーを構造体などの形式で扱えるようにするインターフェース。

import java.lang.foreign.MemoryLayout;

「構造体を返す関数」を呼び出す例

「構造体を返す関数」を呼び出す例。

struct MyStruct1 {
    int value1;	// 32bit
    int value2;	// 32bit
}
// MyStructを返す関数
MyStruct1 create_my_struct1(int, int);

Rustで構造体と関数を定義する例


まず、構造体を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_HANDLEVALUE2_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側の定義は先の例と同じ。

Rustで構造体と関数を定義する例


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を渡せばいい。


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