1. 程式人生 > >Qt5的外掛機制(4)--Qt外掛的元資訊metaData

Qt5的外掛機制(4)--Qt外掛的元資訊metaData


<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

JSON 與Qt外掛的元資訊 MetaData


Qt外掛的原始碼中,基本都能見到一個 xxx.json 的檔案,這個檔案中通常只包含一句:

{
    "Keys": [ "yyy" ]
}

我們可以猜到這個檔案中的"Keys"應該是指定了與外掛相關的關鍵字。那這個 .json 檔案到底是如何起作用的?
先來認識一下 JSON .

JSON是一種儲存結構化資料的格式,它有6中基本資料型別,分別是:

bool    布林型,取值可以是 true 或 false
double    數字型別
string    字串型別
array    陣列型別
object    物件型別
null    空型別

具體可參見 Qt Assistant 中關於"JSON Support in Qt "的介紹。

A simple JSON document encoding a person, his/her age, address and phone numbers could look like:

{
    "FirstName": "John",    # FirstName是變數(欄位)的名稱;John是變數的值
    "LastName": "Doe",
    "Age": 43,
    "Address": {
        "Street": "Downing Street 10",
        "City": "London",
        "Country": "Great Britain"
    },
    "Phone numbers": [
        "+44 1234567",
        "+44 2345678"
    ]
}



值得一提的是,陣列型別的欄位在.json檔案中賦值時應該用方括號 '[' 和 ']' 括起來,物件型別的欄位在賦值時
應用花括號 '{' 和 '}' 括起來,普通型別的資料則不需要括。每一個 .json 檔案描述了一個 JSON物件,而一個JSON
物件中的物件型別欄位,又可以看做是一個子JSON物件(JSON物件的巢狀)。
再回過頭來看看外掛原始碼中的 xxx.json 檔案,可以發現其中的變數 Keys 其實是個陣列變數(因為它賦值時用方括號
括住了),只不過這個陣列中通常只有一個元素而已,即一個外掛一般只有一個關鍵字,當然也可以為一個外掛設定多個
關鍵字。

.json在Qt外掛中主要用於儲存Qt外掛的元資訊(metaData),在Qt中,有一個專門的類 QJsonObject 來描述一個JSON。
QLibraryPrivate::metaData() 返回的是一個QJsonObject類的指標,通過這個指標可以訪問該 QLibraryPrivate 物件對應的庫的元資訊;
QFactoryLoader::metaData() 返回的是一個 QList<QJsonObject> 列表,這個列表中順序存放了與該 QFactoryLoader 物件相關的所有庫
的元資訊(動態庫的元資訊在前,靜態庫的元資訊在後)。
另外值得注意的是,QLibraryPrivate::metaData() 返回的QJsonObject物件中一般都有一個 MetaData 欄位,這個欄位是物件型別的資料,
他可以看做是庫的元資訊的一個子JSON物件,而且它對應的就是 .json 檔案中的內容。(.json檔案中的內容只是一個庫的元資訊的一部分)

每個QFactoryLoader物件都有一個 d->iid 成員 (d是與QFactoryLoader物件關聯的QFactoryLoaderPrivate例項),可用於描述外掛的種類,
每一類外掛都有一個獨立的 IID 值, 比如平臺輸入法類外掛的IDD都應該是 org.qt-project.Qt.QPlatformInputContextFactoryInterface,
平臺類外掛的IDD都應是 org.qt-project.Qt.QPA.QPlatformIntegrationFactoryInterface.5.2 等。



<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

QLibraryPrivate的metaData(庫的元資訊)的產生過程



在編寫Qt外掛時需要用到 Q_PLUGIN_METADATA 這個巨集來設定一個外掛的元資訊,但在Qt的原始碼中,發現這個巨集是空的,所以不用說,這個
巨集是被MOC解析的而不是C++編譯器。那就看看MOC會怎麼對付這個巨集吧。找了個很簡單的檔案,用moc處理一下,看看都生成了什麼。

main.cpp檔案內容如下,其中使用了Q_PLUGIN_METADATA巨集


#include <qpa/qplatforminputcontextplugin_p.h>
#include <QtCore/QStringList>
#include "QtMinimalInputMethodFrame.h"
QT_BEGIN_NAMESPACE
class QtMinimalInputMethodFramePlugin : public QPlatformInputContextPlugin
{
    Q_OBJECT
    Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QPlatformInputContextFactoryInterface" FILE "qtminimal.json")    // 指定了IID和.json檔案

public:
    QtMinimalInputMethodFrame *create(const QString &, const QStringList &);
};
QtMinimalInputMethodFrame *QtMinimalInputMethodFramePlugin::create(const QString &system, const QStringList ¶mList)
{
    Q_UNUSED(paramList);
    if (system.compare(system, QStringLiteral("qtminimal"), Qt::CaseInsensitive) == 0)
        return new QtMinimalInputMethodFrame;
    return 0;
}
QT_END_NAMESPACE
#include "main.moc"



