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

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

[TOC]

引言

  此文是对先前文章《4步实现C++插件化编程,轻松实现功能定制与扩展》 的延伸,重点记录在原版本基础上新增的插件热拔插功能。

  起因源于读者的一个评论,如下:

  看到这个问题时,当时的软件尚不具备“热拔插”功能。 但思考了一下,不支持“热拔插”的插件,应属于一种功能缺陷。于是乎,在原有的基础上增加了这一功能。这里,也很感谢这位读者提出这么好的问题。

  插件化编程的实现方案和代码细节已经在上一篇文章中记录了,本篇主要记录下新增的热拔插功能的实现细节。

优化策略

  第一版软件仅在启动时加载插件。在此基础上,新增以下功能:

  • 在主程序运行过程中,若指定路径下新增插件库,程序将自动识别并加载。
  • 若在主程序运行中从指定路径移除或删除插件库,程序将自动卸载对应的已加载插件。

  要实现上述功能,需要对指定路径下的文件变动进行监控。在Linux环境中,可以利用inotify接口来达成这一目的。关于如何使用 inotify 实现实时文件监控的具体方法,可参考先前文章《使用inotify实现实时文件监控》

详细设计

  优化后的插件加载主要拆分为两个大类SprDirWatchPluginManager:
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进行回收。
  • 优化过程中认识到,功能设计需细致入微,同时也应积极采纳并分析他人建议,以提高方案的可行性和实用性。此次就非常感激那位读者提出的问题。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容