Qt學習--Qt Plugin建立及呼叫2(外掛管理器)

張明奇-卡哥發表於2020-09-30

Qt Plugin建立及呼叫2–外掛管理器

簡述

Qt 本身提供了外掛相關的技術,但並沒有提供一個通用的外掛框架!倘若要開發一個較大的 GUI 應用程式,並希望使其可擴充套件,那麼擁有這樣一個外掛框架無疑會帶來很大的好處。

外掛系統構成

外掛系統,可以分為三部分:

  • 主系統
    通過外掛管理器載入外掛,並建立外掛物件。一旦外掛物件被建立,主系統就會獲得相應的指標/引用,它可以像任何其他物件一樣使用。
  • 外掛管理器
    用於管理外掛的生命週期,並將其暴露給主系統。它負責查詢並載入外掛,初始化它們,並且能夠進行解除安裝。它還應該讓主系統迭代載入的外掛或註冊的外掛物件。
  • 外掛
    外掛本身應符合外掛管理器協議,並提供符合主系統期望的物件。

實際上,很少能看到這樣一個相對獨立的分離,外掛管理器通常與主系統緊密耦合,因為外掛管理器需要最終提供(定製)某些型別的外掛物件的例項。

程式流

框架的基本程式流,如下所示:

外掛管理器

上面提到,外掛管理器有一個職責 - 載入外掛。那麼,是不是所有的外掛都需要載入呢?當然不是!只有符合我們約定的外掛,才會被認為是標準的、有效的外掛,外來外掛一律認定為無效。

為了解決這個問題,可以為外掛設定一個“標識(Interface)” - PluginInterface.h

#ifndef PLUGININTERFACE_H
#define PLUGININTERFACE_H
#include <QStringList>
#include <QWidget>
class PluginInterface
{
public:
    virtual ~PluginInterface() {}

public:
    virtual void setInitData(QStringList &strlist) = 0;
    virtual void getResultData(QStringList &strlist) = 0;
};
#define PluginInterface_iid "QtPluginsTest.QtPluginsManager.PluginInterface"

Q_DECLARE_INTERFACE(PluginInterface, PluginInterface_iid)

#endif // PLUGININTERFACE_H

後期實現的所有外掛,都必須繼承自 PluginInterface,這樣才會被認定是自己的外掛,以防外部外掛注入。

注意:使用 Q_DECLARE_INTERFACE 巨集,將 PluginInterface介面與識別符號一起公開。

外掛的基本約束有了,外掛的具體實現外掛管理器並不關心,它所要做的工作是載入外掛、解除安裝外掛、檢測外掛的依賴、以及掃描外掛的後設資料(Json 檔案中的內容)。。。為了便於操作,將其實現為一個單例。

qtpluginmanager.h內容如下:

#ifndef QTPLUGINSMANAGER_H
#define QTPLUGINSMANAGER_H

#include "qtpluginsmanager_global.h"
#include <QObject>
#include <QPluginLoader>
#include <QVariant>

class QtPluginsManagerPrivate;

class QTPLUGINSMANAGERSHARED_EXPORT QtPluginsManager : public QObject
{
    Q_OBJECT
public:
    QtPluginsManager();
    ~QtPluginsManager();


    static QtPluginsManager *instance();

    //載入所有外掛
    void loadAllPlugins();
    //掃描JSON檔案中的外掛後設資料
    void scanMetaData(const QString &filepath);
    //載入其中某個外掛
    void loadPlugin(const QString &filepath);
    //解除安裝所有外掛
    void unloadAllPlugins();
    //解除安裝某個外掛
    void unloadPlugin(const QString &filepath);
    //獲取所有外掛
    QList<QPluginLoader *> allPlugins();
    //獲取所有外掛名稱
    QList<QVariant> allPluginsName();
    //獲取某個外掛名稱
    QVariant getPluginName(QPluginLoader *loader);
private:
    static QtPluginsManager *m_instance;
    QtPluginsManagerPrivate *d;
};

可以看到,外掛管理器中有一個 d 指標,它包含了外掛後設資料的雜湊表。此外,由於其擁有所有外掛的後設資料,所以還為其賦予了另外一個職能 - 檢測外掛的依賴關係:

class QtPluginsManagerPrivate
{
public:
    //外掛依賴檢測
    bool check(const QString &filepath);

    QHash<QString, QVariant> m_names; //外掛路徑--外掛名稱
    QHash<QString, QVariant> m_versions; //外掛路徑--外掛版本
    QHash<QString, QVariantList>m_dependencies; //外掛路徑--外掛額外依賴的其他外掛
    QHash<QString, QPluginLoader *>m_loaders; //外掛路徑--QPluginLoader例項
};

注意: 這裡的 check() 是一個遞迴呼叫,因為很有可能存在“外掛A”依賴於“外掛B”,而“外掛B”又依賴於“外掛C”的連續依賴情況。

QtPluginsManagerPrivate中的雜湊表在初始化外掛管理器時被填充:

