Qt原始碼解析——元物件系統熱身

JoggingJack發表於2023-11-10

關鍵詞:Qt 原始碼 QObject QMetaObject 元物件系統 屬性 事件 訊號 槽

概述

原系列文章地址

官方文件第二章內容就是元物件系統,它在介紹裡描述到:

Qt的元物件系統提供了訊號和槽機制(用於物件間的通訊)、執行時型別資訊和動態屬性系統。

元物件系統基於三個要素:

  1. QObject類為那些可以利用元物件系統的物件提供了一個基類
  2. 在類宣告的私有部分中使用Q_OBJECT宏用於啟用元物件特性,比如動態屬性、訊號和槽。
  3. 元物件編譯器(moc)為每個QObject子類提供必要的程式碼來實現元物件特性。

moc工具讀取C++原始檔,如果發現一個或多個包含Q_OBJECT宏的類宣告,它會生成另一個C++原始檔,其中包含了這些類的每個元物件的程式碼。這個生成的原始檔被#include進入類的原始檔,更常見的是被編譯並連結到類的實現中。

引入這個系統的主要原因是訊號和槽機制,此外它還提供了一些額外功能:

  • QObject::metaObject() 返回與該類相關聯的元物件。
  • QMetaObject::className() 在執行時以字串形式返回類名,而無需透過 C++ 編譯器提供本地執行時型別資訊(RTTI)支援。
  • QObject::inherits() 函式返回一個物件是否是在 QObject 繼承樹內繼承了指定類的例項。
  • QObject::tr()QObject::trUtf8() 用於國際化的字串翻譯。
  • QObject::setProperty()QObject::property() 動態地透過名稱設定和獲取屬性。
  • QMetaObject::newInstance() 構造該類的新例項。

上面說到的元物件系統三要素,第3點moc會在後面用單獨篇章分析,下面就不再展開,第1點我們在上一篇中做了簡單的分析,本篇我們看看第2點——Q_OBJECT到底怎麼啟用了元物件系統(然而啟用非常複雜,我們先瀏覽個大概,所以標題叫熱身)。

staticMetaObject

找到原始碼中出現QMetaObject的地方:

//qobject.h
class Q_CORE_EXPORT Qobject{
    Q_OBJECT
    //...
protected:
    static const QMetaObject staticQtMetaObject;
    //...
}

QMetaObject相關的變數只有2個地方出現,既然前面說了Q_OBJECT和元物件系統相關,那我們就直接看Q_OBJECT的定義:

//qobjectdefs.h
#define Q_OBJECT \
public: \
    QT_WARNING_PUSH \
    Q_OBJECT_NO_OVERRIDE_WARNING \
    static const QMetaObject staticMetaObject; \
    virtual const QMetaObject *metaObject() const; \
    virtual void *qt_metacast(const char *); \
    virtual int qt_metacall(QMetaObject::Call, int, void **); \
    QT_TR_FUNCTIONS \
private: \
    Q_OBJECT_NO_ATTRIBUTES_WARNING \
    Q_DECL_HIDDEN_STATIC_METACALL static void qt_static_metacall(QObject *, QMetaObject::Call, int, void **); \
    QT_WARNING_POP \
    struct QPrivateSignal {}; \
    QT_ANNOTATE_CLASS(qt_qobject, "")

我們關注變數static const QMetaObject staticMetaObject,這是一個QMetaObject型別的靜態變數,它應該是和元物件系統相關,文件對QMetaObject的描述:

QMetaObject類包含有關Qt物件的元資訊。每個在應用程式中使用的QObject子類都會建立一個QMetaObject例項,該例項儲存了該QObject子類的所有元資訊。此物件可透過QObject::metaObject()方法獲得。

QMetaObject就是元物件系統的關鍵了,檢視QMetaObject的定義:

//qobjectdefs.h
struct Q_CORE_EXPORT QMetaObject{
    //...
    struct { // private data
        const QMetaObject *superdata;
        const QByteArrayData *stringdata;
        const uint *data;
        typedef void (*StaticMetacallFunction)(QObject *, QMetaObject::Call, int, void **);
        StaticMetacallFunction static_metacall;
        const QMetaObject * const *relatedMetaObjects;
        void *extradata; //reserved for future use
    } d;
}

QMetaObject是個結構體,沒有建構函式。忽略掉所有方法宣告,只剩一個結構體變數,而且我們在qobject.cpp中也沒有看到staticMetaObject對應的初始化。那會不會在子類中初始化了?我們新建一個空的QMainWindow工程,繼承關係是這樣的:

//MainWindow->QMainWindow->QWidget->QObject

遺憾的是我們並沒有在MainWindowQMainWindowQWidget的構造器中找到staticMetaObject初始化的痕跡。

moc_mainwindow.cpp

想起來官方文件說moc會處理Q_OBJECT宏,那就去moc檔案找找——果然找到了staticMetaObject相關的語句:

