Qt Creator 源码学习 08:PluginSpec

前面我们已经了解到有关 Qt 中常见的 D 指针的相关内容,下面就可以开始真正的代码学习了。

首先,我们从ExtensionSystem::PluginSpec这个类开始。之所以选择这个类,是因为这是一个最基础的类,它代表 Qt Creator 的“一个插件”。以 Windows 平台为例,Qt Creator 的插件是以 dll 的形式存在的。我们可以打开 %QT_PATH%\Tools\QtCreator\lib\qtcreator\plugins 找到这些插件的物理文件。每一个插件都是一个动态链接库;同时,Qt Creator 可以指定这个插件是否启用,也就是是否允许在 Qt Creator 启动时加载这个插件以及更多的操作。Qt Creator 将这个动态链接库对应的物理文件抽象为ExtensionSystem::PluginSpec类。这个类更多的是提供给整个插件系统关于这个插件的信息,包括插件的名字、当前状态等。其中,名字这样的静态数据是嵌入到插件文件中的,作为插件的“元数据”;当前状态则是标记这个插件当前是出于加载中,还是已经加载完毕之类,是保存在内存中的动态数据。同时,如果插件出现了任务错误,其详细的错误信息都是在这个类中。因此,我们说,ExtensionSystem::PluginSpec类表示的是与插件相关的信息,既包括静态信息,又包含动态状态。

在 Qt Creator 中,每一个插件会对应着一个 XML 文件,用于描述该插件的元信息、插件的依赖等。我们会在后面详细介绍这个 XML 文件。

ExtensionSystem::PluginSpec相关的有三个文件:pluginspec.h、pluginspec_p.h 和 pluginspec.cpp。通过前面一章的介绍我们已经可以从文件名上面知道,这个类使用了 D 指针模式。我们从 pluginspec.h 开始读起;期间,我们将结合对应的实现文件来了解到各个函数的实现。

我们已经见过#pragma once这样的写法,这里不再赘述。extensionsystem_global.h 与之前见到的 aggregation_global.h 类似,主要定义用于向外暴露的宏。然后我们注意到 pluginspec.h 类似如下的结构:

虽然源文件很长,但是简单抽取出来之后我们发现,这里其实定义了两个命名空间:ExtensionSystemExtensionSystem::Internal。顾名思义,前者是扩展系统的命名空间,而后者则是扩展系统内部实现的命名空间。所有在ExtensionSystem::Internal中的类,都不应该被我们自己的插件使用,因为那是内部实现类,不是暴露给外界的。Qt Creator 中很多内部使用类都是在类似Internal这样的命名空间中,这就是我们以后使用时需要注意的,同样也是我们的代码中可以学习的:将不希望被外界使用的类放在一个特定的命名空间中。

包括Internal在内的几行其实只是前置声明:

接下来定义了一个可以被外部使用的类PluginDependency

一个插件可能建立在其它插件的基础之上。如果插件 A 必须在插件 B 加载成功之后才能够加载,那么我们就说,插件 A 依赖于插件 B,插件 B 是插件 A 的被依赖插件。按照这一的加载模式,最终 Qt Creator 会得到一棵插件树。PluginDependency定义了有关被依赖插件的信息,包括被依赖插件的名字以及版本号等。我们使用PluginDependency定义所需要的依赖,Qt Creator 则根据我们的定义,利用 Qt 的反射机制,通过名字和版本号获取到插件对应的状态,从而获知被依赖插件是否加载之类的信息。值得注意的是,Qt Creator 在匹配版本号时,并不会直接按照这里给出的version值完全匹配,而是按照一定的算法,选择一段区间内兼容的版本。这样做的目的是,有些插件升级了版本号之后,另外的插件可以按照版本号兼容,不需要一同升级。

qHash()函数是一个全局函数,用于计算PluginDependency类的散列值。该函数的作用是允许PluginDependency类作为QHash这样的集合类的键。按照QHash文档要求,QHash的键必须在其类型所在命名空间中同时提供operator==()以及qHash()函数。有关具体细节,可以参考QHash的相关文档。在这里,这个函数的实现相当简单:只是计算了name属性的散列值:

下面的PluginArgumentDescription是一个简单的数据类,用于描述插件参数。Qt Creator 的插件可以在启动时提供额外的参数,类似main()函数参数的作用。

然后,我们可以看到最主要的PluginSpec类的定义:

首先,是一个State枚举,用于指示插件加载时的状态。当插件加载失败时,我们可以根据插件的状态来判断是哪个环节出了问题。这些状态的含义如下:

状态 含义
Invalid 起始点:任何信息都没有读取,甚至连插件元数据都没有读到
Read 成功读取插件元数据,并且该元数据是合法的;此时,插件的相关信息已经可用
Resolved 插件描述文件中给出的各个依赖已经被成功找到,这些依赖可以通过dependencySpecs()函数获取
Loaded 插件的库已经加载,插件实例成功创建;此时插件实例可以通过plugin()函数获取
Initialized 调用插件实例的IPlugin::initialize()函数,并且该函数返回成功
Running 插件的依赖成功初始化,并且调用了extensionsInitialized()函数;此时,加载过程完毕
Stopped 插件已经停止,插件的IPlugin::aboutToShutdown()函数被调用
Deleted 插件实例被删除销毁

头文件提供了很多函数。这些函数有的是访问函数,很多数据是从插件对应的 XML 文件中读取的。

现在,我们用了很长的篇幅,阐明了 pluginspec.h 中所有公共函数的主要作用。很多函数只需要看名字就能明白其作用,不过我们还是不厌其烦地写明。这是因为PluginSpec实在是一个非常重要的基础类。插件的主要信息都是通过这个类获取的,所以,我们有必要详细介绍这个类的对外接口。从静态意义上说,PluginSpec主要保存了插件的元数据,比如版本号、协议、类别等;从动态意义上说,PluginSpec记录了插件的加载状态。

函数列表的最后,hasError()errorString()是 Qt 中常见的函数。前者描述是否有错误发生;后者描述如果有错误,则具体的错误信息是什么。如果熟悉 Qt SQL 编程,SQL 模块很多错误都是用这种方法通知的。这样的错误处理避免了 C 风格的用int作为返回值,同时使用char **接收错误信息的机制。Qt 的处理模式显然更为简洁。

最后,私有变量部分:

注意,PluginSpec的构造函数是私有的。这意味着我们不能创建其实例。这个类显然不是单例,并且明显没有提供static的工厂函数,那么,我们如何创建其实例呢?答案就是,我们不能:PluginSpec的实例只能通过 Qt Creator 自身创建,而能够创建的类,就是这里定义的友元类。这里其实使用了 C++ 语言特性,即友元类可以访问到私有函数。我们将Internal::PluginManagerPrivate设置为PluginSpec的友元,就可以通过这个类调用PluginSpec私有的构造函数,从而创建其实例。这一技巧依赖于 C++ 语言特性,不能推广到其它语言,不过如果你使用的正是 C++,那么不妨尝试使用这一技巧,实现一种只能通过系统本身才能实例化的类。

上一篇
下一篇

One Response

  1. fog 2017年3月29日

Leave a Reply