Динамічні метаоб'єкти (part 1, вивчення)

Передмова
Сподіваюся, всім, хто використовував у розробці 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 *екстраданих; //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 такої структури не описано, що досить дивно, тому заведемо свою <code lang=«cpp:>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.

Залишилося тільки написати код, який буде генерувати все це з яких-небудь наших даних. Якщо буде інтерес, в наступному випуску покажу, як написати цілком робочий динамічний метаобъект.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.