1. 概述
pluma 是一个用 c++ 开发的可用于管理插件的开源架构。该架构是个轻量级架构,非常易于理解。
pluma 架构有以下基本概念:
1)插件的外在行为体现为一个纯虚类,可以叫作插件接口;
2)继承于同一个插件接口的若干派生类,被认为属于同一种插件,可以叫作插件类;
3)每一个插件接口或插件类都有个一一对应的 provider 类,其中,插件接口对应的 provider 类里会定义一个特殊字符串常量:pluma_provider_type,表示这一类 “插件 provider” 共同的类型名称,而这个类型名称其实就是插件接口的类名字符串。
4)多个插件类可以被放入一个插件动态库中,而这个动态库文件名(不包括后缀部分)可以叫作 “插件名”。
5)插件机制使用者可以在自己的架构中包含一个 pluma 管理类,该类支持从所指定的位置加载一个或多个插件动态库,并将每个插件类对应的 provider,记录进内部的表中。
6)插件机制使用者可以在合适时机,利用 pluma 获取内含的插件 provider,并调用某个插件 provider 的 create () 函数,创建出对应的插件对象。
7)使用完插件对象后,不要忘了 delete 它。
现在我们画一张示意图:
2. pluma 管理类
我们刚刚也说了,插件机制使用者可以包含一个 pluma 管理类。
该类继承于 pluginmanager 类。
【pluma-1.1/include/pluma/pluginmanager.hpp】
从上面的 load () 函数和 loadfromfolder () 函数可以看出,插件管理器既允许用户单独加载某个插件动态库,也允许批量性加载某个目录下所有的插件动态库。另外,值得注意的是,getproviders () 函数是 protected 的成员,也就是说,这套架构是不希望用户直接使用这个 pluginmanager 类的,即便用了,你也拿不到 provider。正确的做法是,使用 pluginmanager 的子类:pluma 管理类。 另外,上面的成员变量 libraries,就是记录所有已加载的插件动态库的映射表。而成员变量 host 则负责记录每个插件类对应的 provider 信息。之所以被称为 host(宿主),是针对插件而言的。也就是说插件本身实际上是没资格知道其真实宿主的全貌的,它只能访问和它相关的很小一部分数据而已,因此 pluma 将这一小部分数据整理成一个 host 代理,供插件使用。 pluma 管理类的代码截选如下: 【pluma-1.1/include/pluma/pluma.hpp】
请大家注意上面代码中最后一行,这个 pluma.hpp 还真是有点手黑,偷偷摸摸 #include 了个 pluma.inl 文件,其实展开来就是 acceptprovidertype () 和 getproviders () 这两个模板函数的实现。pluma.inl 文件的内容如下: 【pluma-1.1/include/pluma/pluma.inl】
看到了吧,重新定义了个 getproviders (),还搞成一个模板函数,在函数体内会反过来通过模板参数,进一步得到所涉及的插件 provider 的 pluma_provider_type 信息,这个技巧挺重要。也就是说,外界传来的是 vector,而函数内部可以推断出 providertype::pluma_provider_type。将 pluma_provider_type 传入父类的 pluginmanager::getproviders () 函数,就可以拿到符合所指类型的所有 provider。 我们画一张 pluma 简图,后面再细说相关细节: 同一类插件类,会对应一个 providerinfo 节点,该节点内部的 providers 列表,记录着同属一类的若干 provider。
2.1 host 代理 【pluma-1.1/include/pluma/host.hpp】
正如前文所说,host 代理是针对插件而言的。而 host 只有一个 public 成员函数 add (),说明其主要对外行为就是让插件将对应的 provider 注册进 host。
3. 插件类和其对应的 provider 类
在说了一大堆插件管理类代码后,现在终于要开始说插件部分了。前文已经说过,插件的外在行为体现为一个纯虚类,可以叫作插件接口。我们现在就以 pluma 源码中给出的例子为准,来说明一些细节。
3.1 warrior 接口和 warriorprovider 类
pluma 中的插件接口例子是 warrior,其源码截选如下: 【pluma-1.1/example/src/interface/warrior.hpp】
这个接口里只象征性的写了一个成员函数 getdescription (),大家明白意思即可。 需要注意的是类定义之后的那句 pluma_provider_header,这个宏负责定义和插件接口对应的 provider 类。相关的宏定义如下: 【pluma-1.1/include/pluma/pluma.hpp】
基于这些宏定义,我们可以将 pluma_provider_header (warrior) 展开为:
代码很清晰,为 warrior 接口声明一个配套的 warriorprovider 类。这个类里包含着重要的 pluma_provider_type 常量,以及最关键的 create () 函数。 warrior 的实现文件更加简单: 【pluma-1.1/example/src/interface/warrior.cpp】
也在使用宏,展开宏后可见:
因为 warrior 本身是个纯虚类,所以 warriorprovider 里也不用实现 create () 函数。
3.2 warrior 派生类和派生 provider
在 pluma 源码的例子中,提供了三个 warrior 派生类,simplewarrior、eagle 和 jaguar。默认的是 simplewarrior,它被集成进 example/src/host 目录。也就是说,即便我们一个额外的插件库都不提供,示例至少还可以使用 simplewarrior。而 eagle 和 jaguar 则位于 example/src/plugin 目录,可以打包进一个插件动态库。 【pluma-1.1/example/src/host/simplewarrior.hpp】
前文我们已经看到,对于插件接口(warrior)来说,用到的宏是 pluma_provider_header(warrior),现在针对实际插件类(simplewarrior),会用到另一个宏 pluma_inherit_provider(simplewarrior, warrior)。这个宏的定义如下: 【pluma-1.1/include/pluma/pluma.hpp】
展开后可见:
很简单,就是在完成 provider 的核心使命,提供一个创建插件类对象的 create () 函数。与 simplewarriorprovider 类似,另外两个 warrior 派生类 eagle 和 jaguar 大体也是这么写的。示意图如下: 在研究 pluma 所给示例时,我已事先将 pluma 封装成静态库了,现在要把 eagle 和 jaguar 编译并封装成一个动态库,就需要链接 pluma 静态库,除此之外,还需要编译其他一些辅助文件,列举如下: 1)connector.cpp 2)dllmain.cpp 3)eagle.hpp 4)jaguar.hpp 5)warrior.cpp 其中 connector.cpp 文件,是插件动态库向外界 host 注册自己所有 provider 的地方。它必须实现一个 connect () 函数,代码截选如下:
我们先不要着急分析上面的 connect () 动作,可以先跟着我看看插件的加载流程,后文我们就会知道,connect () 只是加载流程的一环而已。
4. 插件加载流程
我们看一下 pluma 架构所给例子的 main () 函数,就可以了解插件的加载流程了:
其中和加载插件相关的句子主要就是 pluma.acceptprovidertype 和 pluma.load 两句了。前者主要负责在 host 的 knowntypes 映射表中添加一个 providerinfo 节点,后者负责加载插件动态库,并将动态库里匹配的 provider 指针记入 providerinfo 节点。
4.1 pluma.acceptprovidertype()
我们先说 pluma.acceptprovidertype 一句。在前文介绍 pluma.inl 文件的内容时,我们已经看到一个叫作 acceptprovidertype 的模板函数了,当时没有细说,现在我把它的代码再贴一下: 【pluma-1.1/include/pluma/pluma.inl】
里面调用的是 pluginmanager 基类的 registertype () 函数。 我们前文主要关心的是 pluma_provider_type,现在再说一下后两个参数。pluma_interface_version 表示管理器当前应该使用的插件接口的版本,因为我们不能确定更高版本的插件接口会不会增加或删除成员函数,所以这个值其实是个限定值,如果后续用户尝试加载更高版本的插件,那么是无法通过校验的。 第三个参数 pluma_interface_lowest_version 则是限定最低值,如果尝试加载比这个值更低版本的插件,肯定也是不会通过的。 在刚刚看到的 main () 函数里,是这样写的:
pluma.acceptprovidertype();也就是说,pluma 插件管理器对 warrior 接口对应的 warriorprovider 类感兴趣。而当初定义 warrior 时,在 warrior.cpp 文件里的确指明了 warriorprovider 能限定的当前版本号和最低版本号:
pluma_provider_source(warrior, 1, 1);这些类型信息、版本号限定信息都会被注册在 host 的 knowntypes 映射表中,每种接口类型对应一个 providerinfo 节点。注册动作的代码如下:
【pluma-1.1/src/pluma/pluginmanager.cpp】
【pluma-1.1/src/pluma/host.cpp】
当然,新加的 providerinfo 节点的 providers 列表是个空列表,待后续再添加 provider * 内容。
4.2 pluma.load()
接着,我们继续看 main () 函数里调用的 pluma.load (),其实调用的是其父类 pluginmanager 的 load ()。相关代码截选如下:
【pluma-1.1/src/pluma/pluginmanager.cpp】
可以看到,一开始就在着手加载动态库,并调用动态库里的 connect () 函数。前文我们实际上已经列举过示例代码里的 connect () 函数了,现在再贴一次:
前文在阐述到 connect () 时,暂时没有细说 add () 动作,现在我们来看看它的代码:
【pluma-1.1/src/pluma/host.cpp】
上面代码中那个 plumagettype () 函数其实是 provider 的私有成员,一般人访问不了,但 host 是它的友元类,所以可以访问。代码中会先校验待添加的 provider 是否合格,如果合格则以 plumagettype () 返回值为 key 值,并向临时映射表 addrequests 中添加该 provider 指针。所谓合格是指,这个 provider 的类型是 host 感兴趣的,并且其版本号也是合适的。
值得注意的是,待添加的 provider*,只是临时先放进一个 addrequests 映射表中。addrequests 映射表的定义如下:
【pluma-1.1/include/pluma/host.hpp】
那么这个临时性的 addrequests 映射表的内容会怎样处理呢?说起来也简单,会被 “搬移” 进 host 的 knowntypes 映射表中某个 providerinfo 的内部列表去。main () 在调用完 connect () 函数后,调用的 confirmaddictions () 就是做这个事情的:
【pluma-1.1/src/pluma/host.cpp】
我们画一张调用关系图看看:
我们可以通过这张调用关系图回顾一下,主要流程就是在加载插件动态库,并执行动态库里的 connect () 函数。该函数会将动态库里可用的所有 provider * 记入 host 的 knowntypes 映射表中。
同时,动态库对应的 dlibrary 对象,也会插入 pluma 管理类内部的 libraries 映射表中。
为了巩固知识,我们把前文的两张图再整合一下。
5. 使用插件 provider
5.1 pluma.getproviders()
在 providers 都添加进 pluma 管理类后,我们就可以在需要时获取 provider 了,为此 pluma 类提供了 getproviders () 函数:
【pluma-1.1/src/pluma/pluginmanager.cpp】
【pluma-1.1/src/pluma/host.cpp】
代码很简单,就是帮使用者把感兴趣的某类插件 provider 全部找出来。如果当初我们已经通过 acceptprovidertype () 注册了对应的类型(pluma_provider_type),那么至少可以拿到一个 list,否则就只能拿到 null 了。如果我们可以拿到若干 provider,就可以调用其 create () 函数创建对应的插件对象了。
当工作做完后,用户应该及时 delete 掉之前创建出的插件对象。在程序退出之前,用户应该调用 pluma.unloadall () 删除所有插件 provider 及 dlibrary 对象。dlibrary 对象析构时,会自动关闭已经打开的动态链接库。
【pluma-1.1/src/pluma/pluginmanager.cpp】
6. 结束
至此,pluma 架构的主体代码就分析完毕了,希望对大家有所帮助。
DC-DC电源注意事项
受新冠疫情影响 彩电市场又“雪上加霜”
华为p50是5g吗
PCB电路抑制干扰源的常用措施介绍
手机充电时,先插手机还是电源?
一个用C++ 开发的可用于管理插件的开源架构 Pluma
中汽中心正式发布C-ICAP《中国智能网联汽车技术规程》
核酸适配体检测原理 可生成具有所需结合亲和力的适配体
光纤收发器TR-962D932D指示灯说明
非洲猪瘟检测设备的各项参数是怎样的
中国中车承担研制的我国首列中国标准地铁列车在郑州下线
小米众筹上架九安智能血压计,主打三甲医生在线问诊
西井科技香港办公室正式开业!
浅谈指纹锁中的一个重要部件---传感器
数字电路基础知识之MOS管特性
新唐科技N584L040芯片介绍
华勤通讯领先其他ODM厂商是如何做到的?
应用于服务器的SCSI接口
多敏固态控制器光电输入的电路应用原理
LED术语解释一览