前言
最近参与的一个pipeline streamer类的项目开发,用到插件化的思想,简单做个随笔;
插件(Plug-in,又称addin、add-in、addon或add-on,又译外挂)是一种遵循一定规范的应用程序接口编写出来的程序。其只能运行在程序规定的系统平台下(可能同时支持多个平台),而不能脱离指定的平台单独运行。因为插件需要调用原纯净系统提供的函数库或者数据。很多软件都有插件,插件有无数种。
以上是插件的释义。
参照前文c++11 实现依赖注入,我们可以很容易对系统代码作出灵活的拓展,但是这种拓展始终在一个系统内,通俗来讲可能就在一个project中,实际开发过程中,有可能存在框架开发和具体业务开发隔离的形式,比如notepad++、notepad–、vscode中的插件系统,具体的实现类可能就是一个个插件,由他人开发来实现自己想要的功能,而这些人甚至不访问你的代码库,而这些插件作为单独的库存在,只需要放在指定路径就可动态加载进你的系统中。
这么做的优点就不说了,还是老三样,什么方便维护、方便拓展、降低耦合巴拉巴拉;
那这种我们应该怎么做,把这种插件化的思想融入到我们的代码中呢?
plugin关键点
我们要做主要包括以下两点
- 框架如何动态识别外部插件;
- 框架如何不直接依赖的情况下调用外部插件,插件动态插拔;
要实现以上要点,我们经过简单思考可以得出以下解决方案:
- 框架定义接口,插件实现接口
- 框架如何不直接依赖的情况下调用外部插件,试用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+±-的源码。
- ABI 二进制兼容:像notepad++ 这种成熟产品其实对外的接口定义不是由上述sample的基类及虚函数实现,而是是C函数,然后获取出对应接口函数实现符号来调用,因为应用场景plugin与notepad主题应用完全分开编译,c++接口容易有二进制兼容问题,参照前文为什么不建议库导出c++接口,不过具体还需要看自己的应用场景,我这边实际使用还是用的c++。
- 插件间的数据流转:sample里面只写了对一个string data数据处理,所有的plugin处理后的数据都是string,但实际场景处理数据类型可能会变,每个插件处理后的数据结构可能不一样,这种的话可以定义数据抽象类,定义一些处理时间戳之类的基础函数,后续也通过实现新的数据类来拓展。
- 良好的接口设计,数据流控制,适当的数据拓展埋点(回调接口);实际业务中肯定不会像sample接口那么少,毕竟业务易变,但接口改起来就费劲了,预留好回调接口,方便拓展,不过这种东西还是要业务熟悉度高起来才能得心应手。
- plugin配置配套;这种插件系统一般都要搭配对应的配置来做插件的管理,比如在我处理开发stream应用,需要对每一条pipeline的每一个插件节点,各个插件节点的网状结构数据流转,数据吞吐量等等等等,配置文件的话json或者XML都可