実行時にjava.library.pathを変更する
  Java, プログラミング

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を実行時に変更して、ライブラリーの検索パスに反映させる方法は以上です。