void QtPluginsManager::loadAllPlugins()
{
    QDir pluginsdir = QDir(qApp->applicationDirPath());
    pluginsdir.cd("plugins");

    QFileInfoList pluginsInfo = pluginsdir.entryInfoList(QDir::Files | QDir::NoDotAndDotDot);
    //初始化外掛中的後設資料
    for(QFileInfo fileinfo : pluginsInfo)
        scanMetaData(fileinfo.absoluteFilePath());

    //載入外掛
    for(QFileInfo fileinfo : pluginsInfo)
        loadPlugin(fileinfo.absoluteFilePath());
}

後設資料的具體掃描由 scan() 負責:

void QtPluginsManager::scanMetaData(const QString &filepath)
{
    //判斷是否為庫(字尾有效性)
    if(!QLibrary::isLibrary(filepath))
        return ;
    //獲取後設資料
    QPluginLoader *loader = new QPluginLoader(filepath);
    QJsonObject json = loader->metaData().value("MetaData").toObject();

    QVariant var = json.value("name").toVariant();
    d->m_names.insert(filepath, json.value("name").toVariant());
    d->m_versions.insert(filepath, json.value("version").toVariant());
    d->m_dependencies.insert(filepath, json.value("dependencies").toArray().toVariantList());

    delete loader;
    loader = nullptr;
}

一旦所有後設資料被掃描,便可以檢查是否能夠載入外掛:

void QtPluginsManager::loadPlugin(const QString &filepath)
{
    if(!QLibrary::isLibrary(filepath))
        return;

    //檢測依賴
    if(!d->check(filepath))
        return;

    //載入外掛
    QPluginLoader *loader = new QPluginLoader(filepath);
    if(loader->load())
    {
        PluginInterface *plugin = qobject_cast<PluginInterface *>(loader->instance());
        if(plugin)
        {
            d->m_loaders.insert(filepath, loader);
            plugin->connect_information(this, SLOT(onPluginInformation(QString&)), true);
        }
        else
        {
            delete loader;
            loader = nullptr;
        }
    }
}

注意: 這裡用到了前面提到的標識 - PluginInterface,只有 qobject_cast 轉換成功,才會載入到主系統中,這可以算作是真正意義上的第一道防線。

實際上,在內部檢查是通過呼叫 QtPluginManagerPrivate::check() 遞迴地查詢依賴後設資料來完成的。

bool QtPluginsManagerPrivate::check(const QString &filepath)
{
    for(QVariant item : m_dependencies.value(filepath))
    {
        QVariantMap map = item.toMap();
        //依賴的外掛名稱、版本、路徑
        QVariant name = map.value("name");
        QVariant version = map.value("version");
        QString path = m_names.key(name);

        /********** 檢測外掛是否依賴於其他外掛 **********/
        // 先檢測外掛名稱
        if(!m_names.values().contains(name))
        {
            QString strcons = "Missing dependency: "+ name.toString()+" for plugin "+path;
            qDebug()<<Q_FUNC_INFO<<strcons;
            QMessageBox::warning(nullptr, ("Plugins Loader Error"), strcons, QMessageBox::Ok);
            return false;
        }
        //再檢測外掛版本
        if(m_versions.value(path) != version)
        {
            QString strcons = "Version mismatch: " + name.toString() +" version "+m_versions.value(m_names.key(name)).toString()+
                                                    " but " + version.toString() + " required for plugin "+path;
            qDebug()<<Q_FUNC_INFO<<strcons;
            QMessageBox::warning(nullptr, "Plugins Loader Error", strcons, QMessageBox::Ok);
            return false;
        }
        //最後檢測被依賴的外掛是否還依賴其他的外掛
        if(!check(path))
        {
            QString strcons = "Corrupted dependency: "+name.toString()+" for plugin "+path;
            qDebug()<<Q_FUNC_INFO<<strcons;
            QMessageBox::warning(nullptr, "Plugins Loader Error", strcons, QMessageBox::Ok);
            return false;
        }
    }

    return true;
}

外掛解除安裝的過程正好相反:

void QtPluginsManager::unloadAllPlugins()
{
    for(QString filepath : d->m_loaders.keys())
        unloadPlugin(filepath);
}

而具體的解除安裝由 unloadPlugin() 來完成:

void QtPluginsManager::unloadPlugin(const QString &filepath)
{
    QPluginLoader *loader = d->m_loaders.value(filepath);
    //解除安裝外掛,並從內部資料結構中移除
    if(loader->unload())
    {
        d->m_loaders.remove(filepath);
        delete loader;
        loader = nullptr;
    }
}

萬事俱備,然後返回所有的外掛,以便主系統訪問:

QList<QPluginLoader *> QtPluginsManager::allPlugins()
{
    return d->m_loaders.values();
}

這樣,整個外掛管理的機制已經建立起來了,萬里長征第一步。。。那剩下的事基本就比較簡單了!外掛的編寫、外掛之間的互動。。。

相關文章