qtminimal.json檔案內容如下:

{
    "Keys": [ "qtminimal" ]
}

使用 "moc main.cpp moc_main.cpp" ,生成moc_main.cpp檔案,開啟,發現裡面有一部分程式碼如下:

    ...
    ...

static const unsigned char qt_pluginMetaData[] = {
    'Q', 'T', 'M', 'E', 'T', 'A', 'D', 'A', 'T', 'A', ' ', ' ',        // "QTMETADATA",這段字串可看做Qt的外掛元資訊的頭,通過這個關鍵字能搜尋到元資訊的位置
    0x71, 0x62, 0x6a, 0x73, 0x01, 0x00, 0x00, 0x00,
    0x00, 0x01, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00,
    0xec, 0x00, 0x00, 0x00, 0x1b, 0x03, 0x00, 0x00,
    0x03, 0x00, 0x49, 0x49, 0x44, 0x00, 0x00, 0x00,
    0x37, 0x00, 0x6f, 0x72, 0x67, 0x2e, 0x71, 0x74,
    0x2d, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74,
    0x2e, 0x51, 0x74, 0x2e, 0x51, 0x50, 0x6c, 0x61,
    0x74, 0x66, 0x6f, 0x72, 0x6d, 0x49, 0x6e, 0x70,
    0x75, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78,
    0x74, 0x46, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x79,
    0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63,
    0x65, 0x00, 0x00, 0x00, 0x9b, 0x0c, 0x00, 0x00,
    0x09, 0x00, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x4e,
    0x61, 0x6d, 0x65, 0x00, 0x1f, 0x00, 0x51, 0x74,
    0x4d, 0x69, 0x6e, 0x69, 0x6d, 0x61, 0x6c, 0x49,
    0x6e, 0x70, 0x75, 0x74, 0x4d, 0x65, 0x74, 0x68,
    0x6f, 0x64, 0x46, 0x72, 0x61, 0x6d, 0x65, 0x50,
    0x6c, 0x75, 0x67, 0x69, 0x6e, 0x00, 0x00, 0x00,
    0x5a, 0x60, 0xa0, 0x00, 0x07, 0x00, 0x76, 0x65,
    0x72, 0x73, 0x69, 0x6f, 0x6e, 0x00, 0x00, 0x00,
    0x11, 0x00, 0x00, 0x00, 0x05, 0x00, 0x64, 0x65,
    0x62, 0x75, 0x67, 0x00, 0x95, 0x16, 0x00, 0x00,
    0x08, 0x00, 0x4d, 0x65, 0x74, 0x61, 0x44, 0x61,
    0x74, 0x61, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00,
    0x03, 0x00, 0x00, 0x00, 0x34, 0x00, 0x00, 0x00,
    0x14, 0x03, 0x00, 0x00, 0x04, 0x00, 0x4b, 0x65,
    0x79, 0x73, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00,
    0x02, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00,
    0x09, 0x00, 0x71, 0x74, 0x6d, 0x69, 0x6e, 0x69,
    0x6d, 0x61, 0x6c, 0x00, 0x8b, 0x01, 0x00, 0x00,
    0x0c, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00,
    0xa4, 0x00, 0x00, 0x00, 0x54, 0x00, 0x00, 0x00,
    0x98, 0x00, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00
};
    ...
    ...

// 留意一下QT_MOC_EXPORT_PLUGIN這個巨集
QT_MOC_EXPORT_PLUGIN(QtMinimalInputMethodFramePlugin, QtMinimalInputMethodFramePlugin)    

    ...
    ...



