Pull to refresh

Динамические метаобъекты (part 1, изучение)

Reading time8 min
Views9K

Предисловие


Надеюсь, всем, кто использовал в разработке Qt, было интересно узнать, как же устроена метаинформация и что же происходит внутри этого прекрасного фреймворка? Об этом и будет эта запись — мы заглянем внутрь исходников и попробуем написать реализацию динамического метаобъекта (но не в этой записи). Метаобъекта, в котором можно создавать сигнали и слоты в realtime.

Многие скажут, что все уже реализовано (если недоступно: можно найти в кеше гугла). Но с такой реализацией мы не сможем сделать QObject::connect . Ценность такой реализации будет стремиться к нулю.

Немного изучения


Итак, для начала мы рассмотрим содержимое класса QObject. Зачем? Все классы с метаинформацией должны быть наследниками QObject и ещё иметь макрос Q_OBJECT, чтобы moc сгенерировал метаинформацию.

Код из Qt буду копировать с официального сайта. Использовать буду Qt 5.4.

Итак, само объявление класса выглядит так:

Код класса QObject
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<QObjectData> d_ptr;

    static const QMetaObject staticQtMetaObject;
///пропускаем все остальное
}


В тоже самое время можно создать проект с простым классом A

#include <QObject>

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

Но среди всего этого надо обратить внимание на сам метаобъект, и из чего он состоит.
текст MOC
///Пропускаем лишнее

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<A *>(_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<int*>(_a[0]) = -1;
        _id -= 2;
    }
    return _id;
}

// SIGNAL 0
void A::signal()
{
    QMetaObject::activate(this, &staticMetaObject, 0, Q_NULLPTR);
}
QT_END_MOC_NAMESPACE



Итак, из увиденного можно сделать несколько выводов: наработки для динамических метаобъектов есть переменная QDynamicMetaObjectData * QObjectData::metaObject и функция QMetaObject * QObjectData::dynamicMetaObject() const. Следовательно, осталось узнать, как с ними работать и как с ними работают Qt.

Пропуская занудное чтение исходников скажу сразу: нам даже оставили классы для создания динамических метаобъектов.

текст 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
};
///остальное нам тоже не интересно


Итак, что у нас выходит. Если мы создадим новый метаобъект и сохраним его в QObject::d_ptr->metaObject в каком либо наследнике QObject, то мимо нас не пройдет ни одно обращение к сигналам и слотам (кстати, отличный инструмент для отлаживания сигналов и слотов можно сделать), а так же можно занять место под свои сигналы и слоты. В общем, сделать все, что поддержит наше больное воображение, но меня больше вдохновляло создание метаобъекта, которому можно было бы добавлять и сигналы, и слоты, поэтому я освещу здесь именно подготовку к созданию такого метаобъекта.

Боремся с ленью и собираем информацию о задаче


Итак, чтобы сделать свой метаобъект, надо посмотреть, как метаобъект вообще устроен. Для этого снова лезем в исходники и находим это:

Структура метаобъекта
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;
}


Отсюда, и из листинга с MOC генератора, видно, что для валидного метаобъекта требуется заполнить только 2 переменные: stringdata и data, либо полностью переписывать все функции класса QMetaObject . Из 2-х зол я выбрал меньшее — решил заполнить эти данные, потому что поиск по этим данным будет проводиться средствами Qt и искать он будет никак не медленнее обычных метаобъектов (да, это преждевременная оптимизация).

Для начала давайте рассмотрим самое легкое — строковую информацию. MOC нам дает вот такой код для нашего тестового класса A:

Строковый массив
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


Т.е. там всего-то навсего массив QByteArrayData, который содержит относительные ссылки на строки (относительно самого QByteArrayData). Таким образом, мы спокойно можем разместить каждую строку в памяти отдельно, а не вместе, как сделал это MOC.

Теперь давайте обратимся к основной метаинформации, где MOC нам приготовил большой uint массив.

Большой uint массив
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
};


Разделим его на 3 блока. 1-й блок у нас представляет обычный класс QMetaObjectPrivate:

QMetaObjectPrivate основное

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
///класс там продолжается и дальше, но дальше нам не интересно
}


Соответствия, что чему равно из первого блока провести не составляет труда. 2-й блок чуточку посложнее. Там получается массив из структур (в Qt такой структуры не описано, что весьма странно, поэтому заведем свою — DataMethodInfo):

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


С этим все понятно. А вот описание аргументов намного веселее. Вначале идет тип, который метод должен вернуть, и чаще всего это бывает QMetaType::Void. Далее идет перечисление всех типов аргументов. А именно, если у нас метод QString testString (QString src, QString dst), то будет лежать 2 QMetaType::QString. Если же метод не имеет аргументов, то ничего и не заполняем. А после перечисления типов аргументов идет список имен этих аргументов. Таким образов для нашего метода QString testString( QString src, QString dst ) код метадаты будет такой:

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
};

Я мог ошибиться в подсчете offset для аргументов, но смысл, думаю, понятен? Вставив этот код вместо того, что сделал MOC, можно добавить метод testString в наш метаобъект класса A. Вызвать его, правда, не получиться, но в списке значиться он будет. И будет иметь свой уникальный id.

Осталось только написать код, который будет генерировать все это с каких-нибудь наших данных. Если будет интерес, в следующем выпуске покажу, как написать вполне рабочий динамический метаобъект.
Tags:
Hubs:
+14
Comments5

Articles