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<test>()等),才会执行模板注册
    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<test>()等),才会执行模板注册
    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都可
  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: C 语言中常见的程序设计框架包括: 1. 面向过程编程(Procedural Programming):这种框架的主要思想是将程序分成若干个函数,每个函数执行特定的任务。 2. 面向对象编程(Object-Oriented Programming):这种框架的主要思想是将程序分成若干个对象,每个对象都有自己的数据和行为。 3. 函数式编程(Functional Programming):这种框架的主要思想是将程序表示为一系列函数调用。 4. 递归编程(Recursive Programming):这种框架的主要思想是使用递归来解决问题。 5. 迭代编程(Iterative Programming):这种框架的主要思想是使用循环来解决问题。 6. 分治编程(Divide-and-Conquer Programming):这种框架的主要思想是将问题分成若干个小问题,再分别解决。 ### 回答2: 设计程序的框架有很多种,下面列举几个常见的框架。 1. 面向对象编程框架:面向对象编程框架将程序设计的重点放在对象之间的交互上。常见的面向对象编程框架有Java的Spring框架和Python的Django框架等。 2. MVC框架:MVC(Model-View-Controller)框架将程序的设计划分为三部分:模型、视图和控制器。模型用于处理数据逻辑,视图用于展示数据,控制器负责处理用户的请求和调度。常见的MVC框架有Ruby on Rails和ASP.NET MVC等。 3. 响应式编程框架:响应式编程框架通过将数据流和事件流组合起来,使得程序能够对异步的数据流进行响应,从而实现更加灵活和高效的程序设计。常见的响应式编程框架有RxJava和ReactiveX等。 4. 函数式编程框架:函数式编程框架将程序设计的重点放在函数的组合和变换上,通过使用高阶函数和不可变数据结构等特性,实现更加模块化和可维护的程序。常见的函数式编程框架有Haskell的HappStack框架和JavaScript的React框架等。 总之,不同的设计程序框架适用于不同的开发需求和场景,选择合适的框架能够提高程序的开发效率和质量。 ### 回答3: 在设计程序时,常用的框架有以下几种: 1. MVC框架:MVC(Model-View-Controller)是一种软件设计模式,将应用程序分为模型(处理数据逻辑)、视图(显示用户界面)和控制器(处理用户输入)三个部分。MVC框架通过分离业务逻辑和用户界面,提高了程序的可维护性和重用性。 2. MVVM框架:MVVM(Model-View-ViewModel)是一种衍生自MVC的设计模式。它将应用程序分为模型(处理数据逻辑)、视图(显示用户界面)和ViewModel(提供视图与模型之间的数据绑定和交互)三个部分。MVVM框架通过数据绑定实现了视图和模型之间的低耦合,方便了界面的更新和维护。 3. 响应式编程框架:响应式编程框架(例如RxJava、ReactiveCocoa)将程序设计看作是事件流的处理。以事件驱动的方式,通过定义观察者(订阅者)和被观察者(发布者)之间的关系,实现对异步数据流的处理和变换。响应式编程框架可以简化异步编程的复杂性,提高代码的可读性和可维护性。 4. 模块化框架:模块化框架如OSGi、Node.js等,将程序组织为各个功能模块,每个模块独立开发和测试,并通过接口进行交互。模块化框架能够提高代码的可重用性和可扩展性,便于团队协作和代码维护。 5. 微服务框架:微服务框架(例如Spring Cloud、Netflix OSS)将应用程序拆分为一系列小规模的、独立运行的服务,每个服务负责特定的功能。微服务框架支持服务的自主开发、部署和扩展,可以提高系统的可伸缩性、容错性和可维护性。 总之,不同的程序设计框架适用于不同的应用场景,开发者需要根据具体需求和技术特点选择合适的框架设计程序。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值