电脑基础 · 2023年3月12日

QtCreator源码分析(二)——QtCreator插件架构

一、插件架构简介

插件架构即微核架构,把扩展功能从框架中剥离出来,降低了框架的复杂度,让框架更容易实现。扩展功能与框架以一种很松的方式耦合,两者在保持接口不变的情况下,可以独立变化和发布,将软件的复杂度限制在了单个的插件之中,比较适用与需求不定或是业务容易发生变化的软件设计。

QtCreator源码分析(二)——QtCreator插件架构

 1、核心系统

核心系统包含两部分功能:

最小功能集合,提供给各个插件模块使用,也就是插件如何使用核心系统的功能进行功能扩展。

插件模块的生命周期管理。

2、插件模块

插件模块用于增强或扩展核心系统以产生额外的业务功能,插件模块应该是高度内聚,尽量避免产插件之间的依赖。

3、契约

契约包含了核心模块和插件模块的通信协议,模块之间不建议发生任何依赖。常见通信方式包含插件会提供一些虚函数,供核心系统中的模块加载器进行初始化、销毁等工作,核心系统提供一些函数,供具体插件模块使用,还可以通过soap等远程通信方式完成两者之间的通信。

二、QtCreator架构

QtCreator是由插件加载器和一堆插件构成的。QtCreator架构如下:

QtCreator源码分析(二)——QtCreator插件架构

 PluginManager负责插件的加载,管理,销毁等工作。core插件是QtCreator最基础的插件,提供了向界面增加菜单等功能。

1、QtCreator核心系统

QtCreator的核心系统由PluginManager和Core插件构成。PluginManager负责插件的管理工作,Core负责提供QtCreator的最小功能集合,PluginManager将Core当做普通插件进行加载。对于自定义插件,Core插件是一个基础功能库,使用Core库可以扩展QtCreator的功能。

QtCreator的所有功能全由插件实现,优点是简化了顶层业务,即插件管理工作的逻辑,只有PlunginManager和Plugin;缺点是增加了加载插件的复杂度,因为Core基础库插件需要被其他插件依赖,所以QtCreator在插件加载时就必须要考虑插件之间的依赖性。

QtCreator界面如下:

QtCreator源码分析(二)——QtCreator插件架构

 只包括core、Find、Locator、TextEditor四个必须插件的QtCreator如下:

QtCreator源码分析(二)——QtCreator插件架构

 CorePlugin类:

class CorePlugin : public ExtensionSystem::IPlugin
{
    Q_OBJECT
    Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QtCreatorPlugin" FILE "Core.json")
public:
    CorePlugin();
    ~CorePlugin();
    bool initialize(const QStringList &arguments, QString *errorMessage = 0);
    void extensionsInitialized();
    bool delayedInitialize();
    ShutdownFlag aboutToShutdown();
    QObject *remoteCommand(const QStringList & /* options */, const QStringList &args);
public slots:
    void fileOpenRequest(const QString&);
private:
    void parseArguments(const QStringList & arguments);
    MainWindow *m_mainWindow;
    EditMode *m_editMode;
    DesignMode *m_designMode;
};

Core插件初始化:

bool CorePlugin::initialize(const QStringList &arguments, QString *errorMessage)
{
    qsrand(QDateTime::currentDateTime().toTime_t());
    parseArguments(arguments);
    const bool success = m_mainWindow->init(errorMessage);
    if (success) {
        m_editMode = new EditMode;
        addObject(m_editMode);
        ModeManager::activateMode(m_editMode->id());
        m_designMode = new DesignMode;
        InfoBar::initializeGloballySuppressed();
    }
    // Make sure we respect the process's umask when creating new files
    Utils::SaveFile::initializeUmask();
    return success;
}

Core插件初始化函数内部完成QtCreater主界面的初始化工作。

2、插件模块

插件都需要继承IPlugin的接口,插件是由描述文件和继承IPlugin的类库组成。

描述文件描述了插件的基本信息,用于被插件管理器加载,最后一行描述了插件所依赖的其他插件,PluginManager会根据插件之间的依赖关系决定加载顺序。

实现一个插件需要实现的功能:

 A、在一个类中实现ExtensionSystem::IPlugin接口。

 B、使用Q_EXPORT_PLUGIN宏导出插件类。

C、为插件提供一个pluginspec文件,用于描述插件的元信息。

D、向其它插件暴露一个或多个对象。

E、搜寻其它插件暴露出来的可用的一个或多个对象。

