4步實(shí)現(xiàn)C++插件化編程梯找,輕松實(shí)現(xiàn)功能定制與擴(kuò)展

4步實(shí)現(xiàn)C++插件化編程,輕松實(shí)現(xiàn)功能定制與擴(kuò)展

[TOC]

引言

? 在項(xiàng)目開發(fā)中燥筷,我們經(jīng)常面臨為適應(yīng)不同市場或產(chǎn)品層級而需調(diào)整功能的需求箩祥。從軟件工程的角度來看,這意味著使用同一套代碼肆氓,通過配置來實(shí)現(xiàn)產(chǎn)品的功能差異化袍祖。實(shí)現(xiàn)這一目標(biāo)的方法多種多樣,本文將探討如何通過 插件化編程 優(yōu)雅地滿足這一需求谢揪。

概述

? 插件化編程 是一種通過動態(tài)加載功能模塊(即插件)來增強(qiáng)主程序功能的軟件設(shè)計(jì)策略蕉陋。通過制定標(biāo)準(zhǔn)化接口,確保插件與主程序之間的兼容性與獨(dú)立性拨扶。此方法能顯著提高軟件的靈活性凳鬓、可擴(kuò)展性和易維護(hù)性,同時(shí)支持快速定制及對市場變化的迅速響應(yīng)患民。

需求分析

? 通過上述描述缩举,可以將功能需求概括為:使用同一套代碼基礎(chǔ),實(shí)現(xiàn)不同產(chǎn)品的功能差異化。

? 從軟件設(shè)計(jì)的角度來看蚁孔,主要功能需求包括:

  1. 實(shí)現(xiàn)不同產(chǎn)品客制化配置
  • 通過配置文件來啟用或禁用特定功能奶赔。通過配置文件靈活控制功能的開啟與關(guān)閉,以滿足不同市場或客戶的具體需求杠氢。
  • 系統(tǒng)支持查閱配置版本信息站刑。動態(tài)集成配置文件的版本信息,方便現(xiàn)場快速了解當(dāng)前使用的配置狀態(tài)鼻百。
  • 配置文件易于管控和維護(hù)绞旅。客制化配置應(yīng)與具體產(chǎn)品綁定温艇,避免不同產(chǎn)品的配置混淆因悲,確保易于管理和維護(hù);同時(shí)勺爱,配置文件應(yīng)設(shè)計(jì)得易于編輯晃琳。
  1. 實(shí)現(xiàn)依據(jù)配置集成指定模塊
  • 系統(tǒng)能夠準(zhǔn)確識別差異化配置內(nèi)容。
  • 系統(tǒng)支持的功能與配置一致琐鲁。

設(shè)計(jì)方案

? 基于上述分析卫旱,以下是設(shè)計(jì)方案的大致流程:

  1. 配置文件構(gòu)建
  • ① 初步以 modules_configs.cmake 作為模塊配置文件。在 CMake 編譯期間識別配置選項(xiàng)围段,編譯指定模塊顾翼。
  • ② 增加配置版本號。在配置文件中增加版本號字段奈泪,并在編譯期間將該版本號傳遞至軟件中适贸,由軟件寫入實(shí)時(shí)環(huán)境。
  • ③ 增加配置文件版本管理涝桅。每次新增客制化產(chǎn)品時(shí)拜姿,都需要在工程中添加該產(chǎn)品唯一的客制化配置文件。
  1. 依據(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)庫的命名和入口函數(shù)實(shí)現(xiàn)。
    PluginEntryFunc 函數(shù)實(shí)現(xiàn)中狼速,完成該模塊的入口設(shè)計(jì)琅锻。
  • ④ 在主程序中調(diào)用各模塊入口:
    • 首先,主程序通過 dlopen 加載 libplug 前綴的客制化模塊動態(tài)庫;
    • 其次恼蓬,通過 dlsym 獲取動態(tài)庫的入口函數(shù) PluginEntry惊完;
    • 最后,通過函數(shù)指針調(diào)用動態(tài)庫的入口函數(shù)处硬。

詳細(xì)設(shè)計(jì)

主要是通過CMake配置化編譯和插件化編程實(shí)現(xiàn)動態(tài)加載小槐,詳細(xì)實(shí)現(xiàn)如下:

  1. 配置文件 modules_configs.cmake
# 業(yè)務(wù)模塊 Components/Business
set(MODULE_CONFIG_VERSION "DEFAULT_MCONFIG_1001")

