4步实现C++插件化编程,轻松实现功能定制与扩展(2)
[TOC]
引言
此文是对先前文章《4步实现C++插件化编程,轻松实现功能定制与扩展》 的延伸,重点记录在原版本基础上新增的插件热拔插功能。
起因源于读者的一个评论,如下:
看到这个问题时,当时的软件尚不具备“热拔插”功能。 但思考了一下,不支持“热拔插”的插件,应属于一种功能缺陷。于是乎,在原有的基础上增加了这一功能。这里,也很感谢这位读者提出这么好的问题。
插件化编程的实现方案和代码细节已经在上一篇文章中记录了,本篇主要记录下新增的热拔插功能的实现细节。
优化策略
第一版软件仅在启动时加载插件。在此基础上,新增以下功能:
- 在主程序运行过程中,若指定路径下新增插件库,程序将自动识别并加载。
- 若在主程序运行中从指定路径移除或删除插件库,程序将自动卸载对应的已加载插件。
要实现上述功能,需要对指定路径下的文件变动进行监控。在Linux
环境中,可以利用inotify
接口来达成这一目的。关于如何使用 inotify
实现实时文件监控的具体方法,可参考先前文章《使用inotify实现实时文件监控》。
详细设计
优化后的插件加载主要拆分为两个大类SprDirWatch
和 PluginManager
:
SprDirWatch 是一个工具类。专门用于封装 inotify
接口,以便于监控文件系统中的特定路径变化。
PluginManager 则是插件管理类。负责通过 SprDirWatch
捕获指定路径下文件的变化,并据此触发插件的自动“加载”或“卸载”操作。
- SprDirWatch类定义
class SprDirWatch
{
public:
SprDirWatch();
~SprDirWatch();
int GetInotifyFd() const { return mInotifyFd; }
int AddDirWatch(const std::string& path, uint32_t mask);
int RemoveDirWatch(int fd);
private:
int mInotifyFd;
std::set<int> mWatchFds;
};
SprDirWatch
的设计只是对 inotify
接口的一个简洁封装,其主要目的是为了更好地管理和控制 inotify
的监控资源。具体来说:
① 封装 inotify
的使用复杂性,提供了一个更友好、更易于使用的接口。
② 在SprDirWatch
的生命周期结束(即析构)时,自动释放句柄(尽管没必要移除监控句柄,好的编程习惯应该是有始有终)。
- PluginManager类定义
class PluginManager
{
public:
PluginManager();
~PluginManager();
void Init();
private:
void InitWatchDir();
void LoadPlugin(const std::string& path);
void UnloadPlugin(const std::string& path);
void LoadAllPlugins();
void UnloadAllPlugins();
std::string GetDefaultLibraryPath();
private:
SprContext mContext;
SprDirWatch mDirWatch;
std::string mDefaultLibPath;
std::shared_ptr<PFile> mFilePtr;
std::map<std::string, void*> mPluginHandles;
std::map<int, SprObserver*> mPluginModules;
};
PluginManager
的设计则是用于管理所有插件的“加载”和“卸载”。即通过SprDirWatch
监听指定路径“插件”的状态:
插件生成
① 当通过 SprDirWatch
监听到指定路径下有新的插件生成时,调用 LoadPlugin
方法加载新插件。
② LoadPlugin
使用 dlopen
加载插件库,并保存库地址句柄。
③ 调用插件库的入口函数,启动插件模块。
插件卸载
① 当通过 SprDirWatch
监听到指定路径下的插件被删除时,调用 UnloadPlugin
方法卸载该插件。
② UnloadPlugin
调用插件库的退出函数,停止插件模块。
③ 使用 dlclose
关闭插件库,释放资源。
- 监听动态库,插件“热插拔”实现
void PluginManager::InitWatchDir()
{
// Add a watch on the specified directory. The events to monitor include:
// - IN_CLOSE_WRITE: Triggered when a file is closed after being written.
// - IN_DELETE: Triggered when a file or directory is deleted.
// - IN_MOVED_TO: Triggered when a file or directory is moved to the specified directory.
// - IN_MOVED_FROM: Triggered when a file or directory is moved from the specified directory.
// Note: IN_CREATE is not used because it triggers immediately when a file is created,
// which may result in attempting to process the file before it is fully written and closed.
mDirWatch.AddDirWatch(mDefaultLibPath.c_str(), IN_CLOSE_WRITE | IN_MOVED_TO | IN_MOVED_FROM | IN_DELETE);
mFilePtr = std::make_shared<PFile>(mDirWatch.GetInotifyFd(), [&](int fd, void *arg) {
const int size = 100;
char buffer[size];
ssize_t numRead = read(fd, buffer, size);
if (numRead == -1) {
SPR_LOGE("read %d failed! (%s)\n", fd, strerror(errno));
return;
}
int offset = 0;
while (offset < numRead) {
struct inotify_event* pEvent = reinterpret_cast<struct inotify_event*>(&buffer[offset]);
if (!pEvent) {
SPR_LOGE("pEvent is nullptr!\n");
return;
}
if (pEvent->len > 0) {
if (pEvent->mask & IN_CLOSE_WRITE || pEvent->mask & IN_MOVED_TO) {
SPR_LOGD("File %s is created\n", pEvent->name);
LoadPlugin(pEvent->name);
}
if (pEvent->mask & IN_DELETE || pEvent->mask & IN_MOVED_FROM) {
SPR_LOGD("File %s is deleted\n", pEvent->name);
UnloadPlugin(pEvent->name);
}
}
offset += sizeof(struct inotify_event) + pEvent->len;
}
});
EpollEventHandler::GetInstance()->AddPoll(mFilePtr.get());
}
为了避免阻塞或轮询监听动态库路径,使用了 epoll
监听 inotify
的文件描述符,实现触发式监听。
验证
新增插件验证
① 移入插件库
$ mv libpluginonenet.so ../Lib/
② 日志打印确认
$ tail -f /tmp/sprlog/sprlog.log | egrep -i "PlugMgr|EntryOneNet"
10-30 21:08:13.277 19597 PlugMgr D: 84 File libpluginonenet.so is created
10-30 21:08:13.300 19597 EntryOneNet D: 58 Load plug-in OneNet modules
10-30 21:08:13.300 19597 PlugMgr D: 141 Load plugin libpluginonenet.so success!
③ 模块状态确认
Show All Message Queues
-----------------------------------------------------------------------------------------------
HANDLE QLSUM QMUSED QCUSED BLOCK MLLEN MMUSED MLAST MTOTAL NAME
-----------------------------------------------------------------------------------------------
4 10 6 0 BLOCK 1025 51 1 32 /SprMdrQ_20231126
5 10 1 0 BLOCK 1025 43 1 1 /TimerM_7lTva1nY
6 10 1 0 BLOCK 1025 43 1 1 /PowerM_E0pil3lu
7 10 1 0 BLOCK 1025 43 1 1 /OneDrv_BtJzE38A
8 10 1 0 BLOCK 1025 43 1 1 /OneMgr_yXTdsXPW
9 10 1 0 BLOCK 1025 51 1 1 /MQTT-OneJson01_y8M
10 10 1 0 BLOCK 1025 47 1 1 /MQTT-DEV01_z5TzmqV
11 10 1 0 BLOCK 1025 47 1 1 /PC_TEST_01_nOBnl0w
12 10 1 0 BLOCK 1025 47 1 1 /PC_TEST_02_VWQQbIw
-----------------------------------------------------------------------------------------------
Press 'Q' to back
通过日志和模块状态,可确认插件OneNet
加载成功,涉及到的模块运行正常。
移除插件验证
① 移除插件库
$ mv ../Lib/libpluginonenet.so .
② 日志打印确认
10-30 21:11:04.418 19597 PlugMgr D: 88 File libpluginonenet.so is deleted
10-30 21:11:04.418 19597 EntryOneNet D: 83 Unload plug-in OneNet modules
10-30 21:11:04.419 19597 PlugMgr D: 170 Unload plugin libpluginonenet.so success!
③ 模块状态确认
Show All Message Queues
-----------------------------------------------------------------------------------------------
HANDLE QLSUM QMUSED QCUSED BLOCK MLLEN MMUSED MLAST MTOTAL NAME
-----------------------------------------------------------------------------------------------
4 10 6 0 BLOCK 1025 51 3 26 /SprMdrQ_20231126
5 10 1 0 BLOCK 1025 43 1 1 /TimerM_7lTva1nY
6 10 1 0 BLOCK 1025 43 1 1 /PowerM_E0pil3lu
-----------------------------------------------------------------------------------------------
Press 'Q' to back
通过日志和模块状态,可确认插件OneNet
卸载成功,涉及到的模块已正常退出。
总结
- 本次优化实现了插件的“热插拔”功能,通过监控文件变动并相应调用加载或卸载函数来完成。
- 在此过程中,还发现动态链接库句柄泄露的问题,应确保
dlopen
返回的句柄得到妥善管理,在插件或程序退出时通过dlclose
进行回收。 - 优化过程中认识到,功能设计需细致入微,同时也应积极采纳并分析他人建议,以提高方案的可行性和实用性。此次就非常感激那位读者提出的问题。