在基于Qt插件机制扩展应用程序示例我们展示了怎么使用Qt的插件的机制实现一个插件和加载插件,在本文中我们通过源代码来展示Qt为了实现插件机制做了哪些工作。
定义接口类AppInterface
接口类的原生的代码如下:
class AppInterface
{
public:
virtual ~AppInterface() {}
// 插件名字
virtual QString name() const = 0;
// 插件返回的QAction列表
virtual QList<QAction *> actions() const = 0;
};
Q_DECLARE_INTERFACE(AppInterface, "plugindemo_app_interface")
代码1
这里面有一个最重要的宏Q_DECLARE_INTERFACE,把这个宏展开是这样的:
template <> inline const char *qobject_interface_iid<AppInterface *>()
{
return "plugindemo_app_interface";
}
template <> inline AppInterface *qobject_cast<AppInterface *>(QObject *object)
{
return reinterpret_cast<AppInterface *>((object ? object->qt_metacast("plugindemo_app_interface") : Q_NULLPTR));
}
template <> inline AppInterface *qobject_cast<AppInterface *>(const QObject *object)
{
return reinterpret_cast<AppInterface *>((object ? const_cast<QObject *>(object)->qt_metacast("plugindemo_app_interface") : Q_NULLPTR));
}
代码2
不难看出,这个宏展开后有3个接口,一个是获取iid,另外两个是转换函数,把QObject*或const QObject*转换成AppInterface*。
定义插件类Plugin1
#include <QObject>
#include "../app/appinterface.h"
#include "plugin1_global.h"
class PLUGIN1SHARED_EXPORT Plugin1 : public QObject, public AppInterface
{
Q_OBJECT
Q_INTERFACES(AppInterface)
Q_PLUGIN_METADATA(IID "Plugin1")
public:
Plugin1();
// AppInterface interface
public:
virtual QString name() const override;
virtual QList<QAction *> actions() const override;
};
代码3
在这里面最只要的是Q_INTERFACES和Q_PLUGIN_METADATA这两个宏,这两个宏在源码中没有实际的意义,只是moc工具(在QTDIR/bin)在生成moc_plugin1.cpp(一般情况下在编译输出目录里面)的时候会生成插件相关的代码。如果没有这两个宏,就不会生成 if (!strcmp(_clname, "plugindemo_app_interface"))
return static_cast< AppInterface*>(this);和QT_MOC_EXPORT_PLUGIN(Plugin1, Plugin1)这两部分的代码。
moc_plugin1.cpp中部分代码如下:
void *Plugin1::qt_metacast(const char *_clname)
{
if (!_clname) return nullptr;
if (!strcmp(_clname, qt_meta_stringdata_Plugin1.stringdata0))
return static_cast<void*>(this);
if (!strcmp(_clname, "AppInterface"))
return static_cast< AppInterface*>(this);
if (!strcmp(_clname, "plugindemo_app_interface"))
return static_cast< AppInterface*>(this);
return QObject::qt_metacast(_clname);
}
int Plugin1::qt_metacall(QMetaObject::Call _c, int _id, void **_a)
{
_id = QObject::qt_metacall(_c, _id, _a);
return _id;
}
……
QT_MOC_EXPORT_PLUGIN(Plugin1, Plugin1)
代码4
代码2中object->qt_metacast会调用代码4中qt_metacast接口。
插件的加载QPluginLoader
QPluginLoader 类在运行时加载插件。
QPluginLoader提供对Qt插件的访问。Qt插件存储在共享库(DLL)中,与使用QLibrary访问的共享库相比,它具有以下优势:
- QPluginLoader 检查插件是否与应用程序相同版本的 Qt 链接。
- QPluginLoader 提供对根组件对象 (instance()) 的直接访问,而不是强制您手动解析 C 函数。
以上是Qt帮助文档中关于QPluginLoader的一部分内容。
现在通过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();
}
代码5
通过代码5不难看出 ,如果是第一次调用load会调用d->loadPlugin();(什么是d指针可以看Qt-D指针和Q指针及使用),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;
}
代码6
在loadPlugin中调用了QLibraryPrivate::load(),而QLibraryPrivate::load()中调用QLibraryPrivate::load_sys(),我们直接看load_sys(),注意load_sys()的实现在qlibrary_win.cpp或qlibrary_unix.cpp里面,在windows上部分代码如下:
bool QLibraryPrivate::load_sys()
{
……
for (const QString &attempt : qAsConst(attempts)) {
#ifndef Q_OS_WINRT
pHnd = LoadLibrary(reinterpret_cast<const wchar_t*>(QDir::toNativeSeparators(attempt).utf16()));
#else // Q_OS_WINRT
QString path = QDir::toNativeSeparators(QDir::current().relativeFilePath(attempt));
pHnd = LoadPackagedLibrary(reinterpret_cast<LPCWSTR>(path.utf16()), 0);
if (pHnd)
qualifiedFileName = attempt;
#endif // !Q_OS_WINRT
// If we have a handle or the last error is something other than "unable
// to find the module", then bail out
if (pHnd || ::GetLastError() != ERROR_MOD_NOT_FOUND)
break;
}
……
}
代码7
在unix下部分代码如下:
bool QLibraryPrivate::load_sys()
{
……
bool retry = true;
for(int prefix = 0; retry && !pHnd && prefix < prefixes.size(); prefix++) {
for(int suffix = 0; retry && !pHnd && suffix < suffixes.size(); suffix++) {
if (!prefixes.at(prefix).isEmpty() && name.startsWith(prefixes.at(prefix)))
continue;
if (path.isEmpty() && prefixes.at(prefix).contains(QLatin1Char('/')))
continue;
if (!suffixes.at(suffix).isEmpty() && name.endsWith(suffixes.at(suffix)))
continue;
if (loadHints & QLibrary::LoadArchiveMemberHint) {
attempt = name;
int lparen = attempt.indexOf(QLatin1Char('('));
if (lparen == -1)
lparen = attempt.count();
attempt = path + prefixes.at(prefix) + attempt.insert(lparen, suffixes.at(suffix));
} else {
attempt = path + prefixes.at(prefix) + name + suffixes.at(suffix);
}
pHnd = dlopen(QFile::encodeName(attempt), dlFlags);
if (!pHnd && fileName.startsWith(QLatin1Char('/')) && QFile::exists(attempt)) {
// We only want to continue if dlopen failed due to that the shared library did not exist.
// However, we are only able to apply this check for absolute filenames (since they are
// not influenced by the content of LD_LIBRARY_PATH, /etc/ld.so.cache, DT_RPATH etc...)
// This is all because dlerror is flawed and cannot tell us the reason why it failed.
retry = false;
}
}
}
……
}
代码8
通过代码7和代码8可以看到在不同平台上调用了不同的接口来打开插件,在windows上使用LoadLibrary或LoadPackagedLibrary,在unix上使用dlopen。
回到代码6,在load返回true的时候会给instance赋值,instance其实是一个函数指针:
typedef QObject *(*QtPluginInstanceFunction)();
QtPluginInstanceFunction instance;
代码9
resolve会检索导出函数或变量,在windows上调用GetProcAddress,在unix下调用dlsym。哪qt_plugin_instance这个函数是在什么时候定义的?这个其实是Qt帮我们已经实现好了。回到代码3,在Plugin1中有Q_PLUGIN_METADATA这个宏,moc工具会通过这个宏在moc_plugin1.cpp中生成QT_MOC_EXPORT_PLUGIN(Plugin1, Plugin1),qt_plugin_instance就是在这里面定义的。QT_MOC_EXPORT_PLUGIN在windows上展开之后的代码如下(在unix类似):
extern "C" __declspec(dllexport) const char *qt_plugin_query_metadata()
{
return reinterpret_cast<const char *>(qt_pluginMetaData);
}
extern "C" __declspec(dllexport) QObject *qt_plugin_instance()
{
static QPointer<QObject> _instance;
if (!_instance)
_instance = new Plugin1;
return _instance;
}
代码10
通过上面的代码可以看出QLibraryPrivate::instance保存的就是qt_plugin_instance函数的地址。这个函数功能就是创建Plugin1的一个实例。
我们再看看QPluginLoader::instance(),其代码如下:
QObject *QPluginLoader::instance()
{
if (!isLoaded() && !load())
return 0;
if (!d->inst && d->instance)
d->inst = d->instance();
return d->inst.data();
}
代码11
通过这个QPluginLoader::instance()我们就可以拿到Plugin1的实例了。最后我们就可以通过
qobject_cast<AppInteface *>(object);(调用代码2)转换成我们需要的接口类。
总结
通过上面的代码展示,插件本质上还是一个动态库,只是在运行时动态加载。在各个平台上使用不同的系统api来记载动态库。加载完成之后会获取qt_plugin_instance函数的地址。这个函数为我们创建了插件实例。可以通过QPluginLoader::instance()来获取。得到QObject*之后,通过qobject_cast转换成我们需要的接口类,从而调用在该类中定义的接口。