class EXTENSIONSYSTEM_EXPORT IPlugin : public QObject
{
    Q_OBJECT
public:
    enum ShutdownFlag {
        SynchronousShutdown,
        AsynchronousShutdown
    };
    IPlugin();
    virtual ~IPlugin();
    virtual bool initialize(const QStringList &arguments, QString *errorString) = 0;
    virtual void extensionsInitialized() = 0;
    virtual bool delayedInitialize() { return false; }
    virtual ShutdownFlag aboutToShutdown() { return SynchronousShutdown; }
    virtual QObject *remoteCommand(const QStringList & /* options */, const QStringList & /* arguments */) { return 0; }
    PluginSpec *pluginSpec() const;
    void addObject(QObject *obj);
    void addAutoReleasedObject(QObject *obj);
    void removeObject(QObject *obj);
signals:
    void asynchronousShutdownFinished();
private:
    Internal::IPluginPrivate *d;
    friend class Internal::PluginSpecPrivate;
};

一个对外暴露的对象是由一个插件暴露的QObject(或其子类)的实例,对象位于对象池中,并且可以供其他插件使用。

插件暴露其对象的三种方法:

A、IPlugin::addAutoReleasedObject(QObject*)

B、IPlugin::addObject(QObject*)

C、PluginManager::addObject(QObject*)

IPlugin::addObject()和IPlugin::addAutoReleasedObject()其实都是调用的PluginManager::addObject()函数。所以,IPlugin的函数仅仅为了使用方便。推荐使用IPlugin的函数添加对象。addAutoReleasedObject()和addObject()的唯一区别是,前者添加的对象会在插件销毁的时候自动按照注册顺序从对象池中移除并 delete。

在任意时刻,都可以使用IPlugin::removeObject(QObject*)函数将对象从对象池中移除。

插件可以暴露任何对象。通常会把有可能被其它插件使用到的一些提供某些功能的对象暴露出来。在QtCreator中,暴露对象的功能的定义通常使用接口。常用接口如下:

Core::IOptionsPage

Core::IWizard 

Core::IEditor 

Core::IEditorFactory 

Core::IDocumentFactory 

Core::IExternalEditor 

Core::IContext

Core::ICore 

Core::ICoreListener 

Core::IDocument 

Core::IFileWizardExtension 

Core::IMode 

Core::INavigationWidgetFactory

Core::IOutputPane 

Core::IVersionControl 

C++ 开发者通常会将只包含 public纯虚函数的类当做接口。在QtCreator中,接口则是拥有一个或多个纯虚函数的QObject子类。

如果一个插件有实现了IXXX接口的对象,那么这个对象就应该被暴露出来。例如,一个插件中的某个类实现了INavigationWidgetFactory接口,并且暴露出来,那么 Core 就会自动把这个类提供的组件当做导航组件显示出来。

监控暴露对象

当使用PluginManager::addObject()添加对象的时候,PluginManager就会发出objectAdded(QObject*)信号。应用程序可以使用objectAdded信号来处理被添加的对象。

只有插件初始化之后被添加的对象发出的objectAdded()信号,才能够被插件接收到。

通常,连接到objectAdded()信号的 slot 会寻找一个或多个已知接口。假设插件要找的是INavigationWidgetFactory接口,那么就应该使用类似下面的代码:

void Plugin::slotObjectAdded(QObject * obj)
{
    INavigationWidgetFactory *factory = Aggregation::query(obj);
    if(factory)
    {
        // use it here...
    }
}

查找对象

插件需要查找提供其他功能的对象。PluginManager::allObjects()会返回QList<QObject*>形式的一个对象池;通过连接PluginManager::objectAdded()信号,可以知道有对象被暴露出来。

查找对象的另一种方式。

使用PluginManager::getObjects<T>()函数可以查找对象池中实现了某个接口的对象。

ExtensionSystem::PluginManager* pm = ExtensionSystem::PluginManager::instance();
QList<Core::INavigationWidgetFactory*> objects
  = pm->getObjects<Core::INavigationWidgetFactory>();

3、契约

契约包含两部分:一个是核心系统如何加载插件,另一个插件如何使用核心系统为软件扩展功能。

核心系统如何加载插件:

Main函数中:PluginManager::loadPlugins();

void PluginManager::loadPlugins()
{
    return d->loadPlugins();
}
void PluginManagerPrivate::loadPlugins()
{
    //获取待加载的插件,loadQueue函数会根据插件彼此依赖关系排序
    QList<PluginSpec *> queue = loadQueue();
    //加载插件
    foreach (PluginSpec *spec, queue) {
        loadPlugin(spec, PluginSpec::Loaded);
    }
    //初始化插件
    foreach (PluginSpec *spec, queue) {
        loadPlugin(spec, PluginSpec::Initialized);
    }
    QListIterator<PluginSpec *> it(queue);
    //按照相反顺序运行插件
    it.toBack();
    while (it.hasPrevious()) {
        loadPlugin(it.previous(), PluginSpec::Running);
    }
    emit q->pluginsChanged();
}

