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側に渡されている。
試しに -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 |