jnr-ffiでJavaからCのポインタを使う
jnr-ffiは、Javaから簡単にCのライブラリを呼び出せるフレームワークだ。
JNIのような煩雑なコードを書かなくてよいのがメリット。
前に記事を書いたときは、プリミティブ型とかStringしか試さなかったので、今回はそれ以外のポインタを試してみる。
Pointerクラスを使ってみる
まず、C側のコードを書く。環境は64bitのWindows。
extern "C" __declspec(dllexport) void add(int* arguments, int* result) { *result = arguments[0] + arguments[1]; }
NativeLib.dllという名前でビルドした。
そして、Java側のコードを書く。
public interface Native1 { void add(Pointer arguments, Pointer result); }
import java.nio.ByteBuffer; import jnr.ffi.LibraryLoader; import jnr.ffi.Pointer; import jnr.ffi.Runtime; public class PointerTest1 { public static void main(String[] args) { Native1 native1 = LibraryLoader.create(Native1.class).load("NativeLib"); Pointer in = Pointer.wrap(Runtime.getSystemRuntime(), ByteBuffer.allocate(8)); in.putInt(0, 60000); in.putInt(4, 8000); Pointer out = Pointer.wrap(Runtime.getSystemRuntime(), ByteBuffer.allocate(4)); native1.add(in, out); System.out.println(out.getInt(0)); } }
jnr-ffi、あまりドキュメントが無いようなのでよく分からないが…、それらしいクラスのそれらしいメソッドを使って書いてみる。
実行の際は、-Djava.library.path=DLLのパス名 を指定する必要がある。
結果は、
2465
う~ん、違うな。
調べたところ、エンディアンの違いのようだ。
ByteBufferはデフォルトでビッグエンディアン。
60000は0x0000EA60、8000は0x00001F40なので、ビッグエンディアンで足すと0x000109A0=68000だが、リトルエンディアンで足すと0x000009A1=2465になってしまう。
... Pointer in = Pointer.wrap(Runtime.getSystemRuntime(), ByteBuffer.allocate(8).order(Runtime.getSystemRuntime().byteOrder())); ... Pointer out = Pointer.wrap(Runtime.getSystemRuntime(), ByteBuffer.allocate(4).order(Runtime.getSystemRuntime().byteOrder())); ...
のように正しいをエンディアンを指定したら、
68000
と出力された!
ArrayMemoryIOクラスを使ってみる
Pointer#wrapメソッドは、文字通り既存のByteBufferインスタンスをラップするだけで、エンディアンの指定は行ってくれないようだ。
Pointerクラスのサブクラスを調べてみると、ArrayMemoryIOというクラスがあった。
中を見てみると、こちらはエンディアンの指定も行ってくれるっぽいし、簡単そうだ。
上のコードを
... Pointer in = new ArrayMemoryIO(Runtime.getSystemRuntime(), 8); ... Pointer out = new ArrayMemoryIO(Runtime.getSystemRuntime(), 4); ...
のように変えて実行したところ、正しく68000と出力された。
Java側とC側でメモリを共有するには?
C側は
static int* _pointer; extern "C" __declspec(dllexport) void setAddress(int* pointer) { _pointer = pointer; } extern "C" __declspec(dllexport) int getValue() { return *_pointer; }
のように書き、Java側は
public interface Native2 { void setAddress(Pointer pointer); int getValue(); }
public class PointerTest2 { public static void main(String[] args) { Native2 native2 = LibraryLoader.create(Native2.class).load("NativeLib"); Pointer in = Pointer.wrap(Runtime.getSystemRuntime(), ByteBuffer.allocate(4).order(Runtime.getSystemRuntime().byteOrder())); native2.setAddress(in); in.putInt(0, 1234567); System.out.println(native2.getValue()); } }
と書く。
つまり、
①Java側のPointerをC側に渡して保存する
②Java側でPointerの指すメモリを書き換える
③C側でPointerの指すメモリを参照する
としたとき、どうなるのか?
結果は
0
で、共有されていないようだ。。
つまり、メソッドを呼び出すたびに、メモリの内容がコピーされているのだろう。
ByteBufferを調べると、allocateDirectというメソッドがある。
これを使うとよさそうだ。
allocateをallocateDirectに変えて実行してみると、
1234567
と出力された!
つまり、Java側とC側とでメモリを共有できた!
OS | Windows 7 Enterprise (64bit) |
Java | 8 |
jnr-ffi | 2.0.3 |