Dynamic meta objects (part 1, study)

Foreword


I hope everyone who used Qt in the development was interested to know how meta-information is arranged and what is happening inside this beautiful framework? This entry will be about this - we will look inside the sources and try to write an implementation of a dynamic meta object (but not in this entry). A meta object in which you can create signals and slots in realtime.

Many will say that everything has already been implemented (if not available: can be found in the Google cache ). But with such an implementation, we cannot do it QObject::connect . The value of such an implementation will tend to zero.

A little study


So, for a start we will consider the contents of the class QObject. What for? All classes with meta-information must be heirs QObjectand must also have a macro Q_OBJECTso that moc generates meta-information.

I will copy the code from Qt from the official site . I will use Qt 5.4.

So, the class declaration itself looks like this:

QObject Class Code
class Q_CORE_EXPORT QObjectData {
public:
    virtual ~QObjectData() = 0;
    QObject *q_ptr;
    QObject *parent;
    QObjectList children;
    uint isWidget : 1;
    uint blockSig : 1;
    uint wasDeleted : 1;
    uint isDeletingChildren : 1;
    uint sendChildEvents : 1;
    uint receiveChildEvents : 1;
    uint isWindow : 1; //for QWindow
    uint unused : 25;
    int postedEvents;
    QDynamicMetaObjectData *metaObject;
    QMetaObject *dynamicMetaObject() const;
};
class Q_CORE_EXPORT QObject
{
    Q_OBJECT
    Q_PROPERTY(QString objectName READ objectName WRITE setObjectName NOTIFY objectNameChanged)
    Q_DECLARE_PRIVATE(QObject)
///пропускаем лишнее
protected:
    QScopedPointer d_ptr;
    static const QMetaObject staticQtMetaObject;
///пропускаем все остальное
}


At the same time, you can create a project with a simple class A

#include 
class A : public QObject
{
    Q_OBJECT
public:
    explicit A(QObject *parent = 0);
    ~A();
signals:
    void signal();
public slots:
    void slot(){}
};

But among all this, one must pay attention to the metaobject itself, and what it consists of.
MOC text
///Пропускаем лишнее
QT_BEGIN_MOC_NAMESPACE
struct qt_meta_stringdata_A_t {
    QByteArrayData data[4];
    char stringdata[15];
};
#define QT_MOC_LITERAL(idx, ofs, len) \
    Q_STATIC_BYTE_ARRAY_DATA_HEADER_INITIALIZER_WITH_OFFSET(len, \
    qptrdiff(offsetof(qt_meta_stringdata_A_t, stringdata) + ofs \
        - idx * sizeof(QByteArrayData)) \
    )
static const qt_meta_stringdata_A_t qt_meta_stringdata_A = {
    {
QT_MOC_LITERAL(0, 0, 1), // "A"
QT_MOC_LITERAL(1, 2, 6), // "signal"
QT_MOC_LITERAL(2, 9, 0), // ""
QT_MOC_LITERAL(3, 10, 4) // "slot"
    },
    "A\0signal\0\0slot"
};
#undef QT_MOC_LITERAL
static const uint qt_meta_data_A[] = {
 // content:
       7,       // revision
       0,       // classname
       0,    0, // classinfo
       2,   14, // methods
       0,    0, // properties
       0,    0, // enums/sets
       0,    0, // constructors
       0,       // flags
       1,       // signalCount
 // signals: name, argc, parameters, tag, flags
       1,    0,   24,    2, 0x06 /* Public */,
 // slots: name, argc, parameters, tag, flags
       3,    0,   25,    2, 0x0a /* Public */,
 // signals: parameters
    QMetaType::Void,
 // slots: parameters
    QMetaType::Void,
       0        // eod
};
void A::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
{
    if (_c == QMetaObject::InvokeMetaMethod) {
        A *_t = static_cast(_o);
        switch (_id) {
        case 0: _t->signal(); break;
        case 1: _t->slot(); break;
        default: ;
        }
    } else if (_c == QMetaObject::IndexOfMethod) {
      ///код для поддержки нового синтаксиса сигналов и слотов
    }
    Q_UNUSED(_a);
}
///Обратите внимание именно сюда! Так и создается метаобъект!
const QMetaObject A::staticMetaObject = {
    { &QObject::staticMetaObject, qt_meta_stringdata_A.data,
      qt_meta_data_A,  qt_static_metacall, Q_NULLPTR, Q_NULLPTR}
};
const QMetaObject *A::metaObject() const
{
    return QObject::d_ptr->metaObject ? QObject::d_ptr->dynamicMetaObject() : &staticMetaObject;
}
int A::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 < 2)
            qt_static_metacall(this, _c, _id, _a);
        _id -= 2;
    } else if (_c == QMetaObject::RegisterMethodArgumentMetaType) {
        if (_id < 2)
            *reinterpret_cast(_a[0]) = -1;
        _id -= 2;
    }
    return _id;
}
// SIGNAL 0
void A::signal()
{
    QMetaObject::activate(this, &staticMetaObject, 0, Q_NULLPTR);
}
QT_END_MOC_NAMESPACE


