目的
依存 dll を本体 exe とは別の場所に配置したい。
例えば以下のようなシチュエーション:
- 開発しているものがプラグイン dll であり、依存 dll を本体 exe と同じ場所に置くのが難しい
- プラグイン dll の場所は dll サーチパスには入っておらず、同ディレクトリに依存 dll を置いても解決できない
- プラグイン開発者が一度はハマるであろう問題
- 簡単なツールを配布しようとしているが、依存 dll が多数あって exe を見つけづらいため、別ディレクトリに置きたい
- 単なるユーザビリティの問題だが、自分的に無視したくない部分
対応策
インポートライブラリを使わず実行時に LoadLibrary() & GetProcAddress() するとか、dll サーチパスを設定して本体 dll をロードするだけの proxy dll/exe を作成するとかでも解決できるが、どちらも非常に面倒。
幸いにもこういう問題を解決するためのそのものズバリな機能が用意されている。それが delay load である。
Visual Studio 上のリンカオプション "Delay Loaded Dlls" で delay load させたい dll を指定しておく (コマンドラインオプションだと /DELAYLOAD)。これで指定された dll は、その dll の関数が呼ばれる時までロードが遅延され、必要になった時 LoadLibrary() & GetProcAddress() 相当の処理で解決される。よって、dll の関数を呼ぶ前に dll サーチパスを追加しておけばあとは自動で綺麗に解決してくれる。
(ちなみに /DELAYLOAD は #pragma comment(linker) では指定できないオプションらしく、C++ ソース側から指定することはできない)
dll サーチパスの追加は、SetDllDirectory() による指定、SetEnvironmentVariable() で環境変数 PATH に追加などの方法がある。SetDllDirectory() の方が望ましいと思われるが、複数のディレクトリは設定できない点に注意が必要。AddDllDirectory() というそれっぽい API もあるが、これは LoadLibraryEx() に LOAD_LIBRARY_SEARCH_USER_DIRS オプションを入れた時のみ考慮されるものであり、delay load の場合効果がない。
他への影響を最小限に留めたい場合、GetDllDirectory() で元のサーチパスを保存、SetDllDirectory() でサーチパスを指定、__HrLoadAllImportsForDll() で明示的に依存を解決 or エラーハンドリング、SetDllDirectory() で元のサーチパスを復元、とやるのがいいと思われる。
対応例
プラグイン dll のケースでは、実行中に dll 自身のディレクトリを得る必要がある。これは以下のように実現できる。
std::string GetCurrentModuleDirectory()
{
// この関数自身が所属する exe や dll のディレクトリを返す
HMODULE mod = 0;
char buf[MAX_PATH]{};
::GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, (LPCSTR)&GetCurrentModuleDirectory, &mod);
::GetModuleFileNameA(mod, buf, std::size(buf));
return std::string(buf, std::strrchr(buf, '\\'));
}
あとは DllMain() とかで SetDllDirectory() すれば綺麗に問題解決できる。
BOOL WINAPI DllMain(HINSTANCE hinstDll, DWORD dwReason, LPVOID lpReserved)
{
if (dwReason == DLL_PROCESS_ATTACH) {
auto dll_path = GetCurrentModuleDirectory();
::SetDllDirectoryA(dll_path.c_str());
}
return TRUE;
}