チリペヂィア

リンクフリー。サンプルコードなどは関連記事内でライセンスについて明示されない限り商用利用なども自由に行って構いませんが、自己責任でお願いします。またこれら日記内容の著作権自体は放棄していません。引用部分については引用元の権利に従ってください。

.Netで、任意の場所のDLLをロードさせたい時

(とりあえず.Net2010)

1.アプリケーション構成ファイルを使用する
いわゆるconfigファイルにデフォルトのロードディレクトリを指定してしまう方法です。おそらく標準的な方法のように思われますが今回はスルーします。DLLにはconfig設定できなさそう。そういう時に次案。

2.AssemblyResolveイベントを処理する
AssemblyResolveイベントは標準の方法でDLLサーチしても、要求に適応した型情報が見つからない場合に発生するイベントのようです。

というわけで、AssemblyResolveイベントにメソッドを追加して、自分でAssembly.LoadFrom()で好きなファイルパスからアセンブリを読み込んで返してやればよい事になります。

自分がDLLだったとして、そこからさらに"自分のパス(拡張子抜き)/lib"という「自分(DLL)用のDLLライブラリフォルダ」を探査するイメージでサンプルを書いてみます(*1)。

System.Reflection.Assembly CustomAssemblyResolve(System.Object sender, System.ResolveEventArgs args)
{
	System.Reflection.AssemblyName TargetName = new System.Reflection.AssemblyName(args.Name);
	System.Reflection.Assembly ExecutingAssembly = System.Reflection.Assembly.GetExecutingAssembly();

	// 要求のあったアセンブリ名は自分の管轄?
	bool isFind = false;
	System.Reflection.AssemblyName[] ReferencedAssemblies = ExecutingAssembly.GetReferencedAssemblies();
	foreach (System.Reflection.AssemblyName name in ReferencedAssemblies)
	{
		if (System.Reflection.AssemblyName.ReferenceMatchesDefinition(TargetName, name))
		{
			isFind = true;
			break;
		}
	}
	if (isFind == false) return null; // 辞退

	// もし読込済みのアセンブリで対応できるならまずそれを使います。
	foreach (System.Reflection.Assembly assm in System.AppDomain.CurrentDomain.GetAssemblies())
	{
		if (System.Reflection.AssemblyName.ReferenceMatchesDefinition(TargetName, assm.GetName())) return assm;
	}

	System.String DllPath = null;
	try
	{
		// DLLを検査するフォルダパスを適当に生成します。DLLのフルパスから、末尾の".dll"を削除して"\lib"を追加するのと同じ処理をします。
		System.String DllLoadDirectory = System.IO.Path.Combine(
			System.IO.Path.GetDirectoryName(ExecutingAssembly.Location),
			System.IO.Path.GetFileNameWithoutExtension(ExecutingAssembly.Location) + "\\lib");

		// DLLフォルダパスとAssemblyName.Nameプロパティを使ってそれらしい名前を生成します。
		DllPath = System.IO.Path.Combine(DllLoadDirectory, TargetName.Name + ".dll");
	}
	catch (System.Exception) { return null; } // ウボァー(IO.Pathはぶっちゃけ例外吐きやすい…)。

	// 生成したDllPathをとりあえず読み込んで返します。
	if (System.IO.File.Exists(DllPath))
	{
		try { return System.Reflection.Assembly.LoadFrom(DllPath); }
		catch (System.Exception) { }
	}

	return null;
}

で、このメソッドを、外部のDLLが必要になるより前の段階で System.AppDomain.CurrentDomain.AssemblyResolve イベントに追加すれば、アセンブリ未解決エラーを起こす前にDLLを探しに行かせることが出来ます。

補足

書いておいてナンですが、AssemblyResolve で LoadFrom するのは、やりたい放題なだけに洗練されてないと言うか、やや留意点が多いような気がします。というのも実際いろんな斜め上の抜け方があります。まずDLLを改名してたらアウトです。だからと言って、対象ディレクトリの.dllファイルを総当りで開きまくるにはアンロード処理が欲しくなります。しかもより厳密に言うと、同名異DLLを読み込んじゃったケースを弾く必要もあり、それには読み込んだ上でAssemblyNameを比較すると良いのですが、やはり弾いた後にDLLのアンロード処理が欲しくなります。DLLのアンロードは厄介で、まずメモリ管理を切り離すためAppDomainを別に作って、そこにアセンブリを読み込む事になります。別のAppDomainでDLLを動かすには、DLL側にそれなりの対応が必要なので、もしAppDomainの境界超えを想定しないDLLをロードするには、どっちみち一度アンロードして、安全確認済みのDLLパスから自Domainへ再ロードする羽目になります(なんらかのキャッシュが効けば単純倍コストまではいかないと思いますが…)。なので技術的にどうこうするだけじゃなく「DLLハック検知⇒即アサーション的例外発動でいいや」とか「DLLの置換ハックとかアリアリでオッケー」など、仕様面で着地させるのも含めて考える形になるんじゃないでしょうか。

*1:DLLがDLLを呼ぶのもちょっとクドいんですが、ある程度は名前空間や機能ごとにDLLを分けちゃった方がプロジェクト管理がラクになるような気がします。