在项目开发中,我们经常面临为适应不同市场或产品层级而需调整功能的需求。从软件工程的角度来看,这意味着使用同一套代码,通过配置来实现产品的功能差异化。实现这一目标的方法多种多样,本文将探讨如何通过 插件化编程 优雅地满足这一需求。
注:文末提供本文源码获取方式。文章不定时更新,喜欢本公众号系列文章,可以星标公众号,避免遗漏干货文章。源码开源,如果对您有帮助,帮忙分享、点赞加收藏喔!
插件化编程 是一种通过动态加载功能模块(即插件)来增强主程序功能的软件设计策略。通过制定标准化接口,确保插件与主程序之间的兼容性与独立性。此方法能显著提高软件的灵活性、可扩展性和易维护性,同时支持快速定制及对市场变化的迅速响应。
通过上述描述,可以将功能需求概括为:使用同一套代码基础,实现不同产品的功能差异化。
从软件设计的角度来看,主要功能需求包括:
基于上述分析,以下是设计方案的大致流程:
modules_configs.cmake
作为模块配置文件。在 CMake
编译期间识别配置选项,编译指定模块。modules_configs.cmake
配置,在编译期间编译指定需加载的功能模块动态库。libplug
前缀命名;PluginEntry
;void(*PluginEntryFunc)(std::map<int, SprObserver*>& modules, SprContext& ctx)
。PluginEntryFunc
函数实现中,完成该模块的入口设计。dlopen
加载 libplug
前缀的客制化模块动态库;dlsym
获取动态库的入口函数 PluginEntry
;主要是通过CMake配置化编译和插件化编程实现动态加载,详细实现如下:
# 业务模块 Components/Business
set(MODULE_CONFIG_VERSION "DEFAULT_MCONFIG_1001")
set(BUSINESS_MODULES "")
list(APPEND BUSINESS_MODULES OneNetMqtt)
MODULE_CONFIG_VERSION
作为配置版本号变量:其值遵循 [产品]_MCONFIG_[版本号]
的命名规则,每次配置修改时,版本号应递增。BUSINESS_MODULES
作为模块编译列表:用于存储需要编译的模块名称。BUSINESS_MODULES
指定模块## Business
# 动态加载, 配置文件modules_configs.cmake
foreach(module IN LISTS BUSINESS_MODULES)
message(STATUS "Add Business Module: ${module}")
add_subdirectory(${module})
endforeach()
BUSINESS_MODULES
, 包含指定模块的编译路径,确保指定的模块都能被正确编译。// 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::GetInstance
和 OneNetManager::GetInstance
获取模块的单例实例。observers
映射中,以便主程序能够访问和使用这些模块。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
与配置文件中一致
用心感悟,认真记录,写好每一篇文章,分享每一框干货。