1. 程式人生 > >Windows平臺LoadLibrary載入動態庫搜尋路徑的問題

Windows平臺LoadLibrary載入動態庫搜尋路徑的問題

一、背景

在給Adobe Premiere/After Effects等後期製作軟體開發第三方外掛的時候,我們總希望外掛依賴的動態庫能夠脫離外掛的位置,單獨儲存到另外一個地方。這樣一方面可以與其他程式共享這些動態庫,還能保證外掛安裝時非常的清爽。就Adobe Premiere Pro/After Effects來說,外掛檔案是放到C:\Program Files\Adobe\Common\Plug-ins\7.0\MediaCore(Windows平臺)的。這個是PremierePro和AfterEffects的公共外掛目錄,二者在啟動的時候都會嘗試去這個位置載入外掛。與此同時,我們希望自己開發的外掛所依賴的動態庫放到另外的位置,另外也希望外掛顯示連結的動態庫能夠儘量少。因為如果是顯式連結的話,這些外掛依賴的動態庫必須和外掛儲存在同一個位置。不然外掛找不到這些依賴檔案就會載入失敗的。當然,我們也可以在環境變數裡面增加一條路徑,但是這容易汙染環境變數,或者與其他的程式庫產生衝突。LoadLibrary在這個時候就產生作用了。LoadLibrary通過將指定路徑的動態庫載入到當前的呼叫程序,然後獲取其匯出的函式就可以正常使用了。對於像第三方外掛這樣的應用場景,LoadLibrary可以說是個不錯的實現方式。但是正因此也有個弊端,我們無法使用工具得知其的依賴庫。

二、使用例項

我們在給Adobe Premiere Pro開發的一款外掛中,正是使用了這種方法:
(1)首先從登錄檔中獲取到我們外掛依賴的動態庫檔案所在的位置:

 1 bool GetInstallationPath(std::string& result) {
 2     DWORD data_type;
 3     CHAR value[1024];
 4     PVOID pv_data = value;
 5     DWORD size = sizeof(value);
 6     auto err = RegGetValue(HKEY_CLASSES_ROOT, "test_app\\plugin", "install_location", RRF_RT_ANY, &data_type, pv_data, &size);
 7     if (err == ERROR_SUCCESS) {
 8         std::string filepath(value); 
 9         std::regex_replace(std::back_inserter(result), filepath.begin(), filepath.end(), std::regex("[\\\\/]+[^\\\\/]+$"), "");
10         return true;
11     }
12     return false;
13 }

(2)通過呼叫LoadLibrary來載入指定的依賴庫

std::string    dirname;
if (!GetInstallationPath(dirname)) {
    return false;
}  
SetDllDirectory(dirname.c_str());
insmedia_dll.handle = LoadLibrary("core.dll");

如上述程式碼所示,我們的外掛唯一依賴的動態庫叫core.dll。而core.dll檔案存放的位置記錄在登錄檔中。程式先從登錄檔中獲取core.dll所在的資料夾,然後設定到DLL的搜尋路徑中。最後再呼叫LoadLibrary載入它。在最初開發及釋出後,外掛執行的很好。然而,在Adobe釋出Premiere Pro CC 2020之後,外掛就不工作了。這是為啥呢?根據過往的經驗來看,外掛載入不上只有一個原因:依賴的動態庫缺失或者是載入錯了版本。那麼,我們就來看看到底是哪個依賴載入錯了導致外掛載入失敗呢?通過在WinDBG裡面除錯看到了如下的差異:

看上圖很顯然,我們的外掛在載入ffmpeg的庫檔案時,先找到了PremierePro安裝根目錄裡面的版本了。而PremierePro使用的ffmpeg版本顯然跟我們不一樣。正是因為這兩個庫的版本不對,導致我們的外掛載入失敗了。那麼,LoadLibrary這種方法顯然還是存在一些Bug了。我們的core.dll還依賴OpenCV、ffmpeg等第三方庫。看MSDN的解釋是,LoadLibrary會先從呼叫程序的目錄下搜尋動態庫的依賴。這樣的行為顯然不是我們想要的。這個時候,我們還有個選擇:使用LoadLibraryEx。具體的使用方法仍然一樣,只不過傳給LoadLibraryEx的第一個引數是我們要載入的動態庫的絕對路徑:

 1 std::string    dirname;
 2 if (!GetInstallationPath(dirname)) {
 3     return false;
 4 }  
 5  
 6 std::string absolute_path = dirname + "\\InsMedia.dll";
 7 insmedia_dll.handle = LoadLibraryEx(absolute_path.c_str(), nullptr, LOAD_WITH_ALTERED_SEARCH_PATH);
 8 if (!insmedia_dll.handle) {
 9     return false;
10 }

注意到第三個引數為LOAD_WITH_ALTERED_SEARCH_PATH,通過指定LOAD_WITH_ALTERED_SEARCH_PATH,讓系統DLL搜尋順序從DLL所在目錄開始。這樣就能夠保證載入動態庫的時候優先載入我們打包的動態庫。從而避免因為動態庫載入錯誤導致外掛失敗。

從上圖可以看到,所有依賴的動態庫都變成了我們自己提供的庫檔案了,外掛也能正常載入了。完美!

三、參考連結

1. https://blog.csdn.net/cuglifangzheng/article/details/50580279
2. https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-loadlibr