4步实现C++插件化编程,轻松实现功能定制与扩展

4步实现C++插件化编程,轻松实现功能定制与扩展

[TOC]

引言

  在项目开发中,我们经常面临为适应不同市场或产品层级而需调整功能的需求。从软件工程的角度来看,这意味着使用同一套代码,通过配置来实现产品的功能差异化。实现这一目标的方法多种多样,本文将探讨如何通过 插件化编程 优雅地满足这一需求。

概述

插件化编程 是一种通过动态加载功能模块(即插件)来增强主程序功能的软件设计策略。通过制定标准化接口,确保插件与主程序之间的兼容性与独立性。此方法能显著提高软件的灵活性、可扩展性和易维护性,同时支持快速定制及对市场变化的迅速响应。

需求分析

  通过上述描述,可以将功能需求概括为:使用同一套代码基础,实现不同产品的功能差异化。

  从软件设计的角度来看,主要功能需求包括:

  1. 实现不同产品客制化配置
  • 通过配置文件来启用或禁用特定功能。通过配置文件灵活控制功能的开启与关闭,以满足不同市场或客户的具体需求。
  • 系统支持查阅配置版本信息。动态集成配置文件的版本信息,方便现场快速了解当前使用的配置状态。
  • 配置文件易于管控和维护。客制化配置应与具体产品绑定,避免不同产品的配置混淆,确保易于管理和维护;同时,配置文件应设计得易于编辑。
  1. 实现依据配置集成指定模块
  • 系统能够准确识别差异化配置内容。
  • 系统支持的功能与配置一致。

设计方案

  基于上述分析,以下是设计方案的大致流程:

  1. 配置文件构建
  • ① 初步以 modules_configs.cmake 作为模块配置文件。在 CMake 编译期间识别配置选项,编译指定模块。
  • ② 增加配置版本号。在配置文件中增加版本号字段,并在编译期间将该版本号传递至软件中,由软件写入实时环境。
  • ③ 增加配置文件版本管理。每次新增客制化产品时,都需要在工程中添加该产品唯一的客制化配置文件。
  1. 依据配置加载指定模块
  • ① 差异化模块以动态库形式呈现。
    根据 modules_configs.cmake 配置,在编译期间编译指定需加载的功能模块动态库。
  • ② 统一动态库命名前缀、入口函数命名和入口函数形式。
    • 动态库以 libplug 前缀命名;
    • 统一入口函数名为 PluginEntry
    • 函数形式为 void(*PluginEntryFunc)(std::map<int, SprObserver*>& modules, SprContext& ctx)
  • ③ 各模块按上述格式完成动态库的命名和入口函数实现。
    PluginEntryFunc 函数实现中,完成该模块的入口设计。
  • ④ 在主程序中调用各模块入口:
    • 首先,主程序通过 dlopen 加载 libplug 前缀的客制化模块动态库;
    • 其次,通过 dlsym 获取动态库的入口函数 PluginEntry
    • 最后,通过函数指针调用动态库的入口函数。

详细设计

主要是通过CMake配置化编译和插件化编程实现动态加载,详细实现如下:

  1. 配置文件 modules_configs.cmake
# 业务模块 Components/Business
set(MODULE_CONFIG_VERSION "DEFAULT_MCONFIG_1001")

set(BUSINESS_MODULES "")
list(APPEND BUSINESS_MODULES OneNetMqtt)
  • MODULE_CONFIG_VERSION 作为配置版本号变量:其值遵循 [产品]_MCONFIG_[版本号] 的命名规则,每次配置修改时,版本号应递增。
  • BUSINESS_MODULES 作为模块编译列表:用于存储需要编译的模块名称。
  1. 编译BUSINESS_MODULES指定模块
## Business

# 动态加载, 配置文件modules_configs.cmake
foreach(module IN LISTS BUSINESS_MODULES)
    message(STATUS "Add Business Module: ${module}")
    add_subdirectory(${module})
endforeach()
  • 通过循环遍历 BUSINESS_MODULES, 包含指定模块的编译路径,确保指定的模块都能被正确编译。
  1. 动态库入口实现
// 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");
}
  • 实现动态库入口函数:PluginEntry 作为动态库的入口函数,其内部主要负责调用当前模块的初始化函数。
  • 初始化模块实例:通过 OneNetDriver::GetInstanceOneNetManager::GetInstance 获取模块的单例实例。
  • 注册模块实例:将模块实例注册到 observers 映射中,以便主程序能够访问和使用这些模块。
  1. 主程序加载指定动态库
  • 插件化编程实现流程
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();
    }

    ...
}
  • 加载动态库 LoadPlugins():
    加载位于 DEFAULT_PLUGIN_LIBRARY_PATH 路径下,前缀为 DEFAULT_PLUGIN_LIBRARY_FILE_PREFIX 的动态库。
    获取并存储函数 DEFAULT_PLUGIN_LIBRARY_ENTRY_FUNC 的地址。
  • 主函数程序入口 Init():
    调用 LoadPlugins() 加载动态库。
    执行获取到的函数 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 

日志上看,动态库已经加载成功,动态库入口日志正常打印,OneNetMqtt模块启动正常。

  • 查阅系统加载的模块配置信息
$ 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

系统环境中模块配置版本号为DEFAULT_MCONFIG_1001与配置文件中一致

总结

  • 插件化编程通过动态加载功能模块,实现了软件的高度灵活性和可扩展性。其主要思路在于加载动态库,并调用动态库中预定义的入口函数,从而实现主程序与插件之间的解耦。
  • 除了实现产品的功能差异化外,插件化编程还可以应用于性能优化、安全性增强、用户体验提升等多个方面。例如,通过动态加载最新的安全补丁或功能更新,无需重新启动整个应用程序。
  • 在项目中实现差异化的配置时,建议采用单一配置文件或配置管理系统来集中管理所有配置项,减少因配置错误导致的问题。此外,配置文件应具备良好的可读性和易维护性,避免复杂的多重开关设计,以免造成新开发人员的理解困难。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容