MCP项目笔记六(PluginsLoader)
扫描目录└─ 找动态库文件└─ dlopen / LoadLibrary ← 库加载进进程└─ dlsym / GetProcAddress ← 查找导出符号└─ CreatePlugin() ← 创建插件对象└─ Initialize() ← 插件自身初始化└─ 业务使用└─ Shutdown() ← 插件收尾└─ DestroyPlugin() ← 销毁对象└─ dlclose / FreeL
C++ 插件加载器:
从目录扫描、动态库加载、实例创建,到安全卸载的设计思路与实现细节。
一、整体架构概览
这段代码实现了一个完整的运行时插件系统(Runtime Plugin System)。所谓插件系统,就是让主程序在编译完成后,仍然能够在运行时动态扩展功能——把新的逻辑打包成动态库(.dll / .so / .dylib),放入指定目录,主程序便会自动发现并载入。
核心思想:在运行时加载外部模块,并把它们当作对象使用。主程序不依赖任何具体插件实现,二者通过抽象接口
PluginAPI通信。
整套系统的完整调用链如下:
扫描目录 → 找到动态库 → dlopen/LoadLibrary → 获取函数指针
→ CreatePlugin() → Initialize() → 业务使用
→ Shutdown + Destroy → dlclose/FreeLibrary
二、核心数据结构
跨平台句柄抽象
代码通过条件编译统一了不同平台的动态库句柄类型,用 LibraryHandle 这一名字将平台差异隐藏起来:
#ifdef _WIN32
#include <windows.h>
typedef HMODULE LibraryHandle; // Windows
#else
#include <dlfcn.h>
typedef void* LibraryHandle; // Linux / macOS
#endif
PluginEntry:插件档案袋
每一个已加载的插件,都用一个 PluginEntry 结构体来描述。它就像一张"插件身份证",记录了与该插件相关的所有运行时信息:
| 字段 | 类型 | 含义 |
|---|---|---|
path |
std::string |
插件动态库的文件路径 |
handle |
LibraryHandle |
动态库的操作系统句柄 |
instance |
PluginAPI* |
插件对象实例(指向抽象接口) |
createFunc |
函数指针 | 指向 CreatePlugin() 工厂函数 |
destroyFunc |
函数指针 | 指向 DestroyPlugin() 析构函数 |
值得注意的是,插件实例的类型是抽象基类指针 PluginAPI*,主程序对具体插件类一无所知,只通过接口与插件交互。这是解耦的关键所在。
三、目录扫描与过滤
LoadPlugins(directory) 是整个流程的入口。它的职责是"找到候选文件",具体加载逻辑委托给 LoadPlugin(path)——职责分离,各司其职。
递归遍历
代码使用了 C++17 引入的 std::filesystem::recursive_directory_iterator,这意味着插件可以放在目录的任意层级子文件夹里,加载器都能找到:
for (const auto& entry :
std::filesystem::recursive_directory_iterator(directory)) {
if (entry.is_regular_file()) { // 只处理普通文件
std::string ext = entry.path().extension().string();
// 按平台判断是否是动态库...
}
}
平台差异化过滤
不同操作系统的插件后缀名不同,代码通过预处理宏在编译期做了区分:
| 平台 | 动态库后缀 | 系统 API |
|---|---|---|
| Windows | .dll |
LoadLibraryA / GetProcAddress |
| Linux | .so |
dlopen / dlsym |
| macOS | .dylib 或 .so |
dlopen / dlsym |
四、插件生命周期详解
LoadPlugin(path) 是代码中最核心、逻辑最密集的函数。它将一个动态库文件转化为一个可用的插件对象,分为以下七个阶段:
① 创建局部 PluginEntry 记录对象
先创建一个局部档案,逐步填充字段。只有全程成功,才最终推入全局列表,防止中途失败污染状态。
② 加载动态库(dlopen / LoadLibraryA)
将 .so / .dll 文件映射到进程地址空间,获得操作系统句柄。失败时输出详细错误信息并立即返回。
// Linux / macOS
entry.handle = dlopen(path.c_str(), RTLD_LAZY);
if (!entry.handle) {
LOG(ERROR) << "Failed to load plugin: " << dlerror();
return false;
}
// Windows
entry.handle = LoadLibraryA(path.c_str());
if (!entry.handle) {
// FormatMessageA 获取可读错误信息...
return false;
}
③ 查找导出函数(dlsym / GetProcAddress)
在已加载的动态库中,按名称查找 CreatePlugin 和 DestroyPlugin 两个符号地址,并转换为对应类型的函数指针。
entry.createFunc = (PluginAPI*(*)()) dlsym(entry.handle, "CreatePlugin");
entry.destroyFunc = (void(*)(PluginAPI*))dlsym(entry.handle, "DestroyPlugin");
④ 验证插件规范性
检查两个函数指针是否均非空。若缺少任一导出函数,判定该动态库不是合法插件,立即卸载并返回失败。
if (!entry.createFunc || !entry.destroyFunc) {
LOG(ERROR) << "Plugin does not export required functions: " << path;
dlclose(entry.handle);
return false;
}
⑤ 调用工厂函数创建实例
entry.instance = entry.createFunc();
这是插件从"代码"变成"对象"的关键时刻,等价于在插件内部执行 new Plugin()。
⑥ 调用 Initialize() 初始化插件
插件在此完成内部准备工作:读取配置、申请资源、建立连接等。初始化失败则执行完整回滚:
if (!entry.instance->Initialize()) {
LOG(ERROR) << "Plugin initialization failed: " << path;
entry.destroyFunc(entry.instance); // 销毁实例
dlclose(entry.handle); // 卸载动态库
return false;
}
⑦ 注册至 m_plugins 列表
一切就绪后,将完整的 PluginEntry 推入全局插件向量。至此,该插件正式进入"已加载"状态,可供主程序调用。
m_plugins.push_back(entry);
LOG(INFO) << "Loaded plugin: " << entry.instance->GetName()
<< " v" << entry.instance->GetVersion();
为什么用 CreatePlugin / DestroyPlugin?
不直接 new / delete,而是用插件自己提供的工厂函数,是插件系统设计的惯用法:
ABI 兼容性:跨模块(主程序 ↔ 插件动态库)的
new/delete极易因编译器版本、标准库实现、调用约定不同而引发崩溃。由插件自己负责内存的分配与释放,可以彻底规避这类 ABI 问题。原则是:谁分配,谁释放。
五、安全卸载流程
析构函数与 RAII
析构函数体内只有一行:
PluginsLoader::~PluginsLoader() {
UnloadPlugins(); // 对象销毁时自动卸载所有插件
}
这正是 RAII(Resource Acquisition Is Initialization) 的体现——资源的生命周期与对象绑定。即使调用方忘记手动卸载,只要 PluginsLoader 对象离开作用域,所有插件都会被自动清理,不会发生内存或句柄泄漏。
单个插件的卸载顺序
卸载时的操作顺序有严格约束,不能颠倒:
void PluginsLoader::UnloadPlugin(PluginEntry& entry) {
if (entry.instance) {
entry.instance->Shutdown(); // ① 先让插件释放自身资源
entry.destroyFunc(entry.instance); // ② 通过工厂函数销毁对象
entry.instance = nullptr; // ③ 清空指针,避免悬空
}
if (entry.handle) {
dlclose(entry.handle); // ④ 最后才卸载动态库
entry.handle = nullptr;
}
}
⚠ 顺序不能颠倒:必须先执行
Shutdown()和destroyFunc(),最后才能dlclose()。一旦动态库被卸载,插件类的代码段可能从内存中消失,此时再调用对象方法会引发段错误(Segfault)。
总结
这套插件系统流程可以抽象为一个标准模板,之后遇到类似设计基本都是这个范式:
扫描目录
└─ 找动态库文件
└─ dlopen / LoadLibrary ← 库加载进进程
└─ dlsym / GetProcAddress ← 查找导出符号
└─ CreatePlugin() ← 创建插件对象
└─ Initialize() ← 插件自身初始化
└─ 业务使用
└─ Shutdown() ← 插件收尾
└─ DestroyPlugin() ← 销毁对象
└─ dlclose / FreeLibrary ← 卸载库
用一句话总结:在运行时把外部模块装进来,当作对象用,用完再按规范拆干净。
更多推荐

所有评论(0)