//moc_mainwindow.cpp
QT_INIT_METAOBJECT const QMetaObject MainWindow::staticMetaObject = { {
    &QMainWindow::staticMetaObject,
    qt_meta_stringdata_MainWindow.data,
    qt_meta_data_MainWindow,
    qt_static_metacall,
    nullptr,
    nullptr
} };

結合QMetaObject的宣告,我們很容易看出這是在對QMetaObject的變數賦值:

變數名
const QMetaObject *superdata &QMainWindow::staticMetaObject
const QByteArrayData *stringdata qt_meta_stringdata_MainWindow.data
const uint *data qt_meta_data_MainWindow
StaticMetacallFunction static_metacall qt_static_metacall
const QMetaObject * const *relatedMetaObjects nullptr
void *extradata nullptr

對於const QMetaObject *superdata = &QMainWindow::staticMetaObject;

MainWindowstaticMetaObjectsuperdata持有了QMainWindowstaticMetaObject``,說明MainWindow可以訪問QMainWindowstaticMetaObject。由於並不能看到moc_qmainwindow.cpp等,我們只能從變數名合理猜測任何類的staticMetaObject都持有了父類的staticMetaObject

做個實驗測試一下:

//mainwindow.cpp
MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    //...
    const QMetaObject *metaDta = staticMetaObject.d.superdata;
    while(metaDta){
        qDebug() << metaDta->className();
        metaDta = metaDta->d.superdata;
    }
}

/*
輸出結果:
QMainWindow
QWidget
QObject
*/

果不其然,列印結果是輸出了MainWindow所有父類的className。那麼我們基本可以斷定,繼承鏈中staticMetaObject的持有關係如下圖所示:

對於const QByteArrayData *stringdata = qt_meta_stringdata_MainWindow.data;

moc檔案裡找到qt_meta_stringdata_MainWindow變數:

//moc_mainwindow.cpp
static const qt_meta_stringdata_MainWindow_t qt_meta_stringdata_MainWindow = {
    {
QT_MOC_LITERAL(0, 0, 10) // "MainWindow"

    },
    "MainWindow"
};

qt_meta_stringdata_MainWindow是一個qt_meta_stringdata_MainWindow_t型別,這裡對它進行了初始化。繼續找到qt_meta_stringdata_MainWindow_t的定義:

//moc_mainwindow.cpp
struct qt_meta_stringdata_MainWindow_t {
    QByteArrayData data[1];
    char stringdata0[11];
};

也就是說stringdata的值為QT_MOC_LITERAL(0, 0, 10) // "MainWindow"

繼續找到QT_MOC_LITERAL的定義:

//moc_mainwindow.cpp
#define QT_MOC_LITERAL(idx, ofs, len) \
    Q_STATIC_BYTE_ARRAY_DATA_HEADER_INITIALIZER_WITH_OFFSET(len, \
    qptrdiff(offsetof(qt_meta_stringdata_MainWindow_t, stringdata0) + ofs \
        - idx * sizeof(QByteArrayData)) \
    )

這個宏的作用是建立一個靜態的 QByteArrayData 結構體,該結構體包含了字串字面值的後設資料。再結合註釋我們推斷stringdata代表"MainWindow"字串,這裡似乎是儲存的類名MainWindow。從變數名qt_meta_stringdata_MainWindow推斷,這個變數應該就是儲存的元物件相關的字串字面量,但我們預設工程沒有元物件,我們在程式碼中加一個signal

//mainwindow.h
signals:
    void testSignal();

重新編譯,可以看到,qt_meta_stringdata_MainWindow變數的初始化有所改變,從註釋看明顯包含了我們所加訊號的名稱:

//moc_mainwindow.cpp
static const qt_meta_stringdata_MainWindow_t qt_meta_stringdata_MainWindow = {
    {
QT_MOC_LITERAL(0, 0, 10), // "MainWindow"
QT_MOC_LITERAL(1, 11, 10), // "testSignal"
QT_MOC_LITERAL(2, 22, 0) // ""
    },
    "MainWindow\0testSignal\0"
};

對於const uint *data = qt_meta_data_MainWindow;

moc檔案中找到qt_meta_data_MainWindow定義,它是一個uint陣列,目前還看不出它的作用。

//moc_mainwindow.cpp
static const uint qt_meta_data_MainWindow[] = {
 // content:
       8,       // revision
       0,       // classname
       0,    0, // classinfo
       0,    0, // methods
       0,    0, // properties
       0,    0, // enums/sets
       0,    0, // constructors
       0,       // flags
       0,       // signalCount
       0        // eod
};

對於StaticMetacallFunction static_metacall = qt_static_metacall;

moc檔案裡找到qt_static_metacall定義,如果是預設工程,似乎也不做什麼:

//moc_mainwindow.cpp
void MainWindow::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
{
    Q_UNUSED(_o);
    Q_UNUSED(_id);
    Q_UNUSED(_c);
    Q_UNUSED(_a);
}

