Qt Plugin 的用法讨论

884

Qt Plugin 这功能很多个人开发者平时根本用不上,因为大家平时引用的第三方库大都是由 *.h 文件和 *.dll/*.so,*.a 等类型的文件组成的,由操作系统在运行程序时自动加载到内存,即动态链接库。而 Qt Plugin 的实际使用体验却稍有不同,它是需要由开发者手动加载的 dll,并做了一定约束,使插件可以动态的加载,并可进行热更新。

Qt的插件机制是为使用Qt的插件服务的 。它提供了一堆宏,可以帮助我们创建生成插件对象的C函数,并生成元信息(通过moc)以判断对象是否实现了接口。由于Qt的插件使用Qt,它也验证插件是否是用和编译应用程序本身的兼容的Qt编译的。

简述 Qt Plugin 的使用效果

工程布局的角度

我们需要数个工程才可以实现 Qt Plugin

  1. common 工程,作为一个 lib 被主工程和 plugin 工程同时引用,其中需要定义一个接口,例如 MyInterface
  2. main 工程,作为可执行程序 app 启动时的主工程,需要引用 common,使用 QPluginLoader 加载指定的插件,并将插件 dynamic_cast<MyInterface*> 将加载后的插件造型到接口,之后通过调用接口函数进行调用插件中的代码;
  3. plugin 工程:作为 lib 工程,编译出一个 dll 或者 so 文件,被主工程加载,并调用插件中的代码。

Qt Plugin 的适用场景

有时候,我们在做一个应用程序的时候,并不希望我们生成的软件仅仅是一个EXE文件,而是拆分成各个模块,越细越好。在未来更新发布中,都是非常的便利,我们更新的只是其中某一小部分,而不是整个应用程序。

比如:浏览模型的三维软件 meshlab 和 openGP 的开源软件 starlab 都是基于 QtPlugin 来实现的,甚至 Qt Creator 本身也用到了这样的特性。他们具有这样的特点:

  • 主工程负责界面和插件管理;
  • 一个 common 类别的工程,包含网格模型的基本数据结构,和插件的接口类型,包括:加载模型用的接口、处理模型用的接口、渲染模型用的接口等等;
  • 一堆插件,每个插件仅负责一个小小的功能;

这种特点具有显而易见的优势,可以让更多的开发者基于这个项目进行开发。我可以写一个小的算法,用插件的形式,避免了写 OpenGL 代码,只专注于我的算法代码。之后可以在 meshlab 上直接看到运行效果,调试起来也比较方便。我在魔改 starlab LydiaLab 时使用的也是这样的思路。

操作介绍

Not only Qt itself but also Qt application can be extended through plugins. This requires the application to detect and load plugins using QPluginLoader. In that context, plugins may provide arbitrary functionality and are not limited to database drivers, image formats, text codecs, styles, and the other types of plugin that extend Qt's functionality.

不仅 Qt,基于 Qt 的应用都可以通过插件进行拓展。需要使用功能 QPluginLoader 来进行插件检测和加载。

对于主工程来说,需要定一个插件的接口:

  1. 使用 Q_DECLARE_INTERFACE() 宏告诉 Qt meta-object 系统这个接口;
  2. 使用 QPluginLoader 来加载插件;
  3. 使用 qobject_cast() 进行插件造型,判断插件实现了哪个接口;

对于插件工程来说,需要实现一个接口:

  1. 定义一个继承 QObject 和 接口的插件;
  2. 使用 Q_INTERFACES() 声明使用的接口类型;
  3. 使用 the Q_PLUGIN_METADATA() 宏导出元信息;
  4. 使用合适的 *.pro 文件构建工程

接口定义

定义接口时,需要使用 Q_DECLARE_INTERFACE() 宏告诉 Qt meta-object 系统这个接口;

#ifndef MYINTERFACE_H
#define MYINTERFACE_H
#include <QWidget>
class MyInterface {
public:
    virtual QWidget *CreateWidget(QWidget *parent)=0;
    virtual int add(int a,int b)=0;
};
Q_DECLARE_INTERFACE(MyInterface,"sdafasdfsadfsadfsadfsadf");
#endif // MYINTERFACE_H

插件定义

插件的工程设置为生成 lib,实现一个基于接口的插件派生类。需要使用 Q_INTERFACES() 声明使用的接口类型,使用 the Q_PLUGIN_METADATA() 宏导出元信息。

#ifndef MYPLUGIN_H
#define MYPLUGIN_H
#include <QtPlugin>
#include "myinterface.h"
class myPlugin : public QObject,public MyInterface
{
    Q_OBJECT
#if QT_VERSION >= 0x050000
    Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QGenericPluginFactoryInterface" FILE "plugin.json")
    Q_INTERFACES(MyInterface);
#endif // QT_VERSION >= 0x050000
public:
    //myPlugin(QObject *parent = 0);
    QWidget *CreateWidget(QWidget *parent);
    int add(int a,int b);
};

主程序插件加载

主程序中需要通过 QPluginLoader 加载插件,并造型为接口调用插件的函数。

//加载plugin
bool MainWindow::LoadPlugin(QString pluginpath)
{
  QFile file(pluginpath);
  if (!file.exists())
  {
      QMessageBox::warning(this,tr("错误信息"),tr("找不到%1文件").arg(pluginpath));
      return false;
  }
  QPluginLoader loader(pluginpath);
  QObject *instance = loader.instance(); //
  if (instance!= NULL)
      {
           qDebug()<<pluginpath+" is loaded";
           MyInterface *avc = qobject_cast<MyInterface *>(instance);
           m_Form = avc->CreateWidget(this);
            m_Form->show();
           ui->lineEdit->setText(QString::number(avc->add(7,8)));
           return true;
      }
  else {
      QMessageBox::information(this,"failed to load plugin",loader.errorString());
  }
       // 需要手动释放
     delete instance;
}

常见问题

我在实际使用 QPlugin 这个功能的时候,经常碰到加载不了该插件的问题,一般情况下我都是按照下面的顺序进行排查的:

  1. 检查接口定义的是否正确,是否有 Q_DECLARE_INTERFACE宏;
  2. 检查插件实现的是否正确,是否继承 QObject,写入三个宏,Q_OBJECT Q_PLUGIN_METADATA Q_INTERFACES
  3. 检查插件是否实现了接口中的全部虚函数,如果有纯虚函数没有实现,则不能用;
  4. 检查插件中的对象是否存在定义了函数,但没有实现的情况;
  5. 如果还不行,那就再按照这个顺序检查一遍,基本上就可以了;

有对 Qt Plugin 感兴趣的,可以看看我魔改的 starlab LydiaLab,后续我会依照这个工程写几篇博客。

Reference

  1. MeshLab
  2. 魔改的 starlab LydiaLab
  3. How to Create Qt Plugins