C++ plugin 框架设计 随笔

前言

最近参与的一个pipeline streamer类的项目开发,用到插件化的思想,简单做个随笔;

插件(Plug-in,又称addin、add-in、addon或add-on,又译外挂)是一种遵循一定规范的应用程序接口编写出来的程序。其只能运行在程序规定的系统平台下(可能同时支持多个平台),而不能脱离指定的平台单独运行。因为插件需要调用原纯净系统提供的函数库或者数据。很多软件都有插件,插件有无数种。

以上是插件的释义。

参照前文c++11 实现依赖注入,我们可以很容易对系统代码作出灵活的拓展,但是这种拓展始终在一个系统内,通俗来讲可能就在一个project中,实际开发过程中,有可能存在框架开发和具体业务开发隔离的形式,比如notepad++、notepad–、vscode中的插件系统,具体的实现类可能就是一个个插件,由他人开发来实现自己想要的功能,而这些人甚至不访问你的代码库,而这些插件作为单独的库存在,只需要放在指定路径就可动态加载进你的系统中。

这么做的优点就不说了,还是老三样,什么方便维护、方便拓展、降低耦合巴拉巴拉;

那这种我们应该怎么做,把这种插件化的思想融入到我们的代码中呢?

plugin关键点

我们要做主要包括以下两点

  1. 框架如何动态识别外部插件;
  2. 框架如何不直接依赖的情况下调用外部插件,插件动态插拔;

要实现以上要点,我们经过简单思考可以得出以下解决方案:

  1. 框架定义接口,插件实现接口
  2. 框架如何不直接依赖的情况下调用外部插件,试用dlopen动态加载符号表,并且通过框架定义的接口进行调用

实现

参照前文c++11 实现依赖注入sample继续完善,不过从简只做思路参考。

接口定义

插件接口只定义一个process,只做思路展示:

// 基类,可根据业务修改添加接口
class BaseObject
{
  public:
    virtual ~BaseObject(){};
    virtual void process(std::string &data) = 0;
};

plugin 加载

pluginmanger 管理加载运行路径下plugin文件夹里的插件库,根据名字加载动态库

class PluginManager
{
  public:
    static PluginManager &instance()
    {
        static PluginManager fac;
        return fac;
    }
    void init();
    void uninit();

  private:
    PluginManager()
    {
        init();
    }
    ~PluginManager()
    {
        uninit();
    }
    void loadAllPlugin();
    std::unordered_map<std::string, void *> plugins_;
};

std::vector<std::string> list_files(const std::string &directory_path)
{
    std::vector<std::string> result;
    DIR *directory = opendir(directory_path.c_str());
    unsigned char d_type = DT_REG;
    if (directory == nullptr)
    {
        std::cout << "Cannot open directory " << directory_path;
        return result;
    }

    struct dirent *entry;
    while ((entry = readdir(directory)) != nullptr)
    {
        // Skip "." and "..".
        if (strcmp(entry->d_name, ".") != 0 && strcmp(entry->d_name, "..") != 0)
        {
            if (entry->d_type == d_type)
            {
                result.emplace_back(entry->d_name);
            }
        }
    }
    closedir(directory);
    return result;
}

void PluginManager::init()
{
    loadAllPlugin();
}
void PluginManager::uninit()
{
    for (const auto &it : plugins_)
    {
        if (it.second)
        {
            dlclose(it.second);
        }
    }
}

void PluginManager::loadAllPlugin()
{
    auto files = list_files("./plugin");
    for (const auto &it : files)
    {
        if (it.find("libplugin") == 0 && !plugins_.count(it))
        {
            void *dl_handle = dlopen((std::string("./plugin/") + it).c_str(), RTLD_LAZY | RTLD_GLOBAL);
            if (!dl_handle)
            {
                std::cout << "Failed to load plugin " << it << ": " << dlerror() << "\n";
                return;
            }
            std::cout << "Plugin " << it << " loaded!\n";
            plugins_[it] = dl_handle;
        }
    }
}