對於const QMetaObject * const *relatedMetaObjects = nullptr;void *extradata = nullptr;暫時不討論。

我們目前找到了staticMetaObject初始化的位置,知道它被賦值了一些資料結構,這些資料結構都和moc相關。

QMetaObject其他成員

回過頭來,我們看看QMetaObject的其他成員。

//qobjectdefs.h
struct Q_CORE_EXPORT QMetaObject
{
    class Connection;
	//...
}

class Q_CORE_EXPORT QMetaObject::Connection {
    //...
};

ConnectionQMetaObject的內部類,文件描述:

Represents a handle to a signal-slot (or signal-functor) connection.

它代表了訊號-槽的連線,那就是說我們平常使用的connect都和它相關,是個非常重要的角色。

我們可以看看我們一般使用的connect的定義:

//qobject.h
template <typename Func1, typename Func2>
    static inline typename std::enable_if<QtPrivate::FunctionPointer<Func2>::ArgumentCount == -1, QMetaObject::Connection>::type
            connect(/*...*/)
    {
        //...
        return connectImpl(/*...*/);
    }

呼叫了connectImpl()

//qobject.h
static QMetaObject::Connection connectImpl(/*...*/);

的確是返回了QMetaObject::Connection,由此可見Connection是訊號-槽系統的關鍵角色,它代表了一個建立的連線。

再看看其他介面:

//qobjectdefs.h
struct Q_CORE_EXPORT QMetaObject
{
	//...
    //基本資訊
    const char *className() const;
    const QMetaObject *superClass() const;
    bool inherits(const QMetaObject *metaObject) const Q_DECL_NOEXCEPT;
    //和類資訊相關
    int classInfoOffset() const;
    int classInfoCount() const;
    int indexOfClassInfo(const char *name) const;
    QMetaClassInfo classInfo(int index) const;
    //和方法相關
    int methodOffset() const;
    int methodCount() const;
    int indexOfMethod(const char *method) const;
    QMetaMethod method(int index) const;
    //和列舉相關
    int enumeratorOffset() const;
    int enumeratorCount() const;
    int indexOfEnumerator(const char *name) const;
    QMetaEnum enumerator(int index) const;
	//和屬性相關
    int propertyOffset() const;
    int propertyCount() const;
    int indexOfProperty(const char *name) const;
    QMetaProperty property(int index) const;
    QMetaProperty userProperty() const;
	//和構造器相關
    int constructorCount() const;
    int indexOfConstructor(const char *constructor) const;
    QMetaMethod constructor(int index) const;
	//和訊號、槽相關
    int indexOfSignal(const char *signal) const;
    int indexOfSlot(const char *slot) const;
    static bool checkConnectArgs(const char *signal, const char *method);
    static bool checkConnectArgs(const QMetaMethod &signal,
                                 const QMetaMethod &method);
    static QByteArray normalizedSignature(const char *method);
    static QByteArray normalizedType(const char *type);
    //...
}


這些方法幾乎提供了獲取所有"元成員"資訊的方式(好玩的是原始碼作者強迫症一樣地把功能類似的方法放到了一起),包括構造器、方法、屬性等,之所以說“元成員”,是因為被Q_INVOKABLEQ_PROPERTY等宏修飾的成員才具有"元能力"(當然,這也是後話了)。熟悉其他語言中反射特性的同學應該對這些方法的構成和名字比較熟悉,元物件系統的確為Qt提供了類似反射的能力。

接下來是和訊號-槽相關的介面:

//qobjectdefs.h
struct Q_CORE_EXPORT QMetaObject
{
    // internal index-based connect
    static Connection connect(const QObject *sender, int signal_index,
                              const QObject *receiver, int method_index,
                              int type = 0, int *types = nullptr);
    // internal index-based disconnect
    static bool disconnect(const QObject *sender, int signal_index,
                           const QObject *receiver, int method_index);
    //...
    // internal index-based signal activation
    static void activate(QObject *sender, int signal_index, void **argv);
    //...
}


從註釋來看,這些介面用於內部,是以索引為基礎的一些方法,暫時沒接觸到它們使用的場景。

接下來是很多過載或者模板的invokeMethod()

//qobjectdefs.h
struct Q_CORE_EXPORT QMetaObject
{
    //...
    invokeMethod(/*...*/);
    //...
}

官方文件說明:

Invokes the member (a signal or a slot name) on the object obj

看來是用於呼叫obj的訊號或者槽。

接下來是newInstance()

//qobjectdefs.h
struct Q_CORE_EXPORT QMetaObject
{
    //...
    QObject *newInstance(/*...*/);
    //...
}

它是用來呼叫建構函式的。

總結

熱身就到這裡,總結一下,Q_OBJECT宏用於啟用元物件特性,其中staticMetaObject的初始化在moc_xxx.cpp中進行,moc_xxx.cpp包含了許多“元成員”的字串資訊和實現。QMetaObject是元物件系統的關鍵成員,提供了元資訊的介面。

相關文章