0
点赞
收藏
分享

微信扫一扫

深入探索Qt框架系列之元对象编译器

四月Ren间 2024-05-29 阅读 16

上一篇文章简单介绍了Qt框架中的三大编译器(MOC、UIC、RCC),其中我认为最核心,最重要的就是元对象编译器(MOC),下面我们将深入探索MOC生成的代码,并逐步解析。

本文将以下面的源码来解析MOC生成了哪些代码。
test_moc.h:

#ifndef TEST_MOC_H
#define TEST_MOC_H

#include <QDebug>
#include <QObject>

class test_moc : public QObject {
    Q_OBJECT
public:
    test_moc(QObject* parent = nullptr)
        : QObject(parent)
    {
    }

public slots:
    void on_TestSlot() { qDebug() << __FUNCTION__; }
    void on_TestSlot_Param(int num) { qDebug() << __FUNCTION__ << num; }

signals:
    void sigTestSignals();
    void sigTestSignals_Param(int num);
};

#endif // TEST_MOC_H

main.cpp:

#include "test_moc.h"

#include <QApplication>

int main(int argc, char* argv[])
{
    QApplication a(argc, argv);

    test_moc m1, m2;
    QObject::connect(&m1, SIGNAL(sigTestSignals()), &m2, SLOT(on_TestSlot()));
    QObject::connect(&m2, SIGNAL(sigTestSignals_Param(int)), &m1, SLOT(on_TestSlot_Param(int)));

    return a.exec();
}

元对象编译器(MOC)

Qt信号槽和属性系统基于在运行时自省对象的能力,能够列出对象的方法和属性,并拥有有关它们的各种信息,例如参数类型。
因为C++本身是不支持运行时自省对象的能力的,因此Qt附带了一个工具来支持它。这个工具就是MOC。它是一个代码生成器(通过编译来生成新的代码)。
MOC解析头文件并生成一个附加的C++文件,该文件与程序的其余部分一起编译。生成的C++文件包含自省所需的所有信息。

Qt宏定义

在Qt程序中我们可以看到一些关键字不属于纯C++关键字:signalsslotsQ_OBJECTemitSIGNALSLOT。这些被称为C++的Qt扩展宏,它们实际上就是简单的宏,在qobjectdefs.h文件中定义。

signals,slots
#ifndef QT_ANNOTATE_ACCESS_SPECIFIER
# define QT_ANNOTATE_ACCESS_SPECIFIER(x)
#endif
...
# define Q_SLOTS QT_ANNOTATE_ACCESS_SPECIFIER(qt_slot)
# define Q_SIGNALS public QT_ANNOTATE_ACCESS_SPECIFIER(qt_signal)
...
#define slots Q_SLOTS
#define signals Q_SIGNALS

上面代码主要是定义了slotssignals两个宏,摘自qobjectdefs.h文件。
我们可以看到这两个宏展开后就是一个空宏,因为# define QT_ANNOTATE_ACCESS_SPECIFIER(x)定义的宏是一个空宏。

Q_OBJECT
#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, "")

Q_OBJECT定义了一堆函数和一个静态对象staticMetaObject,这些函数在MOC生成的文件中会有实现。

emit
#ifndef QT_NO_EMIT
# define emit
#endif

emit也是一个空宏。而且它不会被MOC解析,也就是说emit没有任何实际意义(除了用来给开发人员提示外)。

SIGNAL,SLOT
Q_CORE_EXPORT const char *qFlagLocation(const char *method);
# define QLOCATION "\0" __FILE__ ":" QT_STRINGIFY(__LINE__)
...
# define SLOT(a)     qFlagLocation("1"#a QLOCATION)
# define SIGNAL(a)   qFlagLocation("2"#a QLOCATION)

这两个宏只是使用预处理器将参数转换为字符串,并在前面添加一段代码。
在调试模式下,如果信号连接不起作用,会以警告的形式输出代码的具体位置,QLOCATION宏就是对应的位置字符串。

MOC生成的代码

上面示例代码通过MOC生成的代码将逐一展示并解释。

字符串表
struct qt_meta_stringdata_test_moc_t {
    QByteArrayData data[7];
    char stringdata0[80];
};
#define QT_MOC_LITERAL(idx, ofs, len) \
    Q_STATIC_BYTE_ARRAY_DATA_HEADER_INITIALIZER_WITH_OFFSET(len, \
    qptrdiff(offsetof(qt_meta_stringdata_test_moc_t, stringdata0) + ofs \
        - idx * sizeof(QByteArrayData)) \
    )
static const qt_meta_stringdata_test_moc_t qt_meta_stringdata_test_moc = {
    {
QT_MOC_LITERAL(0, 0, 8), // "test_moc"
QT_MOC_LITERAL(1, 9, 14), // "sigTestSignals"
QT_MOC_LITERAL(2, 24, 0), // ""
QT_MOC_LITERAL(3, 25, 20), // "sigTestSignals_Param"
QT_MOC_LITERAL(4, 46, 3), // "num"
QT_MOC_LITERAL(5, 50, 11), // "on_TestSlot"
QT_MOC_LITERAL(6, 62, 17) // "on_TestSlot_Param"

    },
    "test_moc\0sigTestSignals\0\0sigTestSignals_Param\0"
    "num\0on_TestSlot\0on_TestSlot_Param"
};
#undef QT_MOC_LITERAL

这段代码的核心是初始化静态的qt_meta_stringdata_test_moc对象,其中QByteArrayData数组是用来存储类名、信号和槽的名字。QT_MOC_LITERAL宏用来创建一个静态QByteArray,它引用下面字符串中的索引值。qt_meta_stringdata_test_moc_t::stringdata0数据就是存储类名、信号和槽的名字、参数名的字符串表。

自省表
static const uint qt_meta_data_test_moc[] = {

 // content:
       8,       // revision
       0,       // classname
       0,    0, // classinfo
       4,   14, // methods
       0,    0, // properties
       0,    0, // enums/sets
       0,    0, // constructors
       0,       // flags
       2,       // signalCount

 // signals: name, argc, parameters, tag, flags
       1,    0,   34,    2, 0x06 /* Public */,
       3,    1,   35,    2, 0x06 /* Public */,

 // slots: name, argc, parameters, tag, flags
       5,    0,   38,    2, 0x0a /* Public */,
       6,    1,   39,    2, 0x0a /* Public */,

 // signals: parameters
    QMetaType::Void,
    QMetaType::Void, QMetaType::Int,    4,

 // slots: parameters
    QMetaType::Void,
    QMetaType::Void, QMetaType::Int,    4,

       0        // eod
};

content部分中,每行有一个值或两个值组成,如果有两个值,那么第一个值代表计数,第二个值代表该数组中描述开始的索引。比如方法methods有四个,分别是两个槽函数和两个信号,所以methods的第一个值是4。前面content包含了索引0~13,第14开始是信号和槽,所以methods的第二个值是14。
方法(信号和槽)字段由5个int组成:

  • 第一个是名称

这里的值对应的是上面字符串表中的索引值,比如当前信号名称是sigTestSignals,对应字符串表中的索引是1。

  • 第二个是参数的数量
  • 第三个是参数描述的索引,该索引对应的是当前自省表中的索引值(即后面parameters部分对应的索引)
  • 后面两个值先忽略

调用信号和槽
void test_moc::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
{
    if (_c == QMetaObject::InvokeMetaMethod) {
        auto *_t = static_cast<test_moc *>(_o);
        Q_UNUSED(_t)
        switch (_id) {
        case 0: _t->sigTestSignals(); break;
        case 1: _t->sigTestSignals_Param((*reinterpret_cast< int(*)>(_a[1]))); break;
        case 2: _t->on_TestSlot(); break;
        case 3: _t->on_TestSlot_Param((*reinterpret_cast< int(*)>(_a[1]))); break;
        default: ;
        }
    } else if (_c == QMetaObject::IndexOfMethod) {
        int *result = reinterpret_cast<int *>(_a[0]);
        {
            using _t = void (test_moc::*)();
            if (*reinterpret_cast<_t *>(_a[1]) == static_cast<_t>(&test_moc::sigTestSignals)) {
                *result = 0;
                return;
            }
        }
        {
            using _t = void (test_moc::*)(int );
            if (*reinterpret_cast<_t *>(_a[1]) == static_cast<_t>(&test_moc::sigTestSignals_Param)) {
                *result = 1;
                return;
            }
        }
    }
}

这段代码分为两部分:

  1. 调用信号和槽

_c == QMetaObject::InvokeMetaMethod时,通过id索引对应的方法,然后调用。

  1. 信号索引

_c == QMetaObject::IndexOfMethod时,通过_a来匹配信号,并返回对应信号的索引值(result)。

QMetaObject
QT_INIT_METAOBJECT const QMetaObject test_moc::staticMetaObject = { {
    QMetaObject::SuperData::link<QObject::staticMetaObject>(),
    qt_meta_stringdata_test_moc.data,
    qt_meta_data_test_moc,
    qt_static_metacall,
    nullptr,
    nullptr
} };


const QMetaObject *test_moc::metaObject() const
{
    return QObject::d_ptr->metaObject ? QObject::d_ptr->dynamicMetaObject() : &staticMetaObject;
}

void *test_moc::qt_metacast(const char *_clname)
{
    if (!_clname) return nullptr;
    if (!strcmp(_clname, qt_meta_stringdata_test_moc.stringdata0))
        return static_cast<void*>(this);
    return QObject::qt_metacast(_clname);
}

int test_moc::qt_metacall(QMetaObject::Call _c, int _id, void **_a)
{
    _id = QObject::qt_metacall(_c, _id, _a);
    if (_id < 0)
        return _id;
    if (_c == QMetaObject::InvokeMetaMethod) {
        if (_id < 4)
            qt_static_metacall(this, _c, _id, _a);
        _id -= 4;
    } else if (_c == QMetaObject::RegisterMethodArgumentMetaType) {
        if (_id < 4)
            *reinterpret_cast<int*>(_a[0]) = -1;
        _id -= 4;
    }
    return _id;
}

这段代码实现了Q_OBJECT宏声明的接口和对象:test_moc::staticMetaObjectmetaObject()qt_metacast()qt_metacall()

  • staticMetaObject

这部分代码是用来初始化静态元对象的,主要包含字符串表中的索引数据(qt_meta_stringdata_test_moc.data)、自省表数据(qt_meta_data_test_moc)以及调用信号和槽的接口(qt_static_metacall)。

  • metaObject()

该函数是返回元对象,其中QObject::d_ptr->dynamicMetaObject()仅用于动态元对象(QML对象),一般情况下,该函数只返回staticMetaObject

  • qt_metacast()

该函数是Qt的动态类型转换工具,类似于C++中的dynamic_cast
调用该函数可以安全地将QObject或其派生类的实例向下转换为更具体的类型。

  • qt_metacall()

该函数是QObject的一个核心功能,它在运行时调用对象的方法(如信号、槽或其他通过Q_INVOKABLE宏定义的方法)。信号槽的调用就是通过该函数实现的。

信号
// SIGNAL 0
void test_moc::sigTestSignals()
{
    QMetaObject::activate(this, &staticMetaObject, 0, nullptr);
}

// SIGNAL 1
void test_moc::sigTestSignals_Param(int _t1)
{
    void *_a[] = { nullptr, const_cast<void*>(reinterpret_cast<const void*>(std::addressof(_t1))) };
    QMetaObject::activate(this, &staticMetaObject, 1, _a);
}

其实信号经过元对象编译后就是一个普通的函数,将该类的指针、静态元对象数据以及参数数组传递给QMetaObject::activate

举报

相关推荐

0 条评论