注意:本文翻译自 http://developer.qt.nokia.com 中的BasicsOfPlugins 与QtPlugin ,中文译文见 插件基础 与 插件,如果你对翻译wiki感兴趣,请参考Wiki中文帮助

注:QtInternal 系列是用来介绍Qt的各种特性是如何设计和实现的。

插件概述

插件是一种扩展现有程序的机制。作为一个例子,一个计算器程序可以通过加载额外的插件来扩展它所支持的操作的列表。插件允许第三方开发者在无需访问计算器程序源代码的情况下来扩展该程序。

创建插件

在C中,插件通过下列步骤被创建:

  • 应用程序为插件定义接口。这是一个应用程序期待插件来实现的函数的列表。
  • 插件实现了接口界面并且代码被编译为共享对象。
  • 应用程序发现插件,动态加载插件,解析插件中的符号/函数并调用接口中定义的方法。

让我们以一个可以使用插件扩展的计算器程序作为例子。

  • 计算器程序的接口定义为:

  • 额外的插件将接口实现为:

该插件使用 'cc -shared -fPIC addition.c -o addition.so' 编译成共享对象。

  • 应用程序在运行时通过搜索预配置的路径中的共享对象来发现插件。

导出 C 中的符号

一个共享对象可能会包含很多函数,但是它们中只有一部分想暴露给外部程序。编译器提供了标记函数可见性的机制。在gcc中,这是通过添加前缀 __attribute__((visibility("default"))) 实现的。而在MSVC中 __declspec(dllexport) 被添加到函数或类前面。

额外的插件应该这样写:

导出 C++ 中的符号

在C中,函数名到符号的映射是标准化的。但是,在C ++,不存在这样的标准,而且每个编译器会为相同的函数名生成不同的符号名。为了支持C++中很多名字相同但签名不同的函数的重载, 名字改编(name mangling) 是必须的。

因此,在C++中,

  • 比如上面的例子中如果插件编译使用C++编译器,使用dlsym的符号解析“操作”将会失败。程序员需要用改编以后的名称来解析。
  • 改编后名称的解析是编译器相关的。如果插件是使用不同的编译器编译的话,即使使用改编后的名称解析也将无法工作。

解决上述问题的技巧是在C++中定义这个接口,但用一个单独的C函数返回一个指向该接口的指针。这个C函数使用 C-linkage 编译(通过使用extern"C")。例如,上面的插件可以用C++实现如下:

 

 

QLibrary

QLibrary 使用各平台提供的标准API从DLL和共享对象中解析C符号。在Unix中使用 dlopen()/dlsym() ,在Windows中使用 GetProcAddress。

Qt 插件

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

考虑用于本文的下列基本的Qt插件的代码:

Q_DECLARE_INTERFACE 和 Q_INTERFACES 的作用在接下来的章节解释。

Q_INTERFACES

当 moc 运行于 hammer.h 代码时,它将检查 Q_INTERFACES。它为一个名为 qt_metacall -void *Hammer::qt_metacast(const char *iname) 的函数生成代码。这个 casting 函数的根据 iname 返回一个接口的指针。moc 也将确认你放于 Q_INTERFACES 的接口的名字是否确实被声明了。它通过检查头文件和查找Q_DECLARE_INTERFACE来实现这点。在我们这个例子中,toolinterface.h 文件内有一个Q_DECLARE_INTERFACE 。

粗糙的伪代码:

一个需要铭记的注意事项是,moc 不懂得接口的继承。举例来说,如果 ToolInterface 继承自 GenericInterface,它将不可能使用 qt_metacast 转换成 GenericInterface。moc 没有 C++ 语法的解析器,因此它不能在前面生成的 qt_metacast 代码中添加 GenericInterface。一种解决办法是在 hammer.h 中写为 Q_INTERFACES(ToolInterface:GenericInterface),“:” 指代派生。

Q_DECLARE_INTERFACE

Q_DECLARE_INTERFACE 是一个定义了使 qobject_cast<Tool *>(hammer)返回工具指针的帮助函数的一个宏。qobject_cast 只是一个模板函数,你可以认为 Q_DECLARE_INTERFACE 为接口提供了一个模板的实例化。这个宏自身只是展开成一个对前面moc生成的qt_metacast函数的一个调用。因此 Q_DECLARE_INTEFACE 定义了一个调用 object->qt_metacall("in.forwardbias.tool/1.0") 的模板函数的实例化 qobject_cast<Tool *>(object)

Q_EXPORT_PLUGIN2

这是一个在共享对象中被导出的 C 函数。它看起来像这样:

注意:qt_plugin_instance 实际中使用了单例,为了易于理解上面进行了简化。

Q_IMPORT_PLUGIN2 定义了创建一个该插件实例的C函数并包含其他额外的函数来返回插件编译时使用的Qt的配置信息。

QPluginLoader

QPluginLoader 使用作为 Q_EXPORT_PLUGIN2 的一部分被嵌入的校验数据(见上面的例子)来确认一个插件是否和应用程序兼容。在UNIX下一个有意思方面是,Qt将 mmap该库然后进行字符串的搜索(从文件的尾部,也即是反向搜索)而不是加载库然后解析函数。这样做的原因似乎是,它显然更快,而且可以避免加载不兼容的插件。

Qt 标准插件

Qt 的多个不同部分可以使用插件扩展 - 编解码、样式、字体引擎等。对于给出的一个插件,必须将其转换(cast)到所有被支持的接口来确定这个插件究竟实现了什么。为了避免这种开销,Qt的定义了插件应当被放置的标准路径。举例来说,样式插件必须在 plugins/styles

静态插件

当Qt构建在静态模式下时,插件也必须是静态的。为什么呢?由于Qt是静态的,它根本无法加载动态链接到Qt的插件。唯一的办法是为所有的插件生成静态库并在目标程序中链接所有的静态库。

当Qt是在静态模式下构建时,Q_EXPORT_PLUGIN2 宏扩展成一个C函数 qt_plugin_instance_##pluginName()。 pluginName有助于避免使用多个插件时的名称冲突 —— 记住这是静态链接时的代码。

Q_EXPORT_PLUGIN2 生成注册静态插件的代码。另外两点需要注意:

  1. 必须有人"注册(register)"这个插件。当静态构建Qt时,开发者决定哪些插件随程序发布。有人需要将这些被选择的插件需要到Qt系统。这是通过使用Q_IMPORT_PLUGIN(pluginName)实现的。它所做的是创建一个全局静态对象,其构造函数使用 qRegisterStaticPluginInstanceFunction 来注册插件的qt_plugin_instance_##pluginName。这样以来,Qt现在能知道这个插件的存在并且知道如何创建插件,但它不知道插件实现了什么! Qt只能通过遍历每个插件对象并尝试转换到每一个标准接口。
  2. 该插件本身是静态库。当我们的应用程序被链接时,我们需要链接这些静态库。这是通过. pro文件的 QTPLUGIN+=pluginName(这只是简单添加了 -l<plugin>.a 的链接选项) 实现的。

FAQ

  1. 多个插件可以共存在一个单一的 DLL/.so中么? 你可以将多个 *相同* 类型的插件放在一个DLL中但不能将不同类型的的插件放于一个单一的 .so 中。比如:你可以将两个图片插件放于同一个.so但是你不能将一个图片插件和一个字体引擎插件放于同一个.so文件。
  2. 动态 Qt 可以加载静态插件么?不可以。
  3. 静态 Qt 可以加载动态插件么?不可以。