今日もプログラミング

IT技術とかプログラミングのこととか特にJavaを中心に書いていきます

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はデフォルトでビッグエンディアン

x64はリトルエンディアン。

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