跨平台的 plugin 開發函式庫:Boost DLL-搭配自訂類別

| | 0 Comments| 09:34
Categories:

前面在《基本使用》、《進階》這兩篇文章,應該算是把 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 這系列就先寫到這裡了,除非指厚實作的時候有碰到什麼覺得值得紀錄的,否則應該不會有續篇了吧。

Leave a Reply

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *