前面在《基本使用》、《進階》這兩篇文章,應該算是把 Boost.DLL 的一般使用狀況都大概寫過了;不過實際上,Heresy 這邊真正要拿來用的話,應該不會直接匯出函式或變數,而是會採用匯出自訂類別(class)的形式吧~
這樣的概念,主要是自己定義一個抽象類別,在裡面規範好需要那些變數、要實作哪些成員函式,之後就只需要操作這個類別的物件就好了~基本上,應該算是比較物件導向的做法。
而這偏就是來稍微寫一下,該怎麼透過 Boost.DLL、把自訂列別當作外掛的介面來用。
使用自訂類別
這邊的作法,主要是參考官方文件的「Factory method in plugin」這段。
一般來說,就是先定義好一個抽象類別,以及所必要的函式,然後在模組的部分,再繼承這個抽象類別,來實作他的內容。
比如說,在 API 定義的 header 檔案,可以寫成下面的形式:
#pragma once // STD Header #include <string> #include <vector> // Boost Header #include <boost/shared_ptr.hpp> // Define compute class class CMyAPI { public: virtual std::string getName() const = 0; virtual double compute(const std::vector<double>& rData) const = 0; }; // define create function typedef boost::shared_ptr<CMyAPI>(API_Create)();
這邊就是定義一個抽象類別 CMyAPI,裡面基本上就是兩個函式,一個是 getName()、藉此取得這個模組的名稱,另一個則是 cpmpute(),用來計算一個 vector 的內容。
最後,則是定義一個 API_Create 的型別、代表要一個用來建立這個模組的函式。
而實作的時候,基本上可以寫成像是下面這樣:
#include "../API/MyAPI.h" #include <iostream> #include <boost/shared_ptr.hpp> #include <boost/dll/alias.hpp> class CAvg : public CMyAPI { public: double compute(const std::vector<double>& rData) const override { // return result } public: static boost::shared_ptr<CMyAPI> create() { return boost::shared_ptr<CMyAPI>( new CAvg() ); } }; BOOST_DLL_ALIAS(CAvg::create, create_plugin )
這邊就是定義了一個 CAvg 來繼承 CMyAPI,在裡面寫他的實作;另外,這邊還撰寫了一個 create() 的靜態函式,是用來建立 CAvg 的物件用的。
最後,則是用 BOOST_DLL_ALIAS 來把 CAvg::create() 這個函式,以 create_plugin 這個別名來匯出。
至於主程式的部分,要使用的話,則可以寫成下面的形式:
auto funcExt = boost::dll::import_alias<API_Create>(pathFile, "create_plugin"); boost::shared_ptr<CMyAPI> pModule = funcExt();
基本上,就是先透過 import_alias<>() 來匯入 create_plugin 的函式,之後再透過他建立出 pModule 這個模組,並使用它來做計算了~
這邊的範例可以參考:https://github.com/KHeresy/Boost.DLL.Example/tree/ClassExample;裡面除了本來的 DLLa,還另外寫了一個 DLLb 的模組,而在主程式的部分,也是改成去掃描整個資料夾,來讀取所有的 .dll / .so 檔。
當然,如果要比較簡單一點,也可以參考官方的「Plugin basics」,直接在模組的程式中,建立出 CAvg 的物件並匯出;但是這樣的缺點,是比較難以細緻地控制他的生命週期就是了。
解決 DLL/SO 過早卸載的問題
感覺上,寫到上面這樣,應該就可以快快樂樂地使用了吧?很遺憾的,這樣的寫法雖然是按照官方文件的範例寫的,但是在許多時候,卻是有問題的。
問題是什麼?在上面的例子裡,透過 funcExt() 這個外部函式所建立出來的 pModule 的型別是 shared_ptr<CMyAPI>,理論上應該會自己做資源的控管,但是實際上在使用的時候,應該可以發現,在他真的需要被釋放的時候,是會整個當掉的…
這邊舉的簡單的例子。如果想建立一個 GetModule() 的函式,來把匯入 funcExt()、產生 pModule 的過程包起來的話,這個函式大致上可以寫成下面的樣子:
boost::shared_ptr<CMyAPI> GetModule(const boost::filesystem::path& pathDLL) { auto funcExt = boost::dll::import_alias<API_Create>(pathDLL, "create_plugin"); return funcExt(); }
感覺上這樣應該沒什麼問題吧?但是實際上,GetModule() 所回傳的 shared_ptr<CMyAPI> 物件,在要被清除的時候,就會當掉。
為什麼呢?在官方的「Misuse」這份文件(網頁)中的「Program crashes after plugin unload」有提到,這是由於 Boost.DLL 的載入(load)和卸載(unload)機制造成的 shared_ptr<CMyAPI> 的 deleter 被過早卸載所造成的。
沒理解錯的話,Boost.DLL 在執行 import_alias<>() 的時候,會把 pathDLL 載入到系統,而當他回傳的物件 funcExt 消失的時候,也就會把 pathDLL 卸載掉;而在 pathDLL 被卸載時,funcExt() 所產生的 shared_ptr<CMyAPI> 這個 smart pointer 內部的 deleter 就會跟著失效,導致當需要用到這個 deleter 的時候,就會讓程式當掉。
所以,解決的方法呢?基本上就是要想辦法確定:只要 shared_ptr<CMyAPI> 還在的時候,pathDLL 都不能被卸載,理論上就可以了!
而官方文件中的「Advanced library reference counting」,基本上就是透過自訂義 share_ptr<> 的方法,來做這件事;Heresy 這邊則是根據他的概念,改寫成 Heresy 自己覺得比較簡單的形式來做。
首先,要修改的是標頭檔中,對於 API_Create 的定義;他本來回傳的型別是 shared_ptr<CMyAPI> 這種智慧指標,而現在要改成原生的指標,也就是變成:
// define create function typedef CMyAPI*(API_Create)();
而對應的,在 CAvg 的實作裡面,也需要把 create() 這個對應到 API_Create 的函式,做對應的修改;修改後的結果如下:
static CMyAPI* create() { return new CAvg(); }
在模組的部分,只需要這樣修改就可以了。
至於在應用程式端呢,則比較麻煩一點。首先,這邊是仿照官方文件的方法,定義一個 library_holding_deleter 的結構,用來取代 share_ptr<CMyAPI> 預設的 deleter;其定義如下:
struct library_holding_deleter { library_holding_deleter(boost::shared_ptr<boost::dll::shared_library> libDLL) : mLib(libDLL){} void operator()(CMyAPI* p) const { delete p; } boost::shared_ptr<boost::dll::shared_library> mLib; };
可以看到,由於它的功用是 deleter,所以需要時做一個 call operator、來做刪除 CMyAPI 的動作;而除此之外,它也就只有一個成員變數 mLib、用來維持一個 boost::shared_ptr<shared_library> 持續存在。
接下來,則是改寫 GetModule() 這個函式,其內容如下:
boost::shared_ptr<CMyAPI> GetModule( const boost::filesystem::path& pathDLL ) { boost::shared_ptr<boost::dll::shared_library> mLib = boost::make_shared<boost::dll::shared_library>(pathDLL); if (mLib && mLib->has("create_plugin")) { std::function<API_Create> funcCreate = mLib->get_alias<API_Create>("create_plugin"); CMyAPI* pModule = funcCreate(); return boost::shared_ptr<CMyAPI>(pModule, library_holding_deleter(mLib)); } return nullptr; }
在這邊,首先是透過 shared_library 來作為 DLL/SO 的生命週期管理,之後,則是透過 get_alias<>() 這個函式,來取得 funcCreate() 這個用來建立 CMyAPI 物件的函式。
之後,則是執行 funcCreate() 來建立出 pModule;和之前不同的地方,是這邊的 pModule 不再是 smart pointer、而是 RAW pointer。
而再來,就是把 pModule 轉換成 smart pointer 的形式、讓系統可以自動進行資源管理;這時候,可以看到第二個參數就是建立了一個 library_holding_deleter、來作為 pModule 的 deleter。而在建立 library_holding_deleter 的時候,則是要對應的 shared_library 物件、也就是 mLib 傳進去,讓他保有一份 shared_library 以避免被卸載掉。
如此一來,只要 GetModule() 這個函式所回傳的 share_ptr<CMyAPI> 物件還在,就會因為他的 deleter library_holding_deleter 裡面有一份 boost::shared_ptr<shared_library> 存在、而避免被載入 dll/so 被系統自動卸載了~
這部分的範例可以參考:https://github.com/KHeresy/Boost.DLL.Example/tree/LibraryHolder;不過實際上,這樣寫到底還有沒有可能有其他問題?恩,不知道。 XD
不過,基本上應該是還有架構上最佳化的空間在就是了。
Boost.DLL 這系列就先寫到這裡了,除非指厚實作的時候有碰到什麼覺得值得紀錄的,否則應該不會有續篇了吧。