今日もプログラミング

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

jnr-ffiでJavaからCを呼び出す (Windows)

jnr-ffiとは?

JavaからCを呼び出す、と言えばJNIだが、C側にJNI固有の処理を書いたりするので結構めんどい。

jnr-ffiというフレームワークを使うと、C側の関数をそのままJavaマッピングして呼べるらしい。

具体的には、C側に

int length(char *s)

のような関数があると、Java側から

int length(String s)

というほぼそのままのメソッドで呼び出せてしまう!

という訳で、今回はこのjnr-ffiを実際に試してみる。

 

jnr-ffiをダウンロードする

jnr-ffiを使うには、jffiやasmも必要だ。

mavenなどを使わず手動でダウンロードする場合は、以下からダウンロードできる。

http://search.maven.org/#artifactdetails|com.github.jnr|jnr-ffi|2.0.3|jar

http://search.maven.org/#artifactdetails|com.github.jnr|jffi|1.2.9|jar

http://forge.ow2.org/project/showfiles.php?group_id=23&release_id=5660

jnr-ffi-2.0.3.jar
jffi-1.2.9-native.jar
jffi-1.2.9.jar
asm-5.0.3.jar
asm-analysis-5.0.3.jar
asm-commons-5.0.3.jar
asm-tree-5.0.3.jar
asm-util-5.0.3.jar
asm-xml-5.0.3.jar

 

C側を実装する

Visual Studioで、クラスライブラリ(Visual C++)のプロジェクトを作成する。

自分の環境は64bitなので、ビルド構成も64bitにする。

プリコンパイル済みヘッダなども取っ払ってしまって、cppファイル1つだけにした。

#include <stdio.h>
#include <string.h>

extern "C" __declspec(dllexport) int int_test(int n) {
	return n * 2;
}

extern "C" __declspec(dllexport) int string_in_test(char* s) {
	printf("#string_in_test s = ");
	for (int i = 0; s[i] != '\0'; i++) {
		printf("%x:", (s[i] & 0xFF));
	}
	printf("\n");
	return strlen(s);
}

extern "C" __declspec(dllexport) void string_out_test(char* s, int n) {
	strncpy(s, "abcdefg", n);
}

extern "C" __declspec(dllexport) void byte_array_test(unsigned char* p, int n) {
	for (int i = 0; i < n; i++) {
		p[i] = p[i] + 1;
	}
}

なお、「extern "C"」を付けておかないと、関数名が修飾されてしまい(C++オーバーロードの解決用らしい)、Java側とうまくリンクできない。

 

Java側を実装する

    public class JnrFfiTest {

	public static interface TestC {
		int int_test(int n);
		int string_in_test(String s);
		void string_out_test(StringBuilder s, int n);
		void byte_array_test(byte[] p, int n);
	}

	public static void main(String[] args) {
		TestC testC = LibraryLoader.create(TestC.class).load("jnr_ffi_test");

		System.out.println("int_test(5) = " + testC.int_test(5));

		System.out.println("string_in_test(\"abcあいう\") = " + testC.string_in_test("abcあいう"));

		StringBuilder builder = new StringBuilder(4);
		testC.string_out_test(builder, 4);
		System.out.println("string_out_test(s, 4) = " + builder.toString());

		byte[] bytes = "ABC".getBytes();
		testC.byte_array_test(bytes, bytes.length);
		System.out.println("byte_array_test(p, 3) = " + new String(bytes));
	}

}

まず、C側の関数に合わせたインターフェイスを用意する(TestC)。

そして、LibraryLoader.createによりインスタンスを取得する。

loadの引数の"jnr_ffi_test"はDLL名(から拡張子を除いたもの)。

DLLは、環境変数PATHおよびJava VM引数 -Djava.library.path で指定したパスから検索される。

 

実行結果はこんな感じ。

int_test(5) = 10
#string_in_test s = 61:62:63:82:a0:82:a2:82:a4:
string_in_test("abcあいう") = 9
string_out_test(s, 4) = abcd
byte_array_test(p, 3) = BCD

 

文字のエンコーディングは?

上の実行結果を見ると、文字列はシフトJISのバイト列に変換されてC側に渡されている。

Windowsのデフォルトのエンコーディングだ。

試しに -Dfile.encoding=UTF8 を指定したところ、以下のようにUTF8になった。

#string_in_test s = 61:62:63:e3:81:82:e3:81:84:e3:81:86:
string_in_test(...) = 12

つまり、Javaのデフォルトエンコーディングが使われるようだ。

更に調べたところ、jnr.ffi.annotations.Encodingアノテーションを付けることにより、パラメータごとにエンコーディングを指定することもできるようだ。

 

StringBuilderに対応するバッファのサイズは?

Java側からStringBuilderを引数で渡すと、バッファが確保され、そのポインタがC側に渡される。

このときのバッファのサイズは何バイトなんだろうか?

ソースを調べてみたら、jnr.ffi.provider.converters.StringBuilderParameterConverterというそれらしいクラスがあった。

中を見ると、以下のような処理がある。

ByteBuffer byteBuffer = ByteBuffer.wrap(new byte[parameter.capacity() * (int) Math.ceil(encoder.maxBytesPerChar()) + 4]);

なるほど。

StringBuilderのcapacity × 1文字の最大バイト数 + 4 だから、長さcapacityの文字列を設定するには十分、ということだ。

 

まとめ

JNIからCの関数を呼ぶには、JNIに合わせた専用の関数を作る必要がある。

jnr-ffiを使えば、Cの関数をそのまま呼べるので、楽だし構成もきれいになる。

こいつはなかなか良さそうだ。

 

OS Windows 7 Enterprise (64bit)
Java 8
jnr-ffi 2.0.3
jffi 1.2.9
asm 5.0.3