set(BUSINESS_MODULES "")
list(APPEND BUSINESS_MODULES OneNetMqtt)
  • MODULE_CONFIG_VERSION 作為配置版本號變量:其值遵循 [產(chǎn)品]_MCONFIG_[版本號] 的命名規(guī)則,每次配置修改時(shí)荷辕,版本號應(yīng)遞增凿跳。
  • BUSINESS_MODULES 作為模塊編譯列表:用于存儲需要編譯的模塊名稱。
  1. 編譯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, 包含指定模塊的編譯路徑疮方,確保指定的模塊都能被正確編譯控嗜。
  1. 動態(tài)庫入口實(shí)現(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");
}
  • 實(shí)現(xiàn)動態(tài)庫入口函數(shù):PluginEntry 作為動態(tài)庫的入口函數(shù),其內(nèi)部主要負(fù)責(zé)調(diào)用當(dāng)前模塊的初始化函數(shù)骡显。
  • 初始化模塊實(shí)例:通過 OneNetDriver::GetInstanceOneNetManager::GetInstance 獲取模塊的單例實(shí)例疆栏。
  • 注冊模塊實(shí)例:將模塊實(shí)例注冊到 observers 映射中,以便主程序能夠訪問和使用這些模塊惫谤。
  1. 主程序加載指定動態(tài)庫
  • 插件化編程實(shí)現(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():
    調(diào)用 LoadPlugins() 加載動態(tài)庫。
    執(zhí)行獲取到的函數(shù) DEFAULT_PLUGIN_LIBRARY_ENTRY_FUNC险污。

驗(yàn)證

  • 從日志上看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與配置文件中一致

總結(jié)

  • 插件化編程通過動態(tài)加載功能模塊,實(shí)現(xiàn)了軟件的高度靈活性和可擴(kuò)展性蚁飒。其主要思路在于加載動態(tài)庫动壤,并調(diào)用動態(tài)庫中預(yù)定義的入口函數(shù),從而實(shí)現(xiàn)主程序與插件之間的解耦淮逻。
  • 除了實(shí)現(xiàn)產(chǎn)品的功能差異化外琼懊,插件化編程還可以應(yīng)用于性能優(yōu)化、安全性增強(qiáng)爬早、用戶體驗(yàn)提升等多個方面哼丈。例如,通過動態(tài)加載最新的安全補(bǔ)丁或功能更新筛严,無需重新啟動整個應(yīng)用程序醉旦。
  • 在項(xiàng)目中實(shí)現(xiàn)差異化的配置時(shí),建議采用單一配置文件或配置管理系統(tǒng)來集中管理所有配置項(xiàng),減少因配置錯誤導(dǎo)致的問題车胡。此外檬输,配置文件應(yīng)具備良好的可讀性和易維護(hù)性,避免復(fù)雜的多重開關(guān)設(shè)計(jì)匈棘,以免造成新開發(fā)人員的理解困難丧慈。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市羹饰,隨后出現(xiàn)的幾起案子伊滋,更是在濱河造成了極大的恐慌,老刑警劉巖队秩,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件笑旺,死亡現(xiàn)場離奇詭異,居然都是意外死亡馍资,警方通過查閱死者的電腦和手機(jī)筒主,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來鸟蟹,“玉大人乌妙,你說我怎么就攤上這事〗ㄔ浚” “怎么了藤韵?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長熊经。 經(jīng)常有香客問我泽艘,道長,這世上最難降的妖魔是什么镐依? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任匹涮,我火速辦了婚禮,結(jié)果婚禮上槐壳,老公的妹妹穿的比我還像新娘然低。我一直安慰自己,他們只是感情好务唐,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布雳攘。 她就那樣靜靜地躺著,像睡著了一般枫笛。 火紅的嫁衣襯著肌膚如雪来农。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天崇堰,我揣著相機(jī)與錄音沃于,去河邊找鬼涩咖。 笑死,一個胖子當(dāng)著我的面吹牛繁莹,可吹牛的內(nèi)容都是我干的檩互。 我是一名探鬼主播,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼咨演,長吁一口氣:“原來是場噩夢啊……” “哼闸昨!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起薄风,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤饵较,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后遭赂,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體循诉,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年撇他,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了茄猫。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡困肩,死狀恐怖划纽,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情锌畸,我是刑警寧澤勇劣,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站潭枣,受9級特大地震影響芭毙,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜卸耘,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望粘咖。 院中可真熱鬧,春花似錦翰铡、人聲如沸锭魔。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至捅位,卻和暖如春搂抒,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背焰雕。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工淀散, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蚜锨,地道東北人亚再。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像棍现,于是被迫代替她去往敵國和親己肮。 傳聞我的和親對象是個殘疾皇子谎僻,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內(nèi)容