Boost.DLL 是 Boost C++ Libraries 在 1.61 版加入的一個新的函式庫,他的設計目的,是讓開發者可以寫出跨平台的 plungin 架構,也就是在程式執行時、再去讀取動態函式庫(DLL、SO/DSO)的檔案來用。
這樣的好處,是可以自己開發一個程式的框架,然後把擴充的方法定義好、讓其他人自行開發符合架構的模組來做擴充;基本上,應該算是一個滿有效、增加程式彈性的方法~
也由於採用這樣的機制可以不需要釋出主程式的原始碼、也可以在不修改主程式的情況下、擴增功能,所以很多軟體,也都是用這種方法來作為擴充、或是提供第三方開發的方法。
而這篇 Heresy 就是簡單測試一下 Boost.DLL 的紀錄了。
首先,官方的文件是:http://www.boost.org/doc/html/boost_dll.html,最基本的使用方法,則可以參考《Getting started》的部分。
Boost.DLL 提供可以匯出、匯入的東西,包括了函式和變數;而由於變數也可以是自己定義的型別,所以基本上彈性也就相當地大了。
要開發這樣的程式,基本上需要兩個(或以上的專案):
-
一個是本來的主程式
以下簡稱主程式。
在 Windows 上來說,就是 .exe 的可執行檔。在他的程式碼裡面,要加入 Boost.DLL 的匯入(import)外部 DLL 檔的功能。
以 Heresy 個人習慣,在 Visual Studio 中,在建立新專案的時候,會選擇「Win32 主控台應用程式」,然後在「應用程式設定」的部分,選擇「主控台應用程式」。 -
另一個專案則是要被匯入的動態函式庫
以下簡稱模組。
以 Windows 來說,就是 DLL 檔。在這個專案的程式碼中,要根據預先定義好的規範,來實作對應的函式,並把這些函式匯出、讓主程式可以使用。
以 Heresy 個人習慣,在 Visual Studio 中,在建立新專案的時候,會選擇「Win32 主控台應用程式」,然後在「應用程式設定」的部分,選擇「DLL」。
撰寫模組
而在一般狀況下,如果要撰寫模組的話,就是要先 include boost/config.hpp 這個檔案,然後在要匯出的變數、或是函式前面,加上「extern "C" BOOST_SYMBOL_EXPORT」就可以了;如果要好看一點的話,也可以像官方範例依樣,先定義:
#define API extern "C" BOOST_SYMBOL_EXPORT
如此一來,要匯出一個變數,就可以簡單地寫成:
API std::string sModuleName;
下面就是一個完整的模組的程式碼的範例:
#include <string>
// Boost Headers
#include <boost/config.hpp>
#define API extern "C" BOOST_SYMBOL_EXPORT
API std::string sModuleName = "Module A";
API std::string getName()
{
return sModuleName;
}
在這個範例裡面,匯出了 sModuleName 這個變數、以及 getName() 這個函式。
之後,只要建置這個專案,產生 DLL 檔就可以了。
撰寫主程式
在主程式的部分,則是要先 include boost/dll.hpp 這個檔案,然後再用 boost::dll::import<>() 這個函式,來把所需要的變數、或是函式,從模組檔案中匯入使用。
他最簡單的使用方法,就是透過 template 的形式、指定要匯入的東西的型別,然後再用引數來指定模組檔案的路徑、以及要匯入的項目名稱,這樣就可以了。
以上面的例子來說,如果要匯入 sModuleName 這個變數的話,可以寫成:
boost::shared_ptr<std::string> pVar = boost::dll::import<std::string>(rPath, "sModuleName");
其中,rPath 的型別是 boost::filesystem 的 path,代表的是 DLL 檔所在的路徑。
而由於 sModuleName 是一個字串,所以這邊匯進來的變數(pVar)的型別,就是一個 boost::shared_ptr<std::string> 的 smart pointer(參考)。
至於函式的部分,也是使用同樣的匯入方法,型別的話則是可以使用標準函式庫的 std::function<>(參考);下面就是要匯入 getName() 這個函式的方法:
std::function<std::string()> funcExt = boost::dll::import<std::string()>(pathDLL, "getName");
之後就可以把 funcExt 當作一般的 function object 來使用了。
整個完整的主程式的程式碼,則會像下面這樣:
#include <iostream>
// Boost Headers
#include <boost/dll.hpp>
int main(int argc, char** argv)
{
#ifdef _WIN32
boost::filesystem::path pathDLL = "DLLa.dll";
#else
boost::filesystem::path pathDLL = "libDLLa.so";
#endif
boost::shared_ptr<std::string> pVar = boost::dll::import<std::string>( pathDLL, "sModuleName" );
std::cout << "Variable: " << *pVar << std::endl;
std::function<std::string()> funcExt = boost::dll::import<std::string()>(pathDLL, "getName");
std::cout << "Function: " << funcExt() << std::endl;
return 0;
}
這邊可以看到,由於 Windows 和 Linux 習慣的動態函式庫檔案命名方式不同,所以還是使用 ifdef 的方法來做區隔,但是其他的部分,程式碼就完全相同了~
而這樣的執行結果,會是:
Variable: Module A
Function: Module A
不過可能要注意的是,由於 Visual Studio 預設的執行路徑會是專案路徑,所以直接在 Visual Studio 按 F5 的話,會因為讀不到所需要的 DLLa.dll 檔、而導致程式當到;所以在執行的時候,建議請把偵錯路徑設定到方案(solution)路徑下對應的 debug 或 release 資料夾(例如 x64 debug 版就是「$(SolutionDir)\x64\Debug」)再執行。
如果有問題的話,也建議請確認 App.exe 和 DLLa.dll 檔有位在同一個目錄中,或是手動將他們放到一起再執行;當然,另一個方法就是修改 pathDLL 的路徑、甚至讓他用自動搜尋的。
Linux 的建置
老實說,Heresy 雖然因為工作上的關係,寫的程式大多會注意跨平台,但是由於主力開發環境還是 Windows + Visual C++,所以其實 Linux 的開發、尤其是 Makefile 的撰寫,並不是那麼熟系。
而這邊,則算是簡單紀錄一下 Heresy 自己摸出來、可以用的 Makefile 了;老實說,細節的參數 Heresy 自己也不熟就是了…
首先,是模組的部分。
INC_PATH = -I/usr/include LIB_PATH = -L/usr/lib TARGET_PATH = ../Bin # for gcc C++ = g++ # system command CP = cp -f RM = rm -f CFLAGS = --std=c++11 -fPIC -g -fvisibility=hidden DLL = libDLLa.so DLL_OBJ = DLLa.o .SUFFIXES: .SUFFIXES: .cpp .o all: $(DLL) $(DLL): $(DLL_OBJ) $(C++) -shared -o $@ $^ $(LIB_PATH) -lboost_system -lboost_filesystem -ldl $(CP) $@ $(TARGET_PATH) .cpp.o: $(C++) $(CFLAGS) $(INC_PATH) -c $< clean: $(RM) *.o $(DLL)
根據這個 Makefile,在建置的時候會先把 DLLa.cpp 編譯成 DLLa.o,然後再和其他函式庫 link、產生出最後的 libDLLa.so。這邊和編譯一般城市的不同,主要就是編譯和連結階段的參數了。
而主程式的 Makefile 則就比較一般了。
INC_PATH = -I/usr/include LIB_PATH = -L/usr/lib TARGET_PATH = ../Bin # for gcc C++ = g++ # system command CP = cp -f RM = rm -f CFLAGS = --std=c++11 APP = App.out APP_OBJ = AppMain.o .SUFFIXES: .SUFFIXES: .cpp .o all: $(APP) $(APP): $(APP_OBJ) $(C++) -pg -o $@ $^ $(LIB_PATH) -lboost_system -lboost_filesystem -ldl $(CP) $@ $(TARGET_PATH) .cpp.o: $(C++) $(CFLAGS) $(INC_PATH) -c $< clean: $(RM) *.o $(APP)
最後,這兩個專案都建置好了之後,檔案會被放到外層的 Bin 資料下,然後再執行 App.out,就可以進行測試了。
Boost.DLL 最基本的使用方法,大概就是這樣了。
這篇文的完整程式碼(含專案、Makefile),可以在 Heresy 的 GitHub 上找到(連結);有興趣的話可以自己試試看。
而實際上,Boost.DLL 還有一些東西可能是會用到的。
像是除了直接使用 BOOST_SYMBOL_EXPORT 把變數或函式匯出外,他也還有提供 BOOST_DLL_ALIAS 這個巨集,可以把現有的函式、變數,取一個新的別名在匯出。
另外,library_info 這個類別(官網)也有提供基本的功能,可以查詢匯入的模組中有那些東西可以用;而 shared_library 這個類別(官網)感覺上則是可以用更物件化的方式、來操作一個匯入的模組。
之後,有機會再來整理後續吧~