DLLからDLLを呼ぶのはこんなに難しい

DLLの検索パスなんて普通は意識する必要ありません。実行ファイルと同じディレクトリに置いておけばちゃんと読み込んでくれます。しかし、あなたが実行ファイルではなくDLLを開発している場合はちょっと注意が必要です。

今私たちは master.dll を開発しているとします。このDLLは何か別のアプリから使ってもらうための汎用的なライブラリです。例として rundll32.exe から呼び出されるものとしましょう。slave.dll は master.dll が依存している別のライブラリです。

この2つのDLLを同じディレクトリに置いて rundll32.exe から呼び出すとどうなるでしょうか?

rundll32 C:\temp\master.dll,Test

結論から言って以下のようになります。
[Win32] LoadLibrary のサーチパス

Hoge.dll が見つからなかったため、このアプリケーションを開始できませんでした。アプリケーションをインストールし直すとこの問題は解決される場合があります。 」や「指定されたモジュールが見つかりません。」というメッセージ、もしくは 126 や 0x8007007E というエラー番号。

slave.dll が見つからず、LoadLibrary は失敗します。master.dll は実行ファイルではなく数あるDLLのひとつでしかないので、slave.dll が同じディレクトリにあっても rundll32.exe からは見つけることができません。

カレントディレクトリを適切に指定していればエラーは起こりません。

cd C:\temp
rundll32 master.dll,Test

あるいは、実行ファイルのほうを修正できるのなら、LoadLibraryEx と LOAD_WITH_ALTERED_SEARCH_PATH を使って解決できます。

しかしライブラリの立場からは根本的な解決になりません。master.dll をどのディレクトリから LoadLibrary しても確実に読み込めるようにするにはどうすれば良いでしょうか?


Visual Studio 限定になりますが、リンカの遅延ロード機能を使うことで解決できます。
Download Visual Studio 2005 Retired documentation from Official Microsoft Download Center

Visual C++ 6.0 以前のバージョンでは、実行時に DLL を読み込む方法は、LoadLibrary と GetProcAddress を使用する方法だけでした。DLL は、その DLL を使用する実行可能ファイルまたは DLL が読み込まれたたときに、オペレーティング システムによって読み込まれました。

Visual C++ 6.0 からは、DLL との静的なリンクの場合に、DLL の関数の呼び出し時に初めて DLL が遅延読み込みされるようにするオプションをリンカで選択できるようになりました。

LoadLibrary と GetProcAddress を使った動的リンクは関数ポインタの作成が面倒ですが、遅延ロードでは静的リンクのままで動的リンクと同じ恩恵を受けられます。master.dll の DllMain が呼ばれてから slave.dll の遅延ロードが行われるまでに若干の猶予があるので、その間に適切に slave.dll を読み込めるように細工すればいいわけです。

大きく分けて4つの方法があります。

  1. 遅延ロードの処理(__delayLoadHelper2)が走る前に自分で slave.dll のフルパスを指定して LoadLibrary を呼んでしまう。すでにDLLが読み込まれていれば検索パスにないDLLを LoadLibrary しても失敗しない。
  2. SetCurrentDirectory でカレントディレクトリを指定する。
  3. SetDllDirectory で検索パスを追加する。
  4. 遅延ロードの dliNotePreLoadLibrary をカスタマイズする。

いずれを選ぶにせよ kernel32.dll のAPIだけで実現可能です。つまり master.dll が LoadLibrary で読み込まれる時、LoadLibrary がハンドルを返す前に DllMain の中で処理できます。が、DllMain の中で LoadLibrary を呼ぶのは非推奨らしいので、1. はやめておいたほうが良さそうです。
2. 3. も副作用があります。逆に外部のコードに上書きされて失敗するかもしれません。
というわけで DllMain で安全に初期化できて、DLLの中で完結できる 4. がいいんじゃないかと思います。

最後に実装例のソースを載せておきます。

#include <delayimp.h>

HINSTANCE g_hInst = NULL;

FARPROC WINAPI DliNotifyHook(unsigned dliNotify, PDelayLoadInfo pdli)
{
	switch(dliNotify){
		case dliNotePreLoadLibrary:
			// master.dll のあるディレクトリにリダイレクト
			TCHAR path[MAX_PATH];
			::GetModuleFileName(g_hInst, path, _countof(path));
			// C:\temp\master.dll を C:\temp\slave.dll に
			LPTSTR ptr = ::_tcsrchr(path, _T('\\'));
			ptr++;
			::_tcscpy(ptr, _T("slave.dll"));
			
			HMODULE hMod = ::LoadLibrary(path);
			return (FARPROC)hMod;
	}
	return NULL;
}

BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
	switch(ul_reason_for_call){
		case DLL_PROCESS_ATTACH:
			g_hInst = hModule;
			// delayimp.h で定義されているグローバル変数を書き換える
			// これで通知フックのインストールは完了
			__pfnDliNotifyHook2 = DliNotifyHook;
			break;
	}
	return TRUE;
}