上面的程式碼中生成了一個名為 qt_pluginMetaData 的陣列,看其名字就能猜出來這個陣列是與外掛的元資訊有關的,
將該陣列用字元顯示,結果為:
QTMETADATA  qbjs^A^@^@^@^@^A^@^@^K^@^@^@ì^@^@^@^[^C^@^@^C^@
IID^@^@^@7^@org.qt-project.Qt.QPlatformInputContextFactoryInterface    // IDD
^@^@^@<9b>^L^@^@^@        
className^@^_^@QtMinimalInputMethodFramePlugin        // 類名
^@^@^@Z` ^@^G^@
version^@^@^@^Q^@^@^@^E^@            // 版本
debug^@<95>^V^@^@^H^@                // debug
MetaData^@^@8^@^@^@^C^@^@^@4^@^@^@^T^C^@^@^D^@    // MetaData
Keys^@^@^\^@^@^@^B^@^@^@^X^@^@^@^@qtminimal    // Keys
^@<8b>^A^@^@^L^@^@^@^L^@^@^@¤^@^@^@T^@^@^@<98>^@^@^@<88>^@^@^@


可以看到其中已經包含了IDD、Keys等資訊,因此可以確定與外掛相關的元資訊就是存在這個陣列中。
上面給出的程式碼最後,使用了QT_MOC_EXPORT_PLUGIN巨集,它的作用是定義兩個函式,qt_plugin_instance() 和
qt_plugin_query_metadata(),前者用於返回外掛類(QtMinimalInputMethodFramePlugin)的一個例項,後者用於
返回陣列qt_pluginMetaData的指標。因此,當這個庫被載入後,通過庫中的qt_plugin_query_metadata()函式就可以獲得
庫的元資訊陣列(二進位制資料)。

#define Q_PLUGIN_INSTANCE(IMPLEMENTATION) \    // 這個巨集用於返回一個類名是IMPLEMENTATION的類的靜態例項
        { \
            static QT_PREPEND_NAMESPACE(QPointer)<QT_PREPEND_NAMESPACE(QObject)> _instance; \
            if (!_instance)      \
                _instance = new IMPLEMENTATION; \
            return _instance; \
        }

#if defined(QT_STATICPLUGIN)
    // 這部分是靜態編譯時用的巨集,我們暫不管它
#  define QT_MOC_EXPORT_PLUGIN(PLUGINCLASS, PLUGINCLASSNAME) \
    static QT_PREPEND_NAMESPACE(QObject) *qt_plugin_instance_##PLUGINCLASSNAME() \
    Q_PLUGIN_INSTANCE(PLUGINCLASS) \
    static const char *qt_plugin_query_metadata_##PLUGINCLASSNAME() { return (const char *)qt_pluginMetaData; } \
    const QT_PREPEND_NAMESPACE(QStaticPlugin) qt_static_plugin_##PLUGINCLASSNAME() { \
        QT_PREPEND_NAMESPACE(QStaticPlugin) plugin = { qt_plugin_instance_##PLUGINCLASSNAME, qt_plugin_query_metadata_##PLUGINCLASSNAME}; \
        return plugin; \
    }

#else
    // 這部分才是動態編譯時用的巨集
#  define QT_MOC_EXPORT_PLUGIN(PLUGINCLASS, PLUGINCLASSNAME)      \
            Q_EXTERN_C Q_DECL_EXPORT \
            const char *qt_plugin_query_metadata() \            // 這個函式返回一個指標,指向元資訊陣列
            { return (const char *)qt_pluginMetaData; } \
            Q_EXTERN_C Q_DECL_EXPORT QT_PREPEND_NAMESPACE(QObject) *qt_plugin_instance() \ 這個函式返回外掛類PLUGINCLASS的一個靜態例項
            Q_PLUGIN_INSTANCE(PLUGINCLASS)

#endif


我們雖然可以通過qt_plugin_query_metadata()函式獲得庫的元資訊陣列(二進位制資料),但是還需要把這些
二進位制資料轉換成 QJsonObject 物件,這樣才方便在程式中對其各個欄位進行訪問。QLibraryPrivate類有個
靜態成員函式fromRawMetaData,可以從二進位制資料中構造一個 QJsonDocument 物件,
而QJsonDocument物件的 object() 方法就能返回所需的 QJsonObject 物件(庫的元資訊)。
通過這些可以猜測,QLibraryPrivate類的 metaData 成員是這樣被設定的:
載入一個庫檔案後,將庫中的qt_plugin_query_metadata()這個符號resolve出來,並通過它得到陣列 qt_pluginMetaData 的指標,
然後呼叫QLibraryPrivate類的fromRawMetaData方法講二進位制儲存的元資訊轉換到一個QJsonObject 物件中,最後將這個物件賦值
給QLibraryPrivate類的metaData成員。


現在問題是, QLibraryPrivate的metaData是在哪裡,被誰設定的?

當第一次使用 QLibraryPrivate類的 isPlugin() 方法調查一個庫是否是外掛時,這個方法內部會呼叫 QLibraryPrivate::updatePluginState() 。

bool QLibraryPrivate::isPlugin()
{
    if (pluginState == MightBeAPlugin)    // pluginState在建構函式中會被初始化為MightBeAPlugin
        updatePluginState();

    return pluginState == IsAPlugin;
}

而QLibraryPrivate::updatePluginState()函式中,需要讀取QLibraryPrivate類的 metaData 資訊來確認一個庫是不是外掛,而 metaData 也就是在這時
被設定的。從這個函式中還可以看出,只有Qt外掛才有 metaData 元資訊, 普通的庫是沒有的(這一點我上不能打保票,不過從下面這個函式的程式碼上看,應該
是這樣的)。

void QLibraryPrivate::updatePluginState()
{
    errorString.clear();
    if (pluginState != MightBeAPlugin)
        return;        // 如果pluginState的狀態已經確定了,則直接返回

    bool success = false;

#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC)
    if (fileName.endsWith(QLatin1String(".debug"))) {
        // refuse to load a file that ends in .debug
        // these are the debug symbols from the libraries
        // the problem is that they are valid shared library files
        // and dlopen is known to crash while opening them

        // pretend we didn't see the file
        errorString = QLibrary::tr("The shared library was not found.");
        pluginState = IsNotAPlugin;
        return;
    }
#endif

    // 這裡開始設定 metaData 。findPatternUnloaded() 和 qt_get_metadata() 兩個函式的內部都會設定 metaData
    if (!pHnd) {
    // 如果庫還沒有被載入,(pHnd==NULL說明庫未被載入),則呼叫findPatternUnloaded函式,它內部會
    // 講對應的庫檔案開啟,讀取其內容,在其中尋找Qt的外掛元資訊的頭Header(就是前面提到的"QTMETADATA"),
    // 如果找到了,就解析元資訊。這種方法的好處是在不載入庫的情況下也能獲得其元資訊。
        // scan for the plugin metadata without loading
        success = findPatternUnloaded(fileName, this);    
    } else {
    // 如果庫已被載入,則呼叫qt_get_metadata獲得元資訊。
    // QtPluginQueryVerificationDataFunction是一個函式指標型別,其具體型別是 :  char*(*)(), 它返回
    // 一個字元指標(指向一個二進位制陣列),準確的說,他應該返回上面提到的qt_pluginMetaData陣列的指標 。

        // library is already loaded (probably via QLibrary)
        // simply get the target function and call it.
        QtPluginQueryVerificationDataFunction getMetaData = NULL;
        getMetaData = (QtPluginQueryVerificationDataFunction) resolve("qt_plugin_query_metadata");    
        // 上面這行將庫中的qt_plugin_query_metadata符號resolve出來
        success = qt_get_metadata(getMetaData, this);
    }

    if (!success) {
    // 如果獲取 元資訊 失敗,即當前庫沒有元資訊,就將pluginState設定為IsNotAPlugin,代表非Qt外掛。
    // 這裡因為當前庫沒有元資訊,就認為它不是Qt外掛,所以這是不是意味著,只有Qt外掛才有元資訊而普通庫是沒有的?
        if (errorString.isEmpty()){
            if (fileName.isEmpty())
                errorString = QLibrary::tr("The shared library was not found.");
            else
                errorString = QLibrary::tr("The file '%1' is not a valid Qt plugin.").arg(fileName);
        }
        pluginState = IsNotAPlugin;
        return;
    }

    pluginState = IsNotAPlugin; // be pessimistic

    // 如果獲取 元資訊 成功,再看版本號是否符合要求,如果版本號合適,則將pluginState設定為IsAPlugin,代表是Qt外掛
    uint qt_version = (uint)metaData.value(QLatin1String("version")).toDouble();
    bool debug = metaData.value(QLatin1String("debug")).toBool();
    if ((qt_version & 0x00ff00) > (QT_VERSION & 0x00ff00) || (qt_version & 0xff0000) != (QT_VERSION & 0xff0000)) {
        if (qt_debug_component()) {
            qWarning("In %s:\n"
                 "  Plugin uses incompatible Qt library (%d.%d.%d) [%s]",
                 (const char*) QFile::encodeName(fileName),
                 (qt_version&0xff0000) >> 16, (qt_version&0xff00) >> 8, qt_version&0xff,
                 debug ? "debug" : "release");
        }
        errorString = QLibrary::tr("The plugin '%1' uses incompatible Qt library. (%2.%3.%4) [%5]")
            .arg(fileName)
            .arg((qt_version&0xff0000) >> 16)
            .arg((qt_version&0xff00) >> 8)
            .arg(qt_version&0xff)
            .arg(debug ? QLatin1String("debug") : QLatin1String("release"));
#ifndef QT_NO_DEBUG_PLUGIN_CHECK
    } else if(debug != QLIBRARY_AS_DEBUG) {
        //don't issue a qWarning since we will hopefully find a non-debug? --Sam
        errorString = QLibrary::tr("The plugin '%1' uses incompatible Qt library."
                 " (Cannot mix debug and release libraries.)").arg(fileName);
#endif
    } else {
        pluginState = IsAPlugin;
    }
}