Qt 插件机制

概述


对于一个大型软件来说,插件是一个必不可少的东西,无论在普通的桌面软件还是大型的游戏软件,都可以看到插件的身影。插件系统最大的功能是在一定程度内提高了软件的灵活度和可扩展性。一个设计精良插件系统甚至可以在宿主软件不退出的情况下加入新的插件,实现热插拔的功能。那么,Qt的插件系统是怎么实现的呢?

1. 插件原理


在C++ 中,插件一般以动态库的显示加载方式提供。利用C++多态的原理,在程序中首先声明一个插件的interface。该interface 只需要实现构造和析构函数,所有用到的功能函数都先定义为虚函数,然后在插件中实现该interface 的具体接口。那么当插件创建的时候,把插件中的子类对象赋值到宿主程序中的基类对象interface。即可实现不同的插件实现不同的功能。

2. Qt中插件的实现


简单来说,插件也就是C++多态的实现,和普通的多态不同的是。怎么创建一个子类对象并赋值到宿主程序中,是插件的核心,也是是不同的插件系统实现的差异,只要搞懂了这一点也就掌握了插件。Qt 中利用元对象系统来实现这一点。

Qt的插件机制由 QPluginLoader 实现,该类提供了插件的加载、释放、实例化等等。下面通过一个自己实现interface 和插件的例子,来详细讲解Qt中插件的实现与加载的过程。

  • 创建interface

编写插件就像编写多态一样,首先创建一个interface基类,regexp 为我们的功能函数,直接可以把它定义为纯虚接口。和普通的多态不同的是,在interfece 中我们需要定义 iid。iid 可以理解为插件的一个标识或者ID。在加载插件的过程中会对IID 进行判断,如果插件中的IID 和 interface 中的IID 不匹配,那么该插件就不会被加载。Q_DECLARE_INTERFACE把IID 和类名进行绑定,这也是必须的。作用是导出一些可以通过 定义了接口ID查找函数和几个QObject到接口的转换函数。

#define RegExpInterface_iid "anobodykey.RegExpInterface/1.0"
class RegExpInterface
{
public:
   virtual ~RegExpInterface(){}
   virtual QString regexp(const QString &message)=0;
};
Q_DECLARE_INTERFACE(RegExpInterface, RegExpInterface_iid)
  • 实现interface

实现interface 也是和多态一样,只是多了两个宏 Q_PLUGIN_METADATA和Q_INTERFACES。前面讲到了,因为宿主程序是不知道插件的名字的,所以如何让插件的子类在宿主程序中实例化是插件系统的关键,在Qt 中 该功能由Q_PLUGIN_METADATA 完成。

跟踪源码发现 Q_PLUGIN_METADATA 这个宏啥也没有定义

#define Q_PLUGIN_METADATA(x) QT_ANNOTATE_CLASS(qt_plugin_metadata, x)


#  #define QT_ANNOTATE_CLASS(type, x)

那么该宏应该在编译的时候由元对象系统Moc解析。在moc 解析的时候将 Q_PLUGIN_METADATA 转化为QT_MOC_EXPORT_PLUGIN 宏并插入到代码中。该宏返回了插件的元对象数据以及 qt_plugin_instance 函数的实现,该函数的函数体由Q_PLUGIN_INSTANCE 定义,函数中的 _instance = new IMPLEMENTATION; 也就是插件的对象,然后通过QObject 指针 _instance 的形式返回(代码如下)。上层通过该方式(见插件的加载过程),获取插件的子类对象赋值给基类,实现多态的调用。

#  define QT_MOC_EXPORT_PLUGIN(PLUGINCLASS, PLUGINCLASSNAME)      \

            Q_EXTERN_C Q_DECL_EXPORT \

            const char *qt_plugin_query_metadata() \

            { return reinterpret_cast<const char *>(qt_pluginMetaData); } \

            Q_EXTERN_C Q_DECL_EXPORT QT_PREPEND_NAMESPACE(QObject) *qt_plugin_instance() \

            Q_PLUGIN_INSTANCE(PLUGINCLASS)

#endif

#define Q_PLUGIN_INSTANCE(IMPLEMENTATION) /

       { /

           static QT_PREPEND_NAMESPACE(QPointer)<QT_PREPEND_NAMESPACE(QObject)> _instance; /

           if (!_instance)      /

               _instance = new IMPLEMENTATION; /

           return _instance; /

       }

下面是插件的具体实现代码。

class PLUGINSHARED_EXPORT RegExpPlugin :public QObject,RegExpInterface
{
   Q_OBJECT
   Q_PLUGIN_METADATA(IID "anobodykey.RegExpInterface/1.0" FILE "regexpplugin.json")
   Q_INTERFACES(RegExpInterface)
public:
   RegExpPlugin();
   QString regexp(const QString &message);

};

