一、背景
在給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-loadlibrarya