首页 Qt Creator 源码学习 Qt Creator 源码学习 08:PluginSpec

Qt Creator 源码学习 08:PluginSpec

3 1.9K

前面我们已经了解到有关 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

#include "extensionsystem_global.h"

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

namespace ExtensionSystem {
    namespace Internal { ... } // Internal
    struct EXTENSIONSYSTEM_EXPORT PluginDependency { ... }
    struct EXTENSIONSYSTEM_EXPORT PluginArgumentDescription { ... }
    class EXTENSIONSYSTEM_EXPORT PluginSpec { ... }
} // namespace ExtensionSystem

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

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

namespace Internal {
    class OptionsParser;
    class PluginSpecPrivate;
    class PluginManagerPrivate;
} // Internal

class IPlugin;
class PluginView;

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

struct EXTENSIONSYSTEM_EXPORT PluginDependency
{
    enum Type {
        Required, // 必须有此依赖
        Optional, // 此依赖不是必须的
        // 在设计插件时需要注意,插件在无此依赖时应该能够正常加载
        // 比如,不能使用被依赖插件的 API 等
        Test
    };

    PluginDependency() : type(Required) {}

    QString name; // 被依赖插件名字
    QString version; // 被依赖插件版本号
    Type type; // 依赖类型
    bool operator==(const PluginDependency &other) const;
        QString toString() const;
    };

    uint qHash(const ExtensionSystem::PluginDependency &value);

一个插件可能建立在其它插件的基础之上。如果插件 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属性的散列值:

uint ExtensionSystem::qHash(const PluginDependency &value)
{
    return qHash(value.name);
}

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

struct EXTENSIONSYSTEM_EXPORT PluginArgumentDescription
{
    QString name;
    QString parameter;
    QString description;
};

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

class EXTENSIONSYSTEM_EXPORT PluginSpec
{
public:
    enum State { Invalid, Read, Resolved, Loaded, Initialized, Running, Stopped, Deleted};
    ~PluginSpec();

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

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

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

// 插件名字。当状态达到 PluginSpec::Read 时才可用。
QString name() const;
// 插件版本。当状态达到 PluginSpec::Read 时才可用。
QString version() const;
// 插件兼容版本。当状态达到 PluginSpec::Read 时才可用。
QString compatVersion() const;
// 插件提供者。当状态达到 PluginSpec::Read 时才可用。
QString vendor() const;
// 插件版权。当状态达到 PluginSpec::Read 时才可用。
QString copyright() const;
// 插件协议。当状态达到 PluginSpec::Read 时才可用。
QString license() const;
// 插件描述。当状态达到 PluginSpec::Read 时才可用。
QString description() const;
// 插件主页 URL。当状态达到 PluginSpec::Read 时才可用。
QString url() const;
// 插件类别,用于在界面分组显示插件信息。如果插件不属于任何类别,直接返回空字符串。
QString category() const;
// 插件兼容的平台版本的正则表达式。如果兼容所有平台,则返回空。
QRegExp platformSpecification() const;
// 对于宿主平台是否可用。该函数用使用 platformSpecification() 的返回值对平台名字进行匹配。
bool isAvailableForHostPlatform() const;
// 是否必须。
bool isRequired() const;
// 是否实验性质的插件。
bool isExperimental() const;
// 默认启用。实验性质的插件可能会被禁用。
bool isEnabledByDefault() const;
// 因配置信息启动。
bool isEnabledBySettings() const;
// 是否在启动时已经加载。
bool isEffectivelyEnabled() const;
// 因为用户取消或者因其依赖项被取消而导致该插件无法加载时,返回 true。
bool isEnabledIndirectly() const;
// 是否通过命令行参数 -load 加载。
bool isForceEnabled() const;
// 是否通过命令行参数 -noload 禁用。
bool isForceDisabled() const;
// 插件依赖列表。当状态达到 PluginSpec::Read 时才可用。
QVector dependencies() const;

typedef QVector PluginArgumentDescriptions;
// 插件处理的命令行参数描述符列表。
PluginArgumentDescriptions argumentDescriptions() const;

// 其它信息,当状态达到 PluginSpec::Read 时才可用。
// 该 PluginSpec 实例对应的插件 XML 描述文件所在目录的绝对位置。
QString location() const;
// 该 PluginSpec 实例对应的插件 XML 描述文件的绝对位置(包含文件名)。
QString filePath() const;

// 插件命令行参数。启动时设置。
QStringList arguments() const;
// 设置插件命令行参数为 arguments。
void setArguments(const QStringList &arguments);
// 将 argument 添加到插件的命令行参数。
void addArgument(const QString &argument);

// 当一个依赖需要插件名为 pluginName、版本为 version 时,返回该插件是否满足。
bool provides(const QString &pluginName, const QString &version) const;

// 插件的依赖。当状态达到 PluginSpec::Resolved 时才可用。
QHash<PluginDependency, PluginSpec *> dependencySpecs() const;
// 否则依赖 plugins 集合中的任一插件。
bool requiresAny(const QSet &plugins) const;

// PluginSpec 实例对应的 IPlugin 实例。当状态达到 PluginSpec::Loaded 时才可用。
IPlugin *plugin() const;

// 当前状态。
State state() const;
// 是否发生错误。
bool hasError() const;
// 错误信息。
QString errorString() const;

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

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

最后,私有变量部分:

private:
    PluginSpec();

    Internal::PluginSpecPrivate *d;
    friend class PluginView;
    friend class Internal::OptionsParser;
    friend class Internal::PluginManagerPrivate;
    friend class Internal::PluginSpecPrivate;
};

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

3 评论

fog 2017年3月29日 - 14:59

好久了,终于又有更新了,非常感谢分享

回复
walter 2020年9月25日 - 13:26

QT最新的插件描述文件是xxxx.JSON.IN, XML文件的描述文件会被淘汰么?

回复
豆子 2020年9月29日 - 21:34

这个也说不准,新兴框架更多使用 JSON 作为配置文件,XML 格式的确少了很多,但 JSON 也有自己的问题,比如 JSON 不支持注释,不支持复杂的结构,比如引用等。这么说来,JSON 也会被更新的技术所取代。事实上,spring boot 就已经在使用 yaml 格式的配置文件了。

回复

发表评论

关于我

devbean

devbean

豆子,生于山东,定居南京。毕业于山东大学软件工程专业。软件工程师,主要关注于 Qt、Angular 等界面技术。

主题 Salodad 由 PenciDesign 提供 | 静态文件存储由又拍云存储提供 | 苏ICP备13027999号-2