我们现在开始根据接口实现几个plugin,处理传入的data字符串做处理

插件1,字符串转大写

class Test1 : public AutoRegister<Test1>
{
  public:
    Test1(){}; // 需要注意派生类需要提供自定义构造函数、或者在程序中有显示构造对象(如new test()
               // make_shared()等),才会执行模板注册
    void process(std::string &data);
    void user_code();
    constexpr static char *kPlguinName = "user1_define";
};

void Test1::process(std::string &data)
{
    std::cout << kPlguinName << " process " << endl;
    std::transform(data.begin(), data.end(), data.begin(), ::toupper);

    user_code();
}

void Test1::user_code()
{
    std::cout << "this user1 code done" << endl;
}

插件2 加后缀

class Test2 : public AutoRegister<Test2> {
public:
    Test2() {}; // 需要注意派生类需要提供自定义构造函数、或者在程序中有显示构造对象(如new test() make_shared()等),才会执行模板注册
    void process(std::string& data);
    void user_code();

    constexpr static char* kPlguinName = "user2_define";
};

void Test2::process(std::string &data)
{
    std::cout << kPlguinName << " process " << endl;
    data = data + "_suffix";
    user_code();
}

void Test2::user_code()
{
    std::cout << "this user2 code done" << endl;
}

主函数测试,这里的主函数相当于notepad++的窗口程序

int main(int, char **)
{
    PluginManager::instance();
    vector<string> input = {"user1_define", "user2_define"};
    std::cout << " has " << DIContainer::instance().m_map.size() << " pulgins" << std::endl;

    vector<shared_ptr<BaseObject>> handlechain;
    handlechain.reserve(input.size());
    for (const auto &key : input)
    {
        handlechain.emplace_back(move(DIContainer::instance().resolve(key)));
    }

    std::string data = "input_data";

    std::cout << "The unprocessed data :" << data << std::endl;
    for (const auto &node : handlechain)
    {
        node->process(data);
    }

    std::cout << "The processed data :" << data << std::endl;
}

输出:

Plugin libplugin2.so loaded!
Plugin libplugin1.so loaded!
 has 3 pulgins
The unprocessed data :input_data
user1_define process 
this user1 code done
user2_define process 
this user2 code done
The processed data :INPUT_DATA_suffix

sample附件

以上例程附件,cmake工程

近一步思考

如开头所述,以上sample只能说作为一个plugin 框架设计的一个思路展示,玩具demo性质的玩意转换成真正可用的产品还需要考虑挺多的,我们可以参照notepad+±-的源码。

  1. ABI 二进制兼容:像notepad++ 这种成熟产品其实对外的接口定义不是由上述sample的基类及虚函数实现,而是是C函数,然后获取出对应接口函数实现符号来调用,因为应用场景plugin与notepad主题应用完全分开编译,c++接口容易有二进制兼容问题,参照前文为什么不建议库导出c++接口,不过具体还需要看自己的应用场景,我这边实际使用还是用的c++。
  2. 插件间的数据流转:sample里面只写了对一个string data数据处理,所有的plugin处理后的数据都是string,但实际场景处理数据类型可能会变,每个插件处理后的数据结构可能不一样,这种的话可以定义数据抽象类,定义一些处理时间戳之类的基础函数,后续也通过实现新的数据类来拓展。
  3. 良好的接口设计,数据流控制,适当的数据拓展埋点(回调接口);实际业务中肯定不会像sample接口那么少,毕竟业务易变,但接口改起来就费劲了,预留好回调接口,方便拓展,不过这种东西还是要业务熟悉度高起来才能得心应手。
  4. plugin配置配套;这种插件系统一般都要搭配对应的配置来做插件的管理,比如在我处理开发stream应用,需要对每一条pipeline的每一个插件节点,各个插件节点的网状结构数据流转,数据吞吐量等等等等,配置文件的话json或者XML都可

你可能感兴趣的:(c++,notepad++,开发语言)