到此为止,一个插件的实现就已经完成了。下面来看一看插件加载的过程。

3. Qt 中插件的加载过程


Qt 框架自身也实现了很多插件,例如平台插件、字体插件、风格插件等等,下面以平台插件为例子,分析下Qt自身插件的加载过程。 首先一般的界面程序都会在main函数中定义QGuiapplication。在其构造函数中对GUI 的运行环境进行初始化。在平台初始化函数init_platform中就实现了平台插件的加载。

图一

可以看到在图1 1213行首先通过QPlatformIntegrationFactory::keys(platformPluginPath); 该函数会创建 QFactoryLoader类,该类的创建表示插件加载过程正式开始,在该类的构造函数中第一步就是获取目标路径下所有的文件列表,然后通过系统提供的库的动态加载函数对每个文件进行加载。这时候就可以对动态库进行一个过滤,所以这里值得注意的是Qt 在加载插件的时候和插件的名称包括后缀名都没有关系。然后判断该库是否为一个插件,不是插件的就进行下一个库的加载。

library = QLibraryPrivate::findOrCreate(QFileInfo(fileName).canonicalFilePath());
if (!library->isPlugin()) {
    if (qt_debug_component()) {
       qDebug() << library->errorString << Qt::endl
                << "         not a plugin";
    }
    library->release();
    continue;
}

当一个插件加载成功之后,接下来就会读取插件的MateDta对插件的IID 进行验证,主要是为了判断插件的类型,区分是平台插件还是主题插件。

QString iid = library->metaData.value(QLatin1String("IID")).toString();
if (iid == QLatin1String(d->iid.constData(), d->iid.size())) {
    QJsonObject object = library->metaData.value(QLatin1String("MetaData")).toObject();
    metaDataOk = true;
    QJsonArray k = object.value(QLatin1String("Keys")).toArray();
    for (int i = 0; i < k.size(); ++i)
        keys += d->cs ? k.at(i).toString() : k.at(i).toString().toLower();
}

最后一步是对插件的Qt 版本进行验证,插件继承了很多虚接口,不同的Qt版本虚接口可能有改变。所有编译插件的Qt版本和编译宿主程序的Qt版本必须相匹配才可以。

QLibraryPrivate *previous = d->keyMap.value(key);
int prev_qt_version = 0;
if (previous) {
    prev_qt_version = (int)previous->metaData.value(QLatin1String("version")).toDouble();
}
int qt_version = (int)library->metaData.value(QLatin1String("version")).toDouble();
if ((prev_qt_version > QT_VERSION && qt_version <= QT_VERSION)) {
    d->keyMap[key] = library;
    ++keyUsageCount;
}

到这时候,插件还并没有真的的被创建,上面查找了该目录下的插件列表,并将这些插件load 到 QFactoryLoader 插件工厂类中。回到图一的代码继续运行,接下来就是一个for循环,参数是QStringList类型的plugins,该参数保存我们首先想去加载的平台后端,通过环境变量、运行参数等等来设置。

开始循环后,将我们指定的平台后端,通过QPlatformIntegrationFactory::create函数来创建,在该函数中调用 QFactoryLoader 的qLoadplugin 函数,首先判断我们想要设置的后端时候存在于刚才加载的插件列表中,如果存在就执行 loader->instance 函数,前面在讲实现插件的时候已经讲到了,该函数就是Qt 宏实现的返回插件自身对象的一个接口。创建插件后进行一个QObject类到插件基类的转换,插件也就加载完成。

template <class PluginInterface, class FactoryInterface, typename ...Args>
PluginInterface *qLoadPlugin(const QFactoryLoader *loader, const QString &key, Args &&...args)
{
   const int index = loader->indexOf(key);
   if (index != -1) {
       QObject *factoryObject = loader->instance(index);
       if (FactoryInterface *factory = qobject_cast<FactoryInterface *>(factoryObject))
           if (PluginInterface *result = factory->create(key, std::forward<Args>(args)...))
               return result;
   }
   return nullptr;
}

在Qt 中加载这些插件的流程比较复杂的,我们自己写的时候就简单多了。流程大体相似,首先定义QPluginLoader 对象。QPluginLoader 的构造函数加载插件目录下所有插件,然后调用 instance 创建插件对象,最后就可以调用插件功能了。

QString loadPlugin(QString pluginPath)
{
   QPluginLoader loader(pluginPath);
   if(! loader.load()) {
       qDebug()<<"load failed ";
   }
   QObject* plugin = loader.instance();
   if(plugin) {
       RegExpInterface* interface = qobject_cast<RegExpInterface*>(plugin);
       if(interface) {
           QString w = interface->regexp();
           return w;
       }
   }
   else {
       qDebug()<<"loader.instance failed!";
       return QString();
   }
}

加载插件大致流程图如下:

图二

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值