用語説明
JNR (Java Native Runtime)
JavaからNativeなコードを呼び出す仕組み
Project Panamaのメインコンテンツ
JEP 191: Foreign Function Interfaceが提案されている
FFI (Foreign Function Interface)
InterfaceまではJavaで定義して~~(しない)~~、実装をCやC++でできる優れもの
LibraryLoaderというやつを使ってダイナミックリンクライブラリ(.dllや.so)からオブジェクトを生成して呼び出す
FFIを使ってみる
プロジェクト作成
JDKのバージョンを12に指定(意味があるかは不明)
ライブラリを追加
build.gradleのdependancesにライブラリを追加します。
version '1.0-SNAPSHOT'
sourceCompatibility = 1.8
repositories {
mavenCentral()
}
//追加部分
dependencies {
//FFIの本体
compile group: 'com.github.jnr', name: 'jnr-ffi', version: '2.1.9'
//FFIが依存するライブラリ
compile group: 'org.ow2.asm', name: 'asm', version: '7.0'
}
関数定義のためにInterfaceを作る
Cで作ったライブラリを呼び出す為、仕様をInterfaceで定義します。
Interfaceでのメソッド定義は、ヘッダファイル(.h)での関数宣言と揃えます。
今回は、最大公約数を求める、ユークリッドの互除法を実装しました
Interfaceを作成する
euclid.hと同じ内容を、Interfaceで記述します。
public interface test {
int euclid(int m, int n);
}
Mainクラスを作成する
呼び出し側のJavaファイルを作成します。
import jnr.ffi.LibraryLoader;
public class Test {
public static void main(String[] args) {
System.out.println(
/* fact型のオブジェクトをロードする
createでは、指定するInterfaceのclassを渡す
loadの引数では、文字列でdllファイルのファイル名(拡張子を除く)を渡す */
LibraryLoader.create(test.class).load("test")
//euclidを実行する
.euclid(2,4)
);
}
}
Cで処理を実装する
int euclid(int i, int j);
int euclid(int m, int n){
int tmp, r;
if(n > m){
tmp = m;
m = n;
n = tmp;
}
r = m % n;
if(r == 0) return n;
return euclid(n, r);
}
はい、普通にCでヘッダファイルとプログラムを実装しました。
.dllファイルを生成する
Foreign Function Interfaceでは、ダイナミックリンクライブラリを利用するため、.dllファイルを生成します。
dllを作成するときは、OSのbit数に合わせたものを作成しましょう。
ちなみに、OSやbit数が違う・そもそもdllがないなどの状態で実行すると、
java.lang.UnsatisfiedLinkError: unknown
というエラーがでます。
筆者は、MinGWのgccを使いました。
gcc -shared -o test.dll test.cpp
システム・プロパティの指定
作成した.dllファイルをプロジェクトに組み込むために、システムプロパティのjava.library.pathにパスを渡す必要があります。
Javaでは、javaコマンドの実行時に、以下のようにシステムプロパティを設定できます。
java -D<プロパティ名>=<プロパティ>
今回の場合は、このように実行することで.dllファイルを組み込むことが出来ます。
java -Djava.library.path=C:\\test.dll Test
Gradleでシステム・プロパティの指定
Gradleで実行するときは、わざわざjavaコマンドを打つことはしないですよね?
そこで、build.gradleに少し追加することで、実行ボタンで実行するときにシステムプロパティを渡せるようにします。
//タスク名は任意?
task launch(type: JavaExec) {
//systemPropertyを指定したいメインクラス名を指定
main = "Test"
//ここはおまじない
classpath sourceSets.main.runtimeClasspath
systemProperty "java.library.path", "<.dllファイルのパス>"
}
Interface生成の自動化
jextract
.hファイルから(Interfaceが入った)jarを自動生成するツール
対応OS
jextractは、現在Project PanamaのEarly Access版にのみ同梱されています。
困ったことに、現在(2019/1/14)はLinux版とMac版しか用意されていないようです。
仕方がないので、筆者はBash on Ubuntu on Windowsを導入しました。
Panama Early Accessのインストール
普通にwgetでバイナリを貰ってきました。
make installなどは不要なようです。
tarコマンドで解凍すればすぐに利用する事ができます
ついでにパスも通しておきましょう。
wget https://download.java.net/java/early_access/panama/archive/0/binaries/jdk-12-foreign+0_linux-x64_bin.tar.gz
jextractの実行
.hファイルを指定するだけです。
jextract test.h
生成されたjarファイル(ここではtest.h.jar)をプロジェクトに追加します。
(編集 -> プロジェクト構造 とかでライブラリに指定します。)
これで、一々同じ記述をInterfaceとヘッダに記述する必要がなくなりました。
本題
で、このjarとdllを置くときに一々ファイル名をプロパティに入れたりするのが嫌だったのと、jar内にdll入れた場合のシステムプロパティのしてお方法が分からなかったので、代わりにやってくれる奴を作りました。
プログラムはgithubに置いておくので、使いたい人は拾って使ってください。
Javakky/UseC4ffi4Windows
使い方
dllをリソースフォルダに置きます。
loadDllに、.dllファイルのパス(/resourceより下)とInterfaceのクラスを渡すだけで、オブジェクトが返ってきます。
public class LoadDllTest {
public static void main(String[] args) {
//これだけでオブジェクトが取れる
IClassA func = LoadDll.loadDll("<.dll名>", <インターフェース>.class));
}
}
gradleで使う
bintryで公開してgradleプロジェクトからも利用できるようにしました。
repositories {
maven {
url 'https://dl.bintray.com/javakky/maven'
}
}
dependencies{
compile group: 'com.github.javakky', name: 'jnr-load-dill', version: '1.0.1'
}