4步實現(xiàn)C++插件化編程,輕松實現(xiàn)功能定制與擴展
[TOC]
引言
? 在項目開發(fā)中,我們經(jīng)常面臨為適應不同市場或產(chǎn)品層級而需調整功能的需求。從軟件工程的角度來看,這意味著使用同一套代碼,通過配置來實現(xiàn)產(chǎn)品的功能差異化。實現(xiàn)這一目標的方法多種多樣,本文將探討如何通過 插件化編程 優(yōu)雅地滿足這一需求。
概述
? 插件化編程 是一種通過動態(tài)加載功能模塊(即插件)來增強主程序功能的軟件設計策略。通過制定標準化接口,確保插件與主程序之間的兼容性與獨立性。此方法能顯著提高軟件的靈活性、可擴展性和易維護性,同時支持快速定制及對市場變化的迅速響應。
需求分析
? 通過上述描述,可以將功能需求概括為:使用同一套代碼基礎,實現(xiàn)不同產(chǎn)品的功能差異化。
? 從軟件設計的角度來看,主要功能需求包括:
- 實現(xiàn)不同產(chǎn)品客制化配置
- 通過配置文件來啟用或禁用特定功能。通過配置文件靈活控制功能的開啟與關閉,以滿足不同市場或客戶的具體需求。
- 系統(tǒng)支持查閱配置版本信息。動態(tài)集成配置文件的版本信息,方便現(xiàn)場快速了解當前使用的配置狀態(tài)。
- 配置文件易于管控和維護??椭苹渲脩c具體產(chǎn)品綁定,避免不同產(chǎn)品的配置混淆,確保易于管理和維護;同時,配置文件應設計得易于編輯。
- 實現(xiàn)依據(jù)配置集成指定模塊
- 系統(tǒng)能夠準確識別差異化配置內容。
- 系統(tǒng)支持的功能與配置一致。
設計方案
? 基于上述分析,以下是設計方案的大致流程:
- 配置文件構建
- ① 初步以
modules_configs.cmake作為模塊配置文件。在CMake編譯期間識別配置選項,編譯指定模塊。 - ② 增加配置版本號。在配置文件中增加版本號字段,并在編譯期間將該版本號傳遞至軟件中,由軟件寫入實時環(huán)境。
- ③ 增加配置文件版本管理。每次新增客制化產(chǎn)品時,都需要在工程中添加該產(chǎn)品唯一的客制化配置文件。
- 依據(jù)配置加載指定模塊
- ① 差異化模塊以動態(tài)庫形式呈現(xiàn)。
根據(jù)modules_configs.cmake配置,在編譯期間編譯指定需加載的功能模塊動態(tài)庫。 - ② 統(tǒng)一動態(tài)庫命名前綴、入口函數(shù)命名和入口函數(shù)形式。
- 動態(tài)庫以
libplug前綴命名; - 統(tǒng)一入口函數(shù)名為
PluginEntry; - 函數(shù)形式為
void(*PluginEntryFunc)(std::map<int, SprObserver*>& modules, SprContext& ctx)。
- 動態(tài)庫以
- ③ 各模塊按上述格式完成動態(tài)庫的命名和入口函數(shù)實現(xiàn)。
在PluginEntryFunc函數(shù)實現(xiàn)中,完成該模塊的入口設計。 - ④ 在主程序中調用各模塊入口:
- 首先,主程序通過
dlopen加載libplug前綴的客制化模塊動態(tài)庫; - 其次,通過
dlsym獲取動態(tài)庫的入口函數(shù)PluginEntry; - 最后,通過函數(shù)指針調用動態(tài)庫的入口函數(shù)。
- 首先,主程序通過
詳細設計
主要是通過CMake配置化編譯和插件化編程實現(xiàn)動態(tài)加載,詳細實現(xiàn)如下:
- 配置文件 modules_configs.cmake
# 業(yè)務模塊 Components/Business
set(MODULE_CONFIG_VERSION "DEFAULT_MCONFIG_1001")
set(BUSINESS_MODULES "")
list(APPEND BUSINESS_MODULES OneNetMqtt)
-
MODULE_CONFIG_VERSION作為配置版本號變量:其值遵循[產(chǎn)品]_MCONFIG_[版本號]的命名規(guī)則,每次配置修改時,版本號應遞增。 -
BUSINESS_MODULES作為模塊編譯列表:用于存儲需要編譯的模塊名稱。
- 編譯
BUSINESS_MODULES指定模塊
## Business
# 動態(tài)加載, 配置文件modules_configs.cmake
foreach(module IN LISTS BUSINESS_MODULES)
message(STATUS "Add Business Module: ${module}")
add_subdirectory(${module})
endforeach()
- 通過循環(huán)遍歷
BUSINESS_MODULES, 包含指定模塊的編譯路徑,確保指定的模塊都能被正確編譯。
- 動態(tài)庫入口實現(xiàn)
// The entry of OneNet business plugin
extern "C" void PluginEntry(std::map<int, SprObserver*>& observers, SprContext& ctx)
{
auto pOneDrv = OneNetDriver::GetInstance(MODULE_ONENET_DRIVER, "OneDrv");
auto pOneMgr = OneNetManager::GetInstance(MODULE_ONENET_MANAGER, "OneMgr");
observers[MODULE_ONENET_DRIVER] = pOneDrv;
observers[MODULE_ONENET_MANAGER] = pOneMgr;
SPR_LOGD("Load plug-in OneNet modules\n");
}
- 實現(xiàn)動態(tài)庫入口函數(shù):
PluginEntry作為動態(tài)庫的入口函數(shù),其內部主要負責調用當前模塊的初始化函數(shù)。 - 初始化模塊實例:通過
OneNetDriver::GetInstance和OneNetManager::GetInstance獲取模塊的單例實例。 - 注冊模塊實例:將模塊實例注冊到
observers映射中,以便主程序能夠訪問和使用這些模塊。
- 主程序加載指定動態(tài)庫
- 插件化編程實現(xiàn)流程
void SprSystem::LoadPlugins()
{
std::string path = DEFAULT_PLUGIN_LIBRARY_PATH;
if (access(DEFAULT_PLUGIN_LIBRARY_PATH, F_OK) == -1) {
GetDefaultLibraryPath(path);
SPR_LOGW("%s not exist, changed path %s\n", DEFAULT_PLUGIN_LIBRARY_PATH, path.c_str());
}
DIR* dir = opendir(path.c_str());
if (dir == nullptr) {
SPR_LOGE("Open %s fail! (%s)\n", path, strerror(errno));
return;
}
// loop: find all plugins library files in path
struct dirent* entry;
while ((entry = readdir(dir)) != NULL) {
if (strncmp(entry->d_name, DEFAULT_PLUGIN_LIBRARY_FILE_PREFIX, strlen(DEFAULT_PLUGIN_LIBRARY_FILE_PREFIX)) != 0) {
continue;
}
void* pDlHandler = dlopen(entry->d_name, RTLD_NOW);
if (!pDlHandler) {
SPR_LOGE("Load plugin %s fail! (%s)\n", entry->d_name, dlerror() ? dlerror() : "unknown error");
continue;
}
auto pEntry = (PluginEntryFunc)dlsym(pDlHandler, DEFAULT_PLUGIN_LIBRARY_ENTRY_FUNC);
if (!pEntry) {
SPR_LOGE("Find %s fail in %s! (%s)\n", DEFAULT_PLUGIN_LIBRARY_ENTRY_FUNC, entry->d_name, dlerror() ? dlerror() : "unknown error");
dlclose(pDlHandler);
continue;
}
mPluginHandles.push_back(pDlHandler);
mPluginEntries.push_back(pEntry);
SPR_LOGD("Load plugin %s success!\n", entry->d_name);
}
closedir(dir);
}
void SprSystem::Init()
{
...
LoadPlugins(); // load plugin libraries
// excute plugin entry function
SprContext ctx;
for (auto& mPluginEntry : mPluginEntries) {
mPluginEntry(mModules, ctx);
}
// excute plug module initialize function
for (auto& module : mModules) {
module.second->Initialize();
}
...
}
- 加載動態(tài)庫
LoadPlugins():
加載位于DEFAULT_PLUGIN_LIBRARY_PATH路徑下,前綴為DEFAULT_PLUGIN_LIBRARY_FILE_PREFIX的動態(tài)庫。
獲取并存儲函數(shù)DEFAULT_PLUGIN_LIBRARY_ENTRY_FUNC的地址。 - 主函數(shù)程序入口
Init():
調用LoadPlugins()加載動態(tài)庫。
執(zhí)行獲取到的函數(shù)DEFAULT_PLUGIN_LIBRARY_ENTRY_FUNC。
驗證
- 從日志上看
OneNetMqtt模塊是否正常
09-28 17:02:23.049 146938 SprSystem D: 173 Load plugin libpluginonenet.so success!
09-28 17:02:23.052 146938 EntryOneNet D: 41 Load plug-in OneNet modules
日志上看,動態(tài)庫已經(jīng)加載成功,動態(tài)庫入口日志正常打印,OneNetMqtt模塊啟動正常。
- 查閱系統(tǒng)加載的模塊配置信息
$ cat /tmp/sparrow_version
System Version : Sparrow 1.0.1
C++ Standard : 11
G++ Version : 11.4.0
Gcc Version : 11.4.0
Running Env : Default
Build Time : 2024-09-28 16:50:58
Build Type : Release
Build Host : Beckett
Build Platform : Linux 5.15.153.1-microsoft-standard-WSL2
Module Config : DEFAULT_MCONFIG_1001
系統(tǒng)環(huán)境中模塊配置版本號為DEFAULT_MCONFIG_1001與配置文件中一致
總結
- 插件化編程通過動態(tài)加載功能模塊,實現(xiàn)了軟件的高度靈活性和可擴展性。其主要思路在于加載動態(tài)庫,并調用動態(tài)庫中預定義的入口函數(shù),從而實現(xiàn)主程序與插件之間的解耦。
- 除了實現(xiàn)產(chǎn)品的功能差異化外,插件化編程還可以應用于性能優(yōu)化、安全性增強、用戶體驗提升等多個方面。例如,通過動態(tài)加載最新的安全補丁或功能更新,無需重新啟動整個應用程序。
- 在項目中實現(xiàn)差異化的配置時,建議采用單一配置文件或配置管理系統(tǒng)來集中管理所有配置項,減少因配置錯誤導致的問題。此外,配置文件應具備良好的可讀性和易維護性,避免復雜的多重開關設計,以免造成新開發(fā)人員的理解困難。