jnr-ffiでJavaからCを呼び出すときに構造体を扱いたい
構造体ポインタを引数にとるCの関数をJavaから呼び出したい
以前、jnr-ffiを使ってJavaからCのライブラリを呼び出してみた。
jnr-ffiでJavaからCを呼び出す (Windows)
このときは、intとかStringなどの基本的な型しか使っていなかったが、構造体の場合はどうするのだろう?
という訳で、いろいろ調べてみた。
シンプルな構造体
C側はこんな感じ。
struct S1 { int n1; int n2; }; extern "C" __declspec(dllexport) int test1(struct S1* s1) { s1->n2 = s1->n1 + 1; return 0; }
jnr-ffiには、Structというクラスがあり、これを継承すれば構造体を表せるようだ。
また、構造体のメンバに対応するクラスUnsigned8、Signed32、UTF8StringなどがStructクラスの内部クラスとして定義されている。
以下のようにして、呼び出すことができた。
public class S1 extends Struct { public Signed32 n1 = new Signed32(); public Signed32 n2 = new Signed32(); public S1(Runtime runtime) { super(runtime); } } public interface TestLib { int test1(S1 s1); } public class Test { public static void main(String[] args) { TestLib testLib = LibraryLoader.create(TestLib.class).load("test-lib"); S1 s1 = new S1(Runtime.getSystemRuntime()); s1.n1.set(99); testLib.test1(s1); System.out.println(s1.n2.get()); } }
構造体のメンバにアクセスするには、getとかsetを使わなくてはいけないのでちょっとめんどい。
配列をメンバに持つ構造体
このように配列をメンバに持つ構造体の場合はどうなるか?
struct S2 { int n1[4]; int n2[4]; };
結論から言えば、Java側もメンバを配列で持てばいいだけ。
Structクラスは配列メンバを初期化するためのメソッドを用意しているので、それを使うと便利だ。
public class S2 extends Struct { public Signed32[] n1 = array(new Signed32[4]); public Signed32[] n2 = array(new Signed32[4]); public S2(Runtime runtime) { super(runtime); } }
単に new Signed32[4] するだけだと配列の要素はnullのままだが、arrayを呼び出すと、各要素を new Signed32() で初期化してくれる。
構造体をメンバに持つ構造体
このように構造体が入れ子になっている場合はどうなるか?
struct S3 { int n; struct S1 s1; int x; };
単に、S3クラスにS1メンバ型の変数を持たせてもうまくいかない。
Structクラスは内部で各メンバの情報を管理しており、そこに登録してやる必要があるからだ。
Signed32クラスなどは、newされると自動的に登録されるようになっている。
調べてみたところ、Structクラスにinnerというメソッドがあり、それによりた構造体をメンバとして登録できることが分かった。
public class S3 extends Struct { public Signed32 n; public S1 s1; public Signed32 x; public S3(Runtime runtime) { super(runtime); n = new Signed32(); s1 = inner(new S1(runtime)); x = new Signed32(); } }
new Signed32() とか inner を呼び出した順にメンバが登録されるので注意。
構造体ポインタをメンバに持つ構造体
構造体のポインタがメンバの場合はどうだろう?
struct S3 { int n; struct S1* s1; };
StructクラスにはPointerという内部クラスがあり(jnr.ffi.Pointerとは別のクラスなので注意!)、これを使えばよいようだ。
結論から言うと、以下のようにしてうまくいった。
public class S4 extends Struct { public Signed32 n = new Signed32(); private Pointer s1p = new Pointer(); public S1 s1; public S4(Runtime runtime) { super(runtime); s1 = new S1(runtime); s1p.set(getMemory(s1, ParameterFlags.DIRECT)); } }
S4クラスには、Pointer s1p と S1 s1 の2つを持たせている。
実際にS4構造体のメンバとなるのはs1pの方。
s1の方はS4構造体のメンバで、別のメモリ上に存在するので、innerでは登録しない。
s1pはポインタなので、s1のアドレスを設定する必要がある。
getMemoryメソッドを呼び出す際、ParameterFlags.DIRECTを指定しないと、実際に物理的メモリが割り当てられず、適切なアドレスが返らないようだ。