So, from what we have seen, several conclusions can be drawn: the developments for dynamic metaobjects are a variable QDynamicMetaObjectData * QObjectData::metaObject and a function QMetaObject * QObjectData::dynamicMetaObject() const. Therefore, it remains to learn how to work with them and how Qt work with them.

Skipping the boring reading of the sources, I’ll say right away: we even left classes for creating dynamic meta objects.

text q_object_p.h
///пропускаем все лишнее
struct QAbstractDynamicMetaObject;
struct Q_CORE_EXPORT QDynamicMetaObjectData
{
    virtual ~QDynamicMetaObjectData() {}
    virtual void objectDestroyed(QObject *) { delete this; }
    virtual QAbstractDynamicMetaObject *toDynamicMetaObject(QObject *) = 0; ///вызывается при каждом вызове metaObject
    virtual int metaCall(QObject *, QMetaObject::Call, int _id, void **) = 0;///вызывается при обращении с сигналам/слотам/свойствам. 
};
///от этого класса и надо наследоваться, чтобы подделать метаобъект. 
struct Q_CORE_EXPORT QAbstractDynamicMetaObject : public QDynamicMetaObjectData, public QMetaObject
{
    virtual QAbstractDynamicMetaObject *toDynamicMetaObject(QObject *) { return this; }
    virtual int createProperty(const char *, const char *) { return -1; }///свойств мы создавать не можем. На всякий пожарный
    virtual int metaCall(QObject *, QMetaObject::Call c, int _id, void **a)
    { return metaCall(c, _id, a); }
    virtual int metaCall(QMetaObject::Call, int _id, void **) { return _id; } // Compat overload
};
///остальное нам тоже не интересно


So what comes out of us. If we create a new meta-object and save it in QObject::d_ptr->metaObjecta successor QObject, then not a single call to signals and slots will pass by (by the way, you can make a great tool for debugging signals and slots), and you can also take a place for your signals and slots. In general, to do everything that will support our sick imagination, but I was more inspired by the creation of a meta-object, which could add both signals and slots, so I will highlight here the preparations for creating such a meta-object.

We fight with laziness and collect information about the task


So, to make your meta-object, you need to see how the meta-object is generally arranged. To do this, we again climb into the sources and find this:

Meta Object Structure
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;
}


From here, and from the listing with the MOC generator, it is clear that for a valid meta-object, only 2 variables are required to be filled: stringdataand data, or, to completely rewrite all the functions of the class QMetaObject . Of the 2 evils, I chose the lesser - I decided to fill out this data, because the search on this data will be carried out by Qt and it will look no more slowly than ordinary meta objects (yes, this is a premature optimization).

First, let's look at the easiest thing - string information. MOC gives us this code for our test class A:

String array
struct qt_meta_stringdata_A_t {
    QByteArrayData data[4];
    char stringdata[15];
};
#define QT_MOC_LITERAL(idx, ofs, len) \
    Q_STATIC_BYTE_ARRAY_DATA_HEADER_INITIALIZER_WITH_OFFSET(len, \
    qptrdiff(offsetof(qt_meta_stringdata_A_t, stringdata) + ofs \
        - idx * sizeof(QByteArrayData)) \
    )
static const qt_meta_stringdata_A_t qt_meta_stringdata_A = {
    {
QT_MOC_LITERAL(0, 0, 1), // "A"
QT_MOC_LITERAL(1, 2, 6), // "signal"
QT_MOC_LITERAL(2, 9, 0), // ""
QT_MOC_LITERAL(3, 10, 4) // "slot"
    },
    "A\0signal\0\0slot"
};
#undef QT_MOC_LITERAL


