目录
1.引言
在设计大型软件时,插件式开发都会被考虑到。无论在普通的桌面软件还是大型的游戏软件,都可以看到插件的身影。例如著名的Qt Creator 系统开发软件都用插件架构。插件系统最大的功能是在一定程度内提高了软件的灵活度和可扩展性。一个设计精良插件系统甚至可以在宿主软件不退出的情况下加入新的插件,实现热插拔的功能。这一点在我的博客C++架构设计中也有涉及:
那么,Qt的插件系统是怎么实现的呢?Qt 提供了两种API用于创建插件:一种是高阶 API,用于扩展 Qt 本身的功能,如自定义数据库驱动,图像格式,文本编码,自定义样式等;一种是低阶 API,用于扩展 Qt 应用程序。本文主要是通过低阶 API 来创建 Qt 插件,并通过静态、动态两种方式来调用插件。
2.插件原理
在C++ 中,插件一般以动态库的显示加载方式提供。利用C++多态的原理,在程序中首先声明一个插件的interface。该interface 只需要实现构造和析构函数,所有用到的功能函数都先定义为虚函数,然后在插件中实现该interface 的具体接口。那么当插件创建的时候,把插件中的子类对象赋值到宿主程序中的基类对象interface。即可实现不同的插件实现不同的功能。
3.插件实现
3.1.定义一个接口集(只有纯虚函数的类)
interfaces.h
#ifndef INTERFACES_H
#define INTERFACES_H
#include <QtPlugin>
QT_BEGIN_NAMESPACE
class QImage;
class QPainter;
class QWidget;
class QPainterPath;
class QPoint;
class QRect;
class QString;
class QStringList;
QT_END_NAMESPACE
//! [0]
class BrushInterface
{
public:
virtual ~BrushInterface() {}
virtual QStringList brushes() const = 0;
virtual QRect mousePress(const QString &brush, QPainter &painter,
const QPoint &pos) = 0;
virtual QRect mouseMove(const QString &brush, QPainter &painter,
const QPoint &oldPos, const QPoint &newPos) = 0;
virtual QRect mouseRelease(const QString &brush, QPainter &painter,
const QPoint &pos) = 0;
};
//! [0]
//! [1]
class ShapeInterface
{
public:
virtual ~ShapeInterface() {}
virtual QStringList shapes() const = 0;
virtual QPainterPath generateShape(const QString &shape,
QWidget *parent) = 0;
};
//! [1]
//! [2]
class FilterInterface
{
public:
virtual ~FilterInterface() {}
virtual QStringList filters() const = 0;
virtual QImage filterImage(const QString &filter, const QImage &image,
QWidget *parent) = 0;
};
//! [2]
QT_BEGIN_NAMESPACE
//! [3] //! [4]
#define BrushInterface_iid "org.qt-project.Qt.Examples.PlugAndPaint.BrushInterface/1.0"
Q_DECLARE_INTERFACE(BrushInterface, BrushInterface_iid)
//! [3]
#define ShapeInterface_iid "org.qt-project.Qt.Examples.PlugAndPaint.ShapeInterface/1.0"
Q_DECLARE_INTERFACE(ShapeInterface, ShapeInterface_iid)
//! [5]
#define FilterInterface_iid "org.qt-project.Qt.Examples.PlugAndPaint.FilterInterface/1.0"
Q_DECLARE_INTERFACE(FilterInterface, FilterInterface_iid)
//! [4] //! [5]
QT_END_NAMESPACE
#endif
创建一个BrushInterface,ShapeInterface,FilterInterface基类,直接可以把它定义为纯虚接口。和普通的多态不同的是,在interfece 中我们需要定义 iid。iid 可以理解为插件的一个标识或者ID。在加载插件的过程中会对IID 进行判断,如果插件中的IID 和 interface 中的IID 不匹配,那么该插件就不会被加载。Q_DECLARE_INTERFACE把IID 和类名进行绑定,这也是必须的。作用是导出一些可以通过 定义了接口ID查找函数和几个QObject到接口的转换函数。
宏Q_DECLARE_INTERFACE导入名为PluginName的插件,它与Q_PLUGIN_METADATA()为插件声明元数据的类的名称相对应。
3.2.实现接口
basictoolsplugin.h
#ifndef BASICTOOLSPLUGIN_H
#define BASICTOOLSPLUGIN_H
//! [0]
#include <interfaces.h>
#include <QRect>
#include <QObject>
#include <QtPlugin>
#include <QStringList>
#include <QPainterPath>
#include <QImage>
//! [1]
class BasicToolsPlugin : public QObject,
public BrushInterface,
public ShapeInterface,
public FilterInterface
{
Q_OBJECT
//! [4]
Q_PLUGIN_METADATA(IID "org.qt-project.Qt.Examples.PlugAndPaint.BrushInterface" FILE "basictools.json")
//! [4]
Q_INTERFACES(BrushInterface ShapeInterface FilterInterface)
//! [0]
//! [2]
public:
//! [1]
// BrushInterface
QStringList brushes() const override;
QRect mousePress(const QString &brush, QPainter &painter,
const QPoint &pos) override;
QRect mouseMove(const QString &brush, QPainter &painter,
const QPoint &oldPos, const QPoint &newPos) override;
QRect mouseRelease(const QString &brush, QPainter &painter,
const QPoint &pos) override;
// ShapeInterface
QStringList shapes() const override;
QPainterPath generateShape(const QString &shape, QWidget *parent) override;
// FilterInterface
QStringList filters() const override;
QImage filterImage(const QString &filter, const QImage &image,
QWidget *parent) override;
//! [3]
};
//! [2] //! [3]
#endif
实现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)
# ifndef Q_COMPILER_VARIADIC_MACROS
# define QT_ANNOTATE_CLASS(type, x)
# else
# define QT_ANNOTATE_CLASS(type, ...)
# endif
#endif
发现根本没干什么,那么该宏应该在编译的时候由元对象系统Moc解析。在moc 解析的时候将 Q_PLUGIN_METADATA 转化为QT_MOC_EXPORT_PLUGIN 宏并插入到代码中。我们在编译后生成的" moc_basictoolsplugin.cpp" 文件中发现了此宏:
翻看Qt的源码(5.12.12版本)找到了该宏返回了插件的元对象数据以及 qt_plugin_instance 函数的实现,该函数的函数体Q_PLUGIN_INSTANCE 定义,函数中的 _instance = new IMPLEMENTATION; 也就是插件的对象,然后通过QObject 指针 _instance 的形式返回(代码如下)。上层通过该方式(见插件的加载过程),获取插件的子类对象赋值给基类,实现多态的调用。
下面是插件的具体实现代码:basictoolsplugin.cpp
#include "basictoolsplugin.h"
#include <QtMath>
#include <QtWidgets>
#include <stdlib.h>
//! [0]
QStringList BasicToolsPlugin::brushes() const
{
return {tr("Pencil"), tr("Air Brush"), tr("Random Letters")};
}
//! [0]
//! [1]
QRect BasicToolsPlugin::mousePress(const QString &brush, QPainter &painter,
const QPoint &pos)
{
return mouseMove(brush, painter, pos, pos);
}
//! [1]
//! [2]
QRect BasicToolsPlugin::mouseMove(const QString &brush, QPainter &painter,
const QPoint &oldPos, const QPoint &newPos)
{
painter.save();
int rad = painter.pen().width() / 2;
QRect boundingRect = QRect(oldPos, newPos).normalized()
.adjusted(-rad, -rad, +rad, +rad);
QColor color = painter.pen().color();
int thickness = painter.pen().width();
QColor transparentColor(color.red(), color.green(), color.blue(), 0);
//! [2] //! [3]
if (brush == tr("Pencil")) {
painter.drawLine(oldPos, newPos);
} else if (brush == tr("Air Brush")) {
int numSteps = 2 + (newPos - oldPos).manhattanLength() / 2;
painter.setBrush(QBrush(color, Qt::Dense6Pattern));
painter.setPen(Qt::NoPen);
for (int i = 0; i < numSteps; ++i) {
int x = oldPos.x() + i * (newPos.x() - oldPos.x()) / (numSteps - 1);
int y = oldPos.y() + i * (newPos.y() - oldPos.y()) / (numSteps - 1);
painter.drawEllipse(x - (thickness / 2), y - (thickness / 2),
thickness, thickness);
}
} else if (brush == tr("Random Letters")) {
QChar ch(QRandomGenerator::global()->bounded('A', 'Z' + 1));
QFont biggerFont = painter.font();
biggerFont.setBold(true);
biggerFont.setPointSize(biggerFont.pointSize() + thickness);
painter.setFont(biggerFont);
painter.drawText(newPos, QString(ch));
QFontMetrics metrics(painter.font());
boundingRect = metrics.boundingRect(ch);
boundingRect.translate(newPos);
boundingRect.adjust(-10, -10, +10, +10);
}
painter.restore();
return boundingRect;
}
//! [3]
//! [4]
QRect BasicToolsPlugin::mouseRelease(const QString & /* brush */,
QPainter & /* painter */,
const QPoint & /* pos */)
{
return QRect(0, 0, 0, 0);
}
//! [4]
//! [5]
QStringList BasicToolsPlugin::shapes() const
{
return {tr("Circle"), tr("Star"), tr("Text...")};
}
//! [5]
//! [6]
QPainterPath BasicToolsPlugin::generateShape(const QString &shape,
QWidget *parent)
{
QPainterPath path;
if (shape == tr("Circle")) {
path.addEllipse(0, 0, 50, 50);
} else if (shape == tr("Star")) {
path.moveTo(90, 50);
for (int i = 1; i < 5; ++i) {
path.lineTo(50 + 40 * std::cos(0.8 * i * M_PI),
50 + 40 * std::sin(0.8 * i * M_PI));
}
path.closeSubpath();
} else if (shape == tr("Text...")) {
QString text = QInputDialog::getText(parent, tr("Text Shape"),
tr("Enter text:"),
QLineEdit::Normal, tr("Qt"));
if (!text.isEmpty()) {
QFont timesFont("Times", 50);
timesFont.setStyleStrategy(QFont::ForceOutline);
path.addText(0, 0, timesFont, text);
}
}
return path;
}
//! [6]
//! [7]
QStringList BasicToolsPlugin::filters() const
{
return {tr("Invert Pixels"), tr("Swap RGB"), tr("Grayscale")};
}
//! [7]
//! [8]
QImage BasicToolsPlugin::filterImage(const QString &filter, const QImage &image,
QWidget * /* parent */)
{
QImage result = image.convertToFormat(QImage::Format_RGB32);
if (filter == tr("Invert Pixels")) {
result.invertPixels();
} else if (filter == tr("Swap RGB")) {
result = result.rgbSwapped();
} else if (filter == tr("Grayscale")) {
for (int y = 0; y < result.height(); ++y) {
for (int x = 0; x < result.width(); ++x) {
QRgb pixel = result.pixel(x, y);
int gray = qGray(pixel);
int alpha = qAlpha(pixel);
result.setPixel(x, y, qRgba(gray, gray, gray, alpha));
}
}
}
return result;
}
//! [8]
到此为止,一个插件的实现就已经完成了。下面来看一看插件加载的过程。
4.插件的加载
4.1.静态插件
4.1.1.静态插件实现方式
静态插件可以把下面这个宏插入到插件应用程序的源代码中:
Q_IMPORT_PLUGIN(qjpeg)
或在pro文件中配置项目为静态插件:
TEMPLATE = lib
CONFIG += plugin static
在构建应用程序时,静态插件也必须包含在链接器中。对于Qt预定义的插件,您可以使用QTPLUGIN来添加所需的插件到您的构建中。例如:
TEMPLATE = app
QTPLUGIN += qjpeg qgif # image formats
静态插件的加载比较简单,直接使用 QPluginLoader::staticInstances() 加载,代码如下:
void PluginDialog::findPlugins(const QString &path,
const QStringList &fileNames)
{
label->setText(tr("Plug & Paint found the following plugins\n"
"(looked in %1):")
.arg(QDir::toNativeSeparators(path)));
const QDir dir(path);
const auto staticInstances = QPluginLoader::staticInstances();
for (QObject *plugin : staticInstances)
populateTreeWidget(plugin, tr("%1 (Static Plugin)")
.arg(plugin->metaObject()->className()));
for (const QString &fileName : fileNames) {
QPluginLoader loader(dir.absoluteFilePath(fileName));
QObject *plugin = loader.instance();
if (plugin)
populateTreeWidget(plugin, fileName);
}
}
4.1.2.静态插件加载的过程
QPluginLoader::staticInstances()为什么就可以加载所有插件呢?翻看QPluginLoader的源码:
/*!
Returns a list of static plugin instances (root components) held
by the plugin loader.
\sa staticPlugins()
*/
QObjectList QPluginLoader::staticInstances()
{
QObjectList instances;
const StaticPluginList *plugins = staticPluginList();
if (plugins) {
const int numPlugins = plugins->size();
instances.reserve(numPlugins);
for (int i = 0; i < numPlugins; ++i)
instances += plugins->at(i).instance();
}
return instances;
}
/*!
Returns a list of QStaticPlugins held by the plugin
loader. The function is similar to \l staticInstances()
with the addition that a QStaticPlugin also contains
meta data information.
\sa staticInstances()
*/
QVector<QStaticPlugin> QPluginLoader::staticPlugins()
{
StaticPluginList *plugins = staticPluginList();
if (plugins)
return *plugins;
return QVector<QStaticPlugin>();
}
typedef QVector<QStaticPlugin> StaticPluginList;
Q_GLOBAL_STATIC(StaticPluginList, staticPluginList)
从以上代码可以看出:
1)首先定义了全局的向量集合StaticPluginList来存储所有的插件信息
2)然后,插件静态加载的过程中,需要要把自己的插件信息注册上来,这个也是最常见的设计手法,于是Qt库提供了注册的函数:
/*!
\relates QPluginLoader
\since 5.0
Registers the \a plugin specified with the plugin loader, and is used
by Q_IMPORT_PLUGIN().
*/
void Q_CORE_EXPORT qRegisterStaticPluginFunction(QStaticPlugin plugin)
{
staticPluginList()->append(plugin);
}
3)看到注册函数,估计你要恍然大悟了吧,为什么在4.1.1节中要使用Q_IMPORT_PLUGIN来声明静态插件,肯定是要调用qRegisterStaticPluginFunction注册自己:
#define Q_IMPORT_PLUGIN(PLUGIN) \
extern const QT_PREPEND_NAMESPACE(QStaticPlugin) qt_static_plugin_##PLUGIN(); \
class Static##PLUGIN##PluginInstance{ \
public: \
Static##PLUGIN##PluginInstance() { \
qRegisterStaticPluginFunction(qt_static_plugin_##PLUGIN()); \
} \
}; \
static Static##PLUGIN##PluginInstance static##PLUGIN##Instance;
果然,定义全局变量static##PLUGIN##Instance,在构造函数中调用了注册函数,注册的对象正是宏QT_MOC_EXPORT_PLUGIN定义的QStaticPlugin,下面看一下QT_MOC_EXPORT_PLUGIN
4)QT_MOC_EXPORT_PLUGIN定义
#define Q_PLUGIN_INSTANCE(IMPLEMENTATION) \
{ \
static QT_PREPEND_NAMESPACE(QPointer)<QT_PREPEND_NAMESPACE(QObject)> _instance; \
if (!_instance) { \
QT_PLUGIN_RESOURCE_INIT \
_instance = new IMPLEMENTATION; \
} \
return _instance; \
}
# define QT_MOC_EXPORT_PLUGIN(PLUGINCLASS, PLUGINCLASSNAME) \
static QT_PREPEND_NAMESPACE(QObject) *qt_plugin_instance_##PLUGINCLASSNAME() \
Q_PLUGIN_INSTANCE(PLUGINCLASS) \
static const char *qt_plugin_query_metadata_##PLUGINCLASSNAME() { return reinterpret_cast<const char *>(qt_pluginMetaData); } \
const QT_PREPEND_NAMESPACE(QStaticPlugin) qt_static_plugin_##PLUGINCLASSNAME() { \
QT_PREPEND_NAMESPACE(QStaticPlugin) plugin = { qt_plugin_instance_##PLUGINCLASSNAME, qt_plugin_query_metadata_##PLUGINCLASSNAME}; \
return plugin; \
}
定义了生成插件对象的全局函数和QtPluginMetaDataFunction,并返回了QStaticPlugin:
struct Q_CORE_EXPORT QStaticPlugin
{
//...
// Since qdoc gets confused by the use of function
// pointers, we add these dummes for it to parse instead:
QObject *instance();
const char *rawMetaData();
//...
};
4.1.3.示例
运行上面的实例,显示出全部的插件信息,如下:
4.2.动态插件
动态插件一般以动态库的方式来加载的。
4.2.1.动态插件的加载过程
在Qt 中加载这些插件的流程比较复杂的,我们自己写的时候就简单多了。流程大体相似,首先定义QPluginLoader 对象。QPluginLoader 的构造函数加载插件目录下所有插件,然后调用 instance 创建插件对象,最后就可以调用插件功能了。
QString loadPlugin(QString pluginPath)
{
QPluginLoader loader(pluginPath);
if(! loader.load()) {
qDebug()<<"load failed ";
}
QObject* plugin = loader.instance();
if(plugin) {
BrushInterface* interface = qobject_cast<BrushInterface*>(plugin);
if(interface) {
//...
}
}
else {
qDebug()<<"loader.instance failed!";
return QString();
}
}
QPluginLoader的load过程如下:
bool QPluginLoader::load()
{
if (!d || d->fileName.isEmpty())
return false;
if (did_load)
return d->pHnd && d->instance;
if (!d->isPlugin())
return false;
did_load = true;
return d->loadPlugin();
}
bool QLibraryPrivate::loadPlugin()
{
if (instance) {
libraryUnloadCount.ref();
return true;
}
if (pluginState == IsNotAPlugin)
return false;
if (load()) {
instance = (QtPluginInstanceFunction)resolve("qt_plugin_instance");
return instance;
}
if (qt_debug_component())
qWarning() << "QLibraryPrivate::loadPlugin failed on" << fileName << ":" << errorString;
pluginState = IsNotAPlugin;
return false;
}
上述代码的核心在:
1) load() 为加载动态库,window和linux实现方式不同,分别调用各自系统的API实现,windows一般调用LoadLibrary实现,linux一般调用dlopen实现,很具体的请自行查阅资料。
2)找到函数qt_plugin_instance的地址,并保存此地址,这个就是实例化插件对象的函数。
那么qt_plugin_instance是在那里定义的呢?自然会联想到QT_MOC_EXPORT_PLUGIN:
#define Q_PLUGIN_INSTANCE(IMPLEMENTATION) \
{ \
static QT_PREPEND_NAMESPACE(QPointer)<QT_PREPEND_NAMESPACE(QObject)> _instance; \
if (!_instance) { \
QT_PLUGIN_RESOURCE_INIT \
_instance = new IMPLEMENTATION; \
} \
return _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)
在QT_MOC_EXPORT_PLUGIN里面就有qt_plugin_instance函数。
5.定位插件
Qt 应用程序将会自动感知可用的插件,因为插件都被存储在标准的子目录当中。因此应用程序不需要任何查找或者加载插件的代码。
在开发过程中,插件的目录是 QTDIR/plugins(QTDIR 是 Qt 的安装目录),每个类型的插件放在相应类型的目录下面。如果想要应用程序使用插件,但不想用标准的插件存放路径,可以在应用程序的安装过程中指定要使用的插件的路径,可以使用 QSettings,保存插件路径,在应用程序运行时读取配置文件。应用程序可以通过QCoreApplication::addLibraryPath()
函数将指定的插件路径加载到应用程序中。
使插件可加载的一种方法是在应用程序所在目录创建一个子目录,用于存放插件。如果要发布和 Qt 一起发布的插件(存放在 plugins 目录)中的任何插件,必须拷贝 plugins 目录下的插件子目录到应用程序的根目录下。
6.插件开发的优势
- 方便功能扩展:通过插件,可以轻松地扩展应用程序的功能,而不需要修改应用程序本身。
- 更新量小:当底层接口不变时,只需要更新插件即可,而不需要重新发布整个应用程序。
- 降低模块间依赖:插件与主程序之间通过接口交互,降低了模块间的依赖关系,支持并行开发。
- 面向未来:通过插件,可以进一步演化API的功能,使API在长时间内保持可用性和适用性。
7.总结
Qt插件机制是一种强大且灵活的功能扩展方式,它允许开发者通过创建和加载插件来增强Qt应用程序的功能。通过遵循一定的规范,开发者可以轻松地创建和使用插件,从而满足各种复杂和多变的需求。