実行時にjava.library.pathを変更する

Java でプラグイン機構を作ったときのお話です。プラグインを JAR ファイルとして実装して動的にロードさせることは簡単にできたのですが そのプラグイン JAR ファイルがネイティブライブラリー DLL を必要とした場合に DLL を動的にロードさせるのに手間取りました。

実行時に java.library.path を変更して ユーザーが指定した場所にある DLL をロードできるようにする方法を紹介します。

仕組みから説明しているので少し長いです。手っ取り早く方法だけ知りたいという方は以下のリンクをクリックしてジャンプしてください。

ライブラリーロードの仕組み

Java では System.load() または System.loadLibrary() を使用してネイティブライブラリー DLL をロードすることができます。これらにはライブラリーのフルパスを指定するか ライブラリー名のみを指定するかという違いがあります。

ライブラリーのフルパスを指定する場合は System.load() を使用します。

System.load("C:/mylibs/foobar.dll");

ライブラリー名を指定する場合は System.loadLibrary() を使用します。このとき java.library.path プロパティーで設定されているパスからライブラリーが検索されることになります。

System.loadLibrary("foobar");

この java.library.path プロパティーというのが曲者で このプロパティーを実行に変更しても なぜかライブラリーの検索対象になってくれません。

//プロパティーの変更はできるが実際の検索対象パスは変わらない
System.setProperty("java.library.path", "C:/mylibs"); 

Java のソースコードを調べる

ソースコードを追いかけて ライブラリーがロードされる仕組みを調べてみます。

System.load

System.load() のソースコードは以下のようになっていました。

public static void load(String filename) {
	Runtime.getRuntime().load0(Reflection.getCallerClass(), filename);
}

Runtime.load0() を呼び出していますね。このソースコードは以下のようになっています。

synchronized void load0(Class<?> fromClass, String filename) {
	SecurityManager security = System.getSecurityManager();
	if (security != null) {
		security.checkLink(filename);
	}
	if (!(new File(filename).isAbsolute())) {
		throw new UnsatisfiedLinkError(
			"Expecting an absolute path of the library: " + filename);
	}
	ClassLoader.loadLibrary(fromClass, filename, true);
}

セキュリティーチェックをして ClassLoader.loadLibrary() を呼び出していることが分かります。ここで少し System.load() の調査を中断して System.loadLibrary() を掘り下げていきます。

System.loadLibrary

System.loadLibrary() のソースコードは以下のようになっていました。

public static void loadLibrary(String libname) {
	Runtime.getRuntime().loadLibrary0(Reflection.getCallerClass(), libname);
}

System.load() と似ています。Runtime.loadLibrary0() のソースコードを見てみます。

synchronized void loadLibrary0(Class<?> fromClass, String libname) {
	SecurityManager security = System.getSecurityManager();
	if (security != null) {
		security.checkLink(libname);
	}
	if (libname.indexOf((int)File.separatorChar) != -1) {
		throw new UnsatisfiedLinkError(
			"Directory separator should not appear in library name: " + libname);
	}
	ClassLoader.loadLibrary(fromClass, libname, false);
}

ここで System.load() のときと同じ ClassLoader.loadLibrary() が出てきました。つまり System.load() System.loadLibrary() も最終的には ClassLoader.loadLibrary() に辿り着くわけです。

ClassLoader.loadLibrary

辿り着いた ClassLoader.loadLibrary() のソースコードを追っていきましょう。このメソッドの実装は少し長いですが 重要なのは先頭の数行だけですので そこだけ抜粋します。

static void loadLibrary(Class<?> fromClass, String name, boolean isAbsolute) {
	ClassLoader loader = (fromClass == null) ? null : fromClass.getClassLoader();
	if (sys_paths == null) {
		usr_paths = initializePath("java.library.path");
		sys_paths = initializePath("sun.boot.library.path");
	}
	(以下省略)

ここに java.library.path を実行時に変更しても効果がない理由がありました。初回の呼び出し時は java.library.path が参照されて usr_paths フィールドに代入されます。また sun.boot.library.path が参照されて sys_paths フィールドも設定されています。

2 回目以降は すでに sys_paths フィールドが null ではなくなっているので 検索パスの初期化はスキップされます。

これが 実行時に java.library.path プロパティーを変更しても検索パスに反映されない理由です。

ライブラリー検索パスを実行時に変更するには

ソースコードを追いかけたことで検索パスが更新されない原因が分かりました。原因が分かれば対処も簡単ですね。

対処方法は 2 つあります。

1. sys_paths null にする方法

簡単なのは sys_paths フィールドを null にするという方法です。ソースコードを見れば分かる通り sys_paths フィールドが null ではないからパスの初期化がスキップされています。逆に言うと sys_paths フィールドが null になれば もう一度 パスの初期化処理が行なわれるということになります。

sys_paths フィールドは private なので 書き換えるためにはリフレクションを使う必要があります。

java.library.path を変更して 検索パスに反映させる完全なコードは以下のようになります。

// java.library.path を変更します。(この時点では反映されません)
System.setProperty("java.library.path", "C:/mylibs");

// sys_paths フィールドに null を代入します。
// これで次にライブラリーをロードするときに最新の java.library.path が参照されます。
Field sys_paths = ClassLoader.class.getDeclaredField("sys_paths");
sys_paths.setAccessible(true);
sys_paths.set(null, null);

2. usr_paths を直接変更する方法

もうひとつの方法として usr_paths フィールドを直接変更する方法があります。

usr_paths = initializePath("java.library.path");

このコードが初回しか実行されないのが原因なわけですから 直接 usr_paths に検索パスを設定してしまえばいいということになります。

こちらは sys_paths null にして再処理を促す方法に比べて少し複雑です。usr_paths の現在の値を取得して そこに新たなパスを足して再設定するという流れになります。

完全なコード例は以下のようになります。

String newPath = "C:/mylibs";

Field usr_paths = ClassLoader.class.getDeclaredField("usr_paths");
usr_paths.setAccessible(true);

// 現在の検索パスを取得します。
String[] paths = (String[])usr_paths.get(null);

// すでに検索パスに含まれていたら何もせずに復帰します。
for(String path : paths) {
	if(path.equals(newPath)) {
		return;
	}
}

// 検索パスを追加した配列を新しく作成して usr_paths に代入します。
String[] newPaths = Arrays.copyOf(paths, paths.length + 1);
newPaths[newPaths.length - 1] = newPath;
usr_paths.set(null, newPaths);

java.library.path を実行時に変更して ライブラリーの検索パスに反映させる方法は以上です。

この記事を共有しませんか?