4、Git插件实例分析

通过分析Git插件源码,了解插件如何使用核心系统提供的功能

Git插件覆盖了git基本操作,以基本的log命令操作入来分析与Git插件与核心系统的交互,完成Log操作以后,界面显示如下,主编辑区显示了log信息。

在源码目录中找到Git插件,Git插件源码如下:

class GitPlugin : public VcsBase::VcsBasePlugin
{
    Q_OBJECT
    Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QtCreatorPlugin" FILE "Git.json")
public:
    GitPlugin();
    ~GitPlugin();
    static GitPlugin *instance();
    bool initialize(const QStringList &arguments, QString *errorMessage);
    GitVersionControl *gitVersionControl() const;
    const GitSettings &settings() const;
    void setSettings(const GitSettings &s);
    GitClient *gitClient() const;
public slots:
    void startCommit();
    void updateBranches(const QString &repository);
private slots:
    void diffCurrentFile();
    void diffCurrentProject();
    void diffRepository();
    void submitEditorDiff(const QStringList &unstaged, const QStringList &staged);
    void submitEditorMerge(const QStringList &unmerged);
    void submitCurrentLog();
    void logFile();
    void blameFile();
    void logProject();
    void logRepository();
    void undoFileChanges(bool revertStaging = true);
    void undoUnstagedFileChanges();
    void resetRepository();
    void startRebase();
    void startChangeRelatedAction();
    void stageFile();
    void unstageFile();
    void gitkForCurrentFile();
    void gitkForCurrentFolder();
    void cleanProject();
    void cleanRepository();
    void updateSubmodules();
    void applyCurrentFilePatch();
    void promptApplyPatch();
    void gitClientMemberFuncRepositoryAction();
    void startAmendCommit();
    void startFixupCommit();
    void stash();
    void stashSnapshot();
    void branchList();
    void remoteList();
    void stashList();
    void fetch();
    void pull();
    void push();
    void startMergeTool();
    void continueOrAbortCommand();
    void updateContinueAndAbortCommands();
protected:
    void updateActions(VcsBase::VcsBasePlugin::ActionState);
    bool submitEditorAboutToClose();
private:
    inline ParameterActionCommandPair
      createParameterAction(Core::ActionContainer *ac,const QString &defaultText, const QString &parameterText,const Core::Id &id, const Core::Context &context, bool addToLocator);
    inline ParameterActionCommandPair
createFileAction(Core::ActionContainer *ac,const QString &defaultText, const QString &parameterText,const Core::Id &id, const Core::Context &context, bool addToLocator,const char *pluginSlot);
    inline ParameterActionCommandPair
      createProjectAction(Core::ActionContainer *ac,const QString &defaultText, const QString &parameterText,const Core::Id &id, const Core::Context &context, bool addToLocator);
    inline ParameterActionCommandPair
      createProjectAction(Core::ActionContainer *ac,const QString &defaultText, const QString &parameterText,const Core::Id &id, const Core::Context &context, bool addToLocator,const char *pluginSlot);
    inline ActionCommandPair createRepositoryAction(Core::ActionContainer *ac,const QString &text, const Core::Id &id,const Core::Context &context, bool addToLocator);
    inline ActionCommandPair createRepositoryAction(Core::ActionContainer *ac,const QString &text, const Core::Id &id,const Core::Context &context,bool addToLocator, const char *pluginSlot);
    inline ActionCommandPair createRepositoryAction(Core::ActionContainer *ac,const QString &text, const Core::Id &id,const Core::Context &context,bool addToLocator, GitClientMemberFunc);
    void updateRepositoryBrowserAction();
    bool isCommitEditorOpen() const;
    Core::IEditor *openSubmitEditor(const QString &fileName, const CommitData &cd);
    void cleanCommitMessageFile();
    void cleanRepository(const QString &directory);
    void applyPatch(const QString &workingDirectory, QString file = QString());
    void startCommit(CommitType commitType);
    void updateVersionWarning();
    static GitPlugin *m_instance;
    Locator::CommandLocator *m_commandLocator;
    QAction *m_submitCurrentAction;
    QAction *m_diffSelectedFilesAction;
    QAction *m_undoAction;
    QAction *m_redoAction;
    QAction *m_menuAction;
    QAction *m_repositoryBrowserAction;
    QAction *m_mergeToolAction;
    QAction *m_submoduleUpdateAction;
    QAction *m_abortMergeAction;
    QAction *m_abortRebaseAction;
    QAction *m_abortCherryPickAction;
    QAction *m_abortRevertAction;
    QAction *m_continueRebaseAction;
    QAction *m_continueCherryPickAction;
    QAction *m_continueRevertAction;
    QAction *m_fixupCommitAction;
    QAction *m_interactiveRebaseAction;
    QVector<Utils::ParameterAction *> m_fileActions;
    QVector<Utils::ParameterAction *> m_projectActions;
    QVector<QAction *> m_repositoryActions;
    Utils::ParameterAction *m_applyCurrentFilePatchAction;
    Gerrit::Internal::GerritPlugin *m_gerritPlugin;
    GitClient                   *m_gitClient;
    QPointer<StashDialog>       m_stashDialog;
    QPointer<BranchDialog>      m_branchDialog;
    QPointer<RemoteDialog>      m_remoteDialog;
    QString                     m_submitRepository;
    QString                     m_commitMessageFileName;
    bool                        m_submitActionTriggered;
    GitSettings m_settings;
};