Those. there is just an array QByteArrayDatathat contains relative references to strings (relative to itself QByteArrayData). Thus, we can safely place each line in memory separately, and not together, as MOC did.

Now let's turn to the main meta-information, where the MOC prepared a large uint array for us.

Big uint array
static const uint qt_meta_data_A[] = {
///1 блок
 // content:
       7,       // revision
       0,       // classname
       0,    0, // classinfo
       2,   14, // methods
       0,    0, // properties
       0,    0, // enums/sets
       0,    0, // constructors
       0,       // flags
       1,       // signalCount
///2 блок
 // signals: name, argc, parameters, tag, flags
       1,    0,   24,    2, 0x06 /* Public */,
 // slots: name, argc, parameters, tag, flags
       3,    0,   25,    2, 0x0a /* Public */,
///3 блок
 // signals: parameters
    QMetaType::Void,
 // slots: parameters
    QMetaType::Void,
       0        // eod
};


Divide it into 3 blocks. The 1st block we have is a regular class QMetaObjectPrivate:

QMetaObjectPrivate core

struct QMetaObjectPrivate
{
    enum { OutputRevision = 7 }; // Used by moc, qmetaobjectbuilder and qdbus
    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
    // revision 7 is Qt 5
///класс там продолжается и дальше, но дальше нам не интересно
}


Correspondence, which is equal to the first block, is not difficult. The 2nd block is a bit more complicated. There we get an array of structures (in Qt, such a structure is not described, which is very strange, so let's get our own one - DataMethodInfo):

DataMethodInfo
struct DataMethodInfo{
    uint name;/// номер имени метода ( в строковом массиве )
    uint argsCount; /// количество аргументов
    uint argOffset; /// offset информации о методах
    uint tag;/// увы, не понял
    uint flags;/// влияет на private/protected/public доступ и на что-то ещё, до конца не разобрался
};


With this, everything is clear. But the description of the arguments is much more fun. First comes the type that the method should return, and most often it happens QMetaType::Void. The following is a listing of all types of arguments. Namely, if we have a method QString testString (QString src, QString dst), then 2 QMetaType :: QString will lie. If the method has no arguments, then nothing is filled. And after listing the types of arguments, there is a list of the names of these arguments. Thus, for our method, QString testString( QString src, QString dst )the metadata code will be as follows:

static const qt_meta_stringdata_A_t qt_meta_stringdata_A = {
    {
QT_MOC_LITERAL(0, 0, 1), // "A"
QT_MOC_LITERAL(1, 2, 6), // "signal"
QT_MOC_LITERAL(2, 9, 0), // ""
QT_MOC_LITERAL(3, 10, 4) // "slot"
QT_MOC_LITERAL(4, 15, 10) // "testString"
QT_MOC_LITERAL(5, 26, 3) // "src"
QT_MOC_LITERAL(6, 30, 3) // "dst"
    },
    "A\0signal\0\0slot\0testString\0src\dst"
};
static const uint qt_meta_data_A[] = {
///1 блок
 // content:
       7,       // revision
       0,       // classname
       0,    0, // classinfo
       3,   14, // methods
       0,    0, // properties
       0,    0, // enums/sets
       0,    0, // constructors
       0,       // flags
       1,       // signalCount
///2 блок
 // signals: name, argc, parameters, tag, flags
       1,    0,   29,    2, 0x06 /* Public */,
 // slots: name, argc, parameters, tag, flags
       3,    0,   30,    2, 0x0a /* Public */,
       4,    2,   31,    2, 0x0a /* Public */,
///3 блок
 // signals: parameters
    QMetaType::Void,
 // slots: parameters
    QMetaType::Void,
////-----------------------------------------------------------------
///| return        |             Arguments Type            | names   |
QMetaType::QString , QMetaType::QString, QMetaType::QString, 5  ,   6  
       0        // eod
};

I could be wrong in calculating offset for arguments, but the meaning, I think, is clear? By inserting this code instead of what MOC did, you can add the testString method to our class A metaobject. However, it will not work, however, but it will appear in the list. And will have its own unique id.

It remains only to write code that will generate all this from some of our data. If there is interest, in the next issue I will show how to write a fully working dynamic meta object.

Also popular now: