本文是 《基于 Qt 的组件合成框架》的其中一节,建议全章阅读。
Qt 插件在形式上是一个多态链接模块,在运行时,按需加载。同一个类型(或者一组)插件,存放在 plugins 目录的一个子目录中。在程序要使用某种插件功能时,需要搜索相关子目录,读取插件的描述信息,并据此找到该功能的一个合适的具体实现插件(模块)。然后加载这个模块,使用它里面的组件功能。
在组件合成框架中,我们将自定义一种插件类型,并且将组件的插件化模块存放在自己的子目录(plugins/components)中。我们的插件描述其实就是一系列类型以及导出、导入描述。使用时,根据组件合成的规则(目标是使用某个具体的类型),去判断需要加载哪个插件。
实现 Qt 插件
实现 Qt 插件,首先需要为自定义的插件种类声明一个接口类,定义一个插件类型 ID。
class QComponentFactoryInterface
{
public:
};
#define ComponentFactory_iid \
"org.qt-project.Qt.ComponentFactoryInterface/1.0"
Q_DECLARE_INTERFACE(QComponentFactoryInterface, ComponentFactory_iid)
宏 Q_DECLARE_INTERFACE 用于实现运行时类型转换 qobject_cast,转换是通过匹配插件类型 ID 实现的。
具体的插件模块需要实现一个上面接口类的派生类,并通过 Q_PLUGIN_METADATA、Q_INTERFACES 关联插件类型ID、插件描述以及接口类。使用 Q_INTERFACES 一次性可以关联多个接口类。
class MultiMediaPlugin : public QGenericPlugin
, public QComponentFactoryInterface
{
Q_OBJECT
Q_PLUGIN_METADATA(IID ComponentFactory_iid FILE "MultiMedia.json")
Q_INTERFACES(QComponentFactoryInterface)
public:
explicit MultiMediaPlugin(QObject *parent = nullptr);
private:
QObject *create(const QString &name, const QString &spec) override;
};
Moc 编译器根据上述信息生成代码,代码将 json 描述文件的内容以及其他信息导出到动态链接库的 TEXT SECTION 中。
同时,Moc 生成的代码也会导出两个动态链接符号(函数) qt_plugin_query_metadata 和 qt_plugin_instance。其中 qt_plugin_query_metadata 用于返回插件信息,qt_plugin_instance 用于创建具体的插件实例。
插件的 create 方法负责创建实现了具体功能的组件,怎么解释参数 name 和 spec 是由自定义插件自行规定。但是在组件合成框架中,插件的 create 方法没有什么作用,因为组件合成框架通过 Qt 的元数据信息来创建组件实例的。
加载插件描述信息
我们并不希望提前将动态链接库加载到内存中,但是又要提前收集插件的描述信息(主要是组件导入导出的描述信息),才能构建所有导出、导入的关联性。
Qt 的插件实现已经可以满足这样的需求,它将插件文件打开后,读出插件信息,然后又关闭的文件。一种实现方案是加载插件模块(在 Win32 中是调用 LoadLibrary),然后调用 qt_plugin_query_metadata 函数,来获取插件信息。还有一种方法是直接分析动态链接库的 TEXT SECTION,解析其中的数据,来构造插件信息。显然第二种方法更高效,因为 LoadLibrary 是需要完成所有符号动态链接过程的。另外 LoadLibrary 再 FreeLibrary 还是有副作用的,比如会执行插件模块的内部全局初始化。
使用 Qt 获取插件描述的代码是这样的:
void QComponentRegistry::importPlugin(const QString &file)
{
Loader * l = new Loader;
l->setFileName(file);
QJsonObject meta = l->metaData();
if (meta.value("IID").toString() != ComponentFactory_iid) {
delete l;
return;
}
meta = meta.value("MetaData").toObject();
}
最后,meta 中包括我们自定义的组件导出、导入描述信息结构。
这里的导出、导入描述信息,与实际的通过全局变量定义的导出、导入声明,还是有区别的。除了信息不完整之外,通过插件描述构建的组件信息没有引用 QMetaObject 元数据对象,也不可能引用 QMetaObject 对象,因为插件还没有加载进来呢。
我们通过虚拟的 QMetaObject 对象来作为临时替代,构建虚拟 QMetaObject 的方法是(这里忽略了填充 int_data、string_data 的细节,有兴趣可以查看源代码):
void MetaObjectBuilder::buildMetaData(QMetaObject * meta)
{
......
// put the metaobject together
meta->d.data = int_data;
meta->d.extradata = nullptr;
meta->d.stringdata = reinterpret_cast<const QByteArrayData *>(string_data);
meta->d.static_metacall = nullptr;
meta->d.relatedMetaObjects = nullptr;
meta->d.superdata = &QObject::staticMetaObject;
}
当加载插件后,插件的全局初始化过程会注册真实的导出、导入声明,我们再替换为真实的 QMetaObject 对象。
加载、卸载插件模块
当真正需要插件某个具体组件时,才需要加载插件模块。获取 QPluginLoader 的 instance 时,就会加载插件。
bool QComponentRegistry::loadPlugin(QComponentRegistry::Loader *loader)
{
if (!loader->isLoaded()) {
if (!loader->instance()) {
return false;
}
......
}
return true;
}
之所以使用 instance() 方法,而不是 load() 方法,是因为两个原因:第一个是能够做更多的检查,防止加载了错误的插件;另一个原因是可以让插件开发者在其自定义 Plugin 类中做一些初始化工作。
当我们不再使用一个插件它的任何组件时,可以将插件卸载,以节省内存空间。
bool QComponentRegistry::unloadPlugin(QComponentRegistry::Loader *loader)
{
if (!loader->unload())
return false;
......
return true;
}