log操作对应的槽函数为logRepository:

void GitPlugin::logRepository()
{
    const VcsBase::VcsBasePluginState state = currentState();
    QTC_ASSERT(state.hasTopLevel(), return);
    m_gitClient->log(state.topLevel());
}
void GitClient::log(const QString &workingDirectory, const QStringList &fileNames,
                    bool enableAnnotationContextMenu, const QStringList &args)
{
    const QString msgArg = fileNames.empty() ? workingDirectory :
                           fileNames.join(QLatin1String(", "));
    const QString title = tr("Git Log \"%1\"").arg(msgArg);
    const Core::Id editorId = Git::Constants::GIT_LOG_EDITOR_ID;
    const QString sourceFile = VcsBase::VcsBaseEditorWidget::getSource(workingDirectory, fileNames);
    //从核心系统获取QtCreator的主编辑区
    VcsBase::VcsBaseEditorWidget *editor = findExistingVCSEditor("logFileName", sourceFile);
    if (!editor)
        editor = createVcsEditor(editorId, title, sourceFile, CodecLogOutput, "logFileName", sourceFile,
                                 new GitLogArgumentsWidget(this, workingDirectory,
                                                           enableAnnotationContextMenu,
                                                           args, fileNames));
    editor->setFileLogAnnotateEnabled(enableAnnotationContextMenu);
    editor->setDiffBaseDirectory(workingDirectory);
    QStringList arguments;
    //连接git的log命令
    arguments << QLatin1String("log") << QLatin1String(noColorOption)
              << QLatin1String(decorateOption);
    int logCount = settings()->intValue(GitSettings::logCountKey);
    if (logCount > 0)
         arguments << QLatin1String("-n") << QString::number(logCount);
    GitLogArgumentsWidget *argWidget = qobject_cast<GitLogArgumentsWidget *>(editor->configurationWidget());
    QStringList userArgs = argWidget->arguments();
    arguments.append(userArgs);
    if (!fileNames.isEmpty())
        arguments << QLatin1String("--") << fileNames;
    //执行git命令,将结果显示在主编辑区
    executeGit(workingDirectory, arguments, editor);
}
VcsBase::Command *GitClient::executeGit(const QString &workingDirectory,
                                        const QStringList &arguments,
                                        VcsBase::VcsBaseEditorWidget* editor,
                                        bool useOutputToWindow,
                                        bool expectChanges,
                                        int editorLineNumber)
{
    outputWindow()->appendCommand(workingDirectory, settings()->stringValue(GitSettings::binaryPathKey), arguments);
    //创建命令
    VcsBase::Command *command = createCommand(workingDirectory, editor, useOutputToWindow, editorLineNumber);
   //增加命令到任务队列
    command->addJob(arguments, settings()->intValue(GitSettings::timeoutKey));
    command->setTerminationReportMode(VcsBase::Command::NoReport);
    command->setUnixTerminalDisabled(false);
    command->setExpectChanges(expectChanges);
   //执行命令
    command->execute();
    return command;
}

5、插件架构扩展

在消息中间件、微服务盛行的今天,核心系统和插件完全可以设计成不在一个进程当中,可以把核心系统和插件之间通过远程调用的方式进行联系,核心系统与插件均可设计为单个进程或者服务,让整个系统的部署更加灵活,单个插件的问题也不会影响到整个系统。当然,核心系统与插件之间的发现使用机制,是需要结合实际使用场景进一步深入思考的。