QT元对象系统(中)

发布于 2021-09-20  70 次阅读


详解MOC的生成过程

书接上文

上文我们提到了Q_OBJECT这个宏,其展开如下:

#define Q_OBJECT \  
public: \  
Q_OBJECT_CHECK \  
static const QMetaObject staticMetaObject; \  
virtual const QMetaObject *metaObject() const; \  
virtual void *qt_metacast(const char *); \  
QT_TR_FUNCTIONS \  
virtual int qt_metacall(QMetaObject::Call, int, void **); \  

其中的几个virtual函数是从QObject继承而来。在探究MOC都生成了啥之前,先来看看QMetaObject究竟提供了哪些信息。

QMetaObject

直接看看类的定义:

struct Q_CORE_EXPORT QMetaObject
{
      const char *className() const;
      const QMetaObject *superClass() const;
    
   struct { // private data
        const QMetaObject *superdata; //父类QMetaObject实例的指针
        const char *stringdata;  // 字符串数组,保存类的 类名,槽函数名  信号函数名等 字符串信息
        const uint *data;  /该数组是个预定义的复合数据结构,由QMetaObjectPrivate 类提供管理,保存了类的基本信息,和一些索引值
        const void *extradata; //额外字段,暂未使用
     } d;
   ...
};

其中这个uint *data,实际上是一个指向 QMetaObjectPrivate 类型的指针, QMetaObjectPrivate是QMetaObject的私有实现类,该数据结构全是int类型,一些是直接的int型信息,比如classInfoCount、 methodCount等,还有一些是用于在QMetaObject的stringdata和data内存块中定位信息的索引值。其定义如下:

struct QMetaObjectPrivate
{
    int revision;
    int className;
    int classInfoCount, classInfoData;
    int methodCount, methodData;
    int propertyCount, propertyData;
    int enumeratorCount, enumeratorData;
    int constructorCount, constructorData; //since revision 2
    int flags; //since revision 3
    int signalCount; //since revision 4
    // revision 5 introduces changes in normalized signatures, no new members
    // revision 6 added qt_static_metacall as a member of each Q_OBJECT and inside QMetaObject itself
};

结合QMetaObjectPrivate和 QMetaObject 中的stringdata,我们就可以进行字符串和int索引的双向关联了。

QMetaObjectPrivate 提供了搜索方法,如getIndexOfSlot,传入一个字符串,代表slot的名称,之后它会从slot段开始搜索这个字符串,找到之后就返回索引。

拿到这个索引之后的思路就很简单了,弄一个switch,依照获取的索引调用不同的函数即可。

一个实际的例子

#include <QObject>  
    class TestObject : public QObject  
    {  
        Q_OBJECT  
        Q_PROPERTY(QString propertyA  READ getPropertyA WRITE getPropertyA RESET resetPropertyA DESIGNABLE true SCRIPTABLE true STORED true USER false)  
        Q_PROPERTY(QString propertyB  READ getPropertyB WRITE getPropertyB RESET resetPropertyB)  
        Q_CLASSINFO("Author", "Long Huihu")  
        Q_CLASSINFO("Version", "TestObjectV1.0")  
        Q_ENUMS(TestEnum)  
    public:  
        enum TestEnum {  
            EnumValueA,  
            EnumValueB  
        };  
    public:  
        TestObject();  
    signals:  
        void clicked();  
        void pressed();  
    public slots:  
        void onEventA(const QString &);  
        void onEventB(int );  
    }

QT的思路继承了C++的,不为自己不需要的功能买单。

如果需要向一个类添加自定义的MetaData,需要使用宏来显式地添加。

这里就手动添加了五个属性,包括两个字符串属性,两个类的信息属性,以及一个类内的枚举类型属性。

MOC会解析这些宏,并添加到 QMetaObject 中。

下面就是经过MOC处理之后的文件,咱一段一段看:

1、索引信息

static const uint qt_meta_data_TestObject[] = {  
     // content:  
           4,       // revision  
           0,       // classname  
           2,   14, // classinfo  
           4,   18, // methods  
           2,   38, // properties  
           1,   44, // enums/sets  
           0,    0, // constructors  
           0,       // flags  
           2,       // signalCount  
     // classinfo: key, value  
          22,   11,  
          44,   29,  
     // signals: signature, parameters, type, tag, flags  
          53,   52,   52,   52, 0x05,  
          63,   52,   52,   52, 0x05,  
     // slots: signature, parameters, type, tag, flags  
          73,   52,   52,   52, 0x0a,  
          91,   52,   52,   52, 0x0a,  
     // properties: name, type, flags  
         113,  105, 0x0a095007,  
         123,  105, 0x0a095007,  
     // enums: name, flags, count, data  
         133, 0x0,    2,   48,  
     // enum data: key, value  
         142, uint(TestObject::EnumValueA),  
         153, uint(TestObject::EnumValueB),  
           0        // eod  
    };  

这里,static const uint qt_meta_data_TestObject[],这里就是生成了metadata的索引,即 QMetaObjectPrivate 。里头包含了各种索引,函数个数统计,以及一些内部的flag。

2、字符串信息

static const char qt_meta_stringdata_TestObject[] = {  
        "TestObject\0Long Huihu\0Author\0"  
        "TestObjectV1.0\0Version\0\0clicked()\0"  
        "pressed()\0onEventA(QString)\0onEventB(int)\0"  
        "QString\0propertyA\0propertyB\0TestEnum\0"  
        "EnumValueA\0EnumValueB\0"  
    };  

static const char qt_meta_stringdata_TestObject[],这里就存着字符串信息,可以看到里头有类的名称,槽函数的名称,手动注册的metaData等。前面的一些索引就是用来指向这里头的。

3、生成QMetaObject对象

const QMetaObject TestObject::staticMetaObject = {  
        { &QObject::staticMetaObject,
          qt_meta_stringdata_TestObject,  
          qt_meta_data_TestObject, 0 }  
 };  

利用了前面生成的两个信息,创建一个MetaObject对象,这就是我们这个类的元对象。

4、重写MetaObject操作函数

const QMetaObject &TestObject::getStaticMetaObject() { return staticMetaObject; }  

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

这部分不是重点,就是重写了获取对象MetaObject的函数。

5、重写metacast函数

void *TestObject::qt_metacast(const char *_clname)  {  
        if (!_clname) return 0;  
        if (!strcmp(_clname, qt_meta_stringdata_TestObject))  
            return static_cast<void*>(const_cast< TestObject*>(this));  
        return QObject::qt_metacast(_clname);  
}  

这个转换函数有点不太明白,具体就是传入类的名称,然后他会和自己的MetaObject的字符串部分进行比较。字符串信息的第一个就是类的名称。如果类名称匹配,就返回自己。

6、重点!!!meta_call

int TestObject::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) {  
            switch (_id) {  
            case 0: clicked(); break;  
            case 1: pressed(); break;  
            case 2: onEventA((*reinterpret_cast< const QString(*)>(_a[1]))); break;  
            case 3: onEventB((*reinterpret_cast< int(*)>(_a[1]))); break;  
            default: ;  
            }  
            _id -= 4;  
        }  
    #ifndef QT_NO_PROPERTIES  
          else if (_c == QMetaObject::ReadProperty) {  
            void *_v = _a[0];  
            switch (_id) {  
            case 0: *reinterpret_cast< QString*>(_v) = getPropertyA(); break;  
            case 1: *reinterpret_cast< QString*>(_v) = getPropertyB(); break;  
            }  
            _id -= 2;  
        } else if (_c == QMetaObject::WriteProperty) {  
            void *_v = _a[0];  
            switch (_id) {  
            case 0: getPropertyA(*reinterpret_cast< QString*>(_v)); break;  
            case 1: getPropertyB(*reinterpret_cast< QString*>(_v)); break;  
            }  
            _id -= 2;  
        } else if (_c == QMetaObject::ResetProperty) {  
            switch (_id) {  
            case 0: resetPropertyA(); break;  
            case 1: resetPropertyB(); break;  
            }  
            _id -= 2;  
        } else if (_c == QMetaObject::QueryPropertyDesignable) {  
            _id -= 2;  
        } else if (_c == QMetaObject::QueryPropertyScriptable) {  
            _id -= 2;  
        } else if (_c == QMetaObject::QueryPropertyStored) {  
            _id -= 2;  
        } else if (_c == QMetaObject::QueryPropertyEditable) {  
            _id -= 2;  
        } else if (_c == QMetaObject::QueryPropertyUser) {  
            _id -= 2;  
        }  
    #endif // QT_NO_PROPERTIES  
        return _id;  
}  

这里是整个系统实现的核心。你发现有一堆的switch-case!

先看参数,传了一个QMetaObject::Call,看意思是用来区分call的类型的,一个metacall可以是调用一个函数、读取属性值,写入属性值,重置属性值等等。

之后传了一个int _id,我们怎么知道call的对象呢?就通过这个id。

我们直接看InvokeMetaMethod的部分。其中看到了老朋友,我们之前定义的信号函数和槽函数,这里就很有意思了,swicth根据传入的id,调用不同的函数,仅此而已。

结合之前的 字符串 属性和 索引,一切豁然开朗。通过字符串拿到函数的索引,然后使用meta_call,根据索引就知道要调哪个函数了。

获取属性的时候也是同理,根据字符串找到属性的索引,然后用meta_call,就知道要操作的是哪个了。

最后传入的是一个双重指针,这就对应的是函数的参数,注意这里的参数调用,MOC会根据槽函数的参数类型对这个指向参数的指针进行转换,最后达到传递参数的目的。

7、实现signal

    // SIGNAL 0  
    void TestObject::clicked()  
    {  
        QMetaObject::activate(this, &staticMetaObject, 0, 0);  
    }  
    // SIGNAL 1  
    void TestObject::pressed()  
    {  
        QMetaObject::activate(this, &staticMetaObject, 1, 0);  
    }  

在定义类的时候,或许会奇怪信号是居然不需要我们实现?这里就解答了,MOC会帮你实现,信号实际上就是调用了一个函数。


至此我们已经对MOC的基本原理有了了解,这只是元对象系统的底层静态部分,至于在程序中动态的调用connect,以及信号和槽之间是如何响应的,请看下集。


当其他人都认为你要鸽的时候,你鸽了,亦是一种不鸽