Qt хорошо известен своим механизмом сигналов и слотов. Но как это работает? В этом посте мы исследуем внутренности QObject и QMetaObject и раскроем их работу за кадром. Я буду давать примеры Qt5 кода, иногда отредактированные для краткости и добавления форматирования.
Сигналы и слоты
Для начала, вспомним, как выглядят сигналы и слоты, заглянув в официальный пример. Заголовочный файл выглядит так:
class Counter : public QObject
{
Q_OBJECT
int m_value;
public:
int value() const { return m_value; }
public slots:
void setValue(int value);
signals:
void valueChanged(int newValue);
};
Где-то, в .cpp файле, мы реализуем setValue():
void Counter::setValue(int value)
{
if (value != m_value) {
m_value = value;
emit valueChanged(value);
}
}
Затем, можем использовать объект Counter таким образом:
Counter a, b;
QObject::connect(&a, SIGNAL(valueChanged(int)), &b, SLOT(setValue(int)));
a.setValue(12); // a.value() == 12, b.value() == 12
Это оригинальный синаксис, который почти не изменялся с начала Qt в 1992 году. Но даже если базовое API не было изменено, реализация же менялась несколько раз. Под капотом добавлялись новые возможности и происходили другие вещи. Тут нет никакой магии и я покажу как это работает.
MOC или метаобъектный компилятор
Сигналы и слоты, а также система свойств Qt, основываются на возможностях самоанализа объектов во время выполнения программы. Самоанализ означает способность перечислить методы и свойства объекта и иметь всю информацию про них, в частности, о типах их аргументов. QtScript и QML вряд ли был бы возможны без этого.
C++ не предоставляет родной поддержки самоанализа, поэтому Qt поставляется с инструментом, который это обеспечивает. Этот инструмент называется MOC. Это кодогенератор (но не препроцессор, как думают некоторые люди).
Он парсит заголовочные файлы и генерирует дополнительный C++ файл, который компилируется с остальной частью программы. Этот сгенерированный C++ файл содержит всю информацию, необходимую для самоанализа.
Qt иногда подвергается критике со стороны языковых пуристов, так как это дополнительный генератор кода. Я позволю документации Qt ответить на эту критику. Нет ничего плохого в кодогенераторе и MOC является превосходным помощником.
Магические макросы
Сможете ли вы заметить ключевые слова, которые не являются ключевыми словами C++? signals, slots, Q_OBJECT, emit, SIGNAL, SLOT. Они известны как Qt-расширение для C++. На самом деле это простые макросы, которые определены в qobjectdefs.h.
#define signals public
#define slots /* nothing */
Это правда, сигналы и слоты являются простыми функциями: компилятор обрабатывает их как и любые другие функции. Макросы еще служат определённой цели: MOC видит их. Сигналы были в секции protected в Qt4 и ранее. Но в Qt5 они уже открыты, для поддержки нового синтаксиса.
#define Q_OBJECT
public:
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_DECL_HIDDEN static void qt_static_metacall(QObject *, QMetaObject::Call, int, void **);
Q_OBJECT определяет связку функций и статический QMetaObject. Эти функции реализованы в файле, сгенерированном MOC.
#define emit /* nothing */
emit – пустой макрос. Он даже не парсится MOC. Другими словами, emit опционален и ничего не значит (за исключением подсказки для разработчика).
Q_CORE_EXPORT const char *qFlagLocation(const char *method);
#ifndef QT_NO_DEBUG
# define QLOCATION "" __FILE__ ":" QTOSTRING(__LINE__)
# define SLOT(a) qFlagLocation("1"#a QLOCATION)
# define SIGNAL(a) qFlagLocation("2"#a QLOCATION)
#else
# define SLOT(a) "1"#a
# define SIGNAL(a) "2"#a
#endif
Эти макросы просто используются препроцессором для конвертации параметра в строку и добавления кода в начале. В режиме отладки мы также дополняем строку с расположением файла предупреждением, если соединение с сигналом не работает. Это было добавлено в Qt 4.5 для совместимости. Для того, чтобы узнать, какие строки содержат информацию о строке, мы используем qFlagLocation, которая регистрирует адрес строки в таблице, с двумя включениями.
Теперь перейдём к коду, сгенерированному MOC.
QMetaObject
const QMetaObject Counter::staticMetaObject = {
{ &QObject::staticMetaObject, qt_meta_stringdata_Counter.data,
qt_meta_data_Counter, qt_static_metacall, 0, 0 }
};
const QMetaObject *Counter::metaObject() const
{
return QObject::d_ptr->metaObject ? QObject::d_ptr->dynamicMetaObject() : &staticMetaObject;
}
Тут мы видим реализацию Counter::metaObject() и Counter::staticMetaObject. Они объявленый в макросе Q_OBJECT. QObject::d_ptr->metaObject используется только для динамических метаобъектов (QML объекты), поэтому, в общем случае, виртуальная функция metaObject() просто возращает staticMetaObject класса. staticMetaObject построен с данными только для чтения. QMetaObject определён в qobjectdefs.h в виде:
struct QMetaObject
{
/* ... пропущены все открытые методы ... */
enum Call { InvokeMetaMethod, ReadProperty, WriteProperty, /*...*/ };
struct { // закрытые данные
const QMetaObject *superdata;
const QByteArrayData *stringdata;
const uint *data;
typedef void (*StaticMetacallFunction)(QObject *, QMetaObject::Call, int, void **);
StaticMetacallFunction static_metacall;
const QMetaObject **relatedMetaObjects;
void *extradata; // зарезервировано для будущего использования
} d;
};
d косвенно символизирует, что все члены должны быть сокрыты, но они не сокрыты для сохранение POD и возможности статической инициализации.
QMetaObject инициализируется с помощью метаобъекта родительского класса superdata (QObject::staticMetaObject в данном случае). stringdata и data инициализируются некоторыми данными, которые будут расмотрены далее. static_metacall это указатель на функцию, инициализируемый Counter::qt_static_metacall.
Таблицы самоанализа
Во-первых, давайте посмотрим на основные данные QMetaObject.
static const uint qt_meta_data_Counter[] = {
// 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, 1, 24, 2, 0x05,
// slots: name, argc, parameters, tag, flags
4, 1, 27, 2, 0x0a,
// signals: parameters
QMetaType::Void, QMetaType::Int, 3,
// slots: parameters
QMetaType::Void, QMetaType::Int, 5,
0 // eod
};
Первые 13 int составляют заголовок. Он предоставляет собой две колонки, первая колонка – это количество, а вторая – индекс массива, где начинается описание. В текущем случае мы имеем два метода, и описание методов начинается с индекса 14.
Описание метода состоит из 5 int. Первый – это имя, индекс в таблице строк (мы детально рассмотрим её позднее). Второе целое – количество параметров, вслед за которым идёт индекс, где мы может найти их описание. Сейчас мы будет игнорировать тег и флаги. Для каждой функции MOC также сохраняет возращаемый тип каждого параметра, их тип и индекс имени.
Таблица строк
struct qt_meta_stringdata_Counter_t {
QByteArrayData data[6];
char stringdata[47];
};
#define QT_MOC_LITERAL(idx, ofs, len)
Q_STATIC_BYTE_ARRAY_DATA_HEADER_INITIALIZER_WITH_OFFSET(len,
offsetof(qt_meta_stringdata_Counter_t, stringdata) + ofs
- idx * sizeof(QByteArrayData)
)
static const qt_meta_stringdata_Counter_t qt_meta_stringdata_Counter = {
{
QT_MOC_LITERAL(0, 0, 7),
QT_MOC_LITERAL(1, 8, 12),
QT_MOC_LITERAL(2, 21, 0),
QT_MOC_LITERAL(3, 22, 8),
QT_MOC_LITERAL(4, 31, 8),
QT_MOC_LITERAL(5, 40, 5)
},
""CountervalueChangednewValuesetValue""
""value""
};
#undef QT_MOC_LITERAL
В основном, это статический массив QByteArray (создаваемый макросом QT_MOC_LITERAL), который ссылается на конкретный индекс в строке ниже.
Сигналы
MOC также реализует сигналы. Они являются функциями, которые просто создают массив указателей на аргументы и передают их QMetaObject::activate. Первый элемент массива это возращаемое значение. В нашем примере это 0, потому что возращаемое значение void. Третий аргумент, передаваемый функции для активации, это индекс сигнала (0 в данном случае).
// SIGNAL 0
void Counter::valueChanged(int _t1)
{
void *_a[] = { 0, const_cast<void*>(reinterpret_cast<const void*>(&_t1)) };
QMetaObject::activate(this, &staticMetaObject, 0, _a);
}
Вызов слота
Также возможно вызвать слот по его индексу, используя функцию qt_static_metacall:
void Counter::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
{
if (_c == QMetaObject::InvokeMetaMethod) {
Counter *_t = static_cast<Counter *>(_o);
switch (_id) {
case 0: _t->valueChanged((*reinterpret_cast< int(*)>(_a[1]))); break;
case 1: _t->setValue((*reinterpret_cast< int(*)>(_a[1]))); break;
default: ;
}
...
}
...
}
Массив указателей на аргументы в таком же формате, как и в сигналах. _a[0] не тронут, потому что везде тут возращается void.
Примечение по поводу индексов
Для каждого QMetaObject, сигналам, слотам и прочим вызываемым методам объекта, даются индексы, начинающиеся с 0. Они упорядочены так, что на первом месте сигналы, затем слоты и затем уже прочие методы. Эти индексы внутри называется относительными индексами. Они не включают индексы родителей. Но в общем, мы не хотим знать более глобальный индекс, который не относится к конкретному классу, но включает все прочие методы в цепочке наследования. Поэтому, мы просто добавляем смещение к относительному индексу и получаем абсолютный индекс. Этот индекс, используемый в публичном API, возращается функциями вида QMetaObject::indexOf{Signal,Slot,Method}.
Механизм соединения использует массив, индексированный для сигналов. Но все слоты занимают место в этом массиве и обычно слотов больше чем сигналов. Так что, с Qt 4.6, появляется новый внутренный индекс для сигналов, который включает только индексы, используемые для сигналов. Если вы разрабатываете с Qt, вам нужно знать только про абсолютный индекс для методов. Но пока вы просматриваете исходный код QObject, вы должны знать разницу между этими тремя индексами.
Как работает соединение
Первое, что делает Qt при соединении, это ищет индексы сигнала и слота. Qt будет просматривать таблицы строк метаобъекта в поисках соответствующих индексов. Затем, создается и добавляется во внутренние списки объект QObjectPrivate::Connection.
Какая информация необходима для хранения каждого соединения? Нам нужен способ быстрого доступа к соединению для данного индекса сигнала. Так как могут быть несколько слотов, присоединённых к одному и тому же сигналу, нам нужно для каждого сигнала иметь список присоединённых слотов. Каждое соединение должно содержать объект-получатель и индекс слота. Мы также хотим, чтобы соединения автоматически удалялись, при удалении получателя, поэтому каждый объект-получатель должен знать, кто соединён с ним, чтобы он мог удалить соединение.
Вот QObjectPrivate::Connection, определённый в qobject_p.h:
struct QObjectPrivate::Connection
{
QObject *sender;
QObject *receiver;
union {
StaticMetaCallFunction callFunction;
QtPrivate::QSlotObjectBase *slotObj;
};
// указатель на следующий односвязный список ConnectionList
Connection *nextConnectionList;
// связные списки отправителей
Connection *next;
Connection **prev;
QAtomicPointer<const int> argumentTypes;
QAtomicInt ref_;
ushort method_offset;
ushort method_relative;
uint signal_index : 27; // в диапазоне сигналов (смотрите QObjectPrivate::signalIndex())
ushort connectionType : 3; // 0 == auto, 1 == direct, 2 == queued, 4 == blocking
ushort isSlotObject : 1;
ushort ownArgumentTypes : 1;
Connection() : nextConnectionList(0), ref_(2), ownArgumentTypes(true) {
// ref_ 2 для использования во внутренних списках и в QMetaObject::Connection
}
~Connection();
int method() const { return method_offset + method_relative; }
void ref() { ref_.ref(); }
void deref() {
if (!ref_.deref()) {
Q_ASSERT(!receiver);
delete this;
}
}
};
Каждый объект имеет массив соединений: это массив, который связывает каждого сигнала списки QObjectPrivate::Connection. Каждый объект также имеет обратные списки соединений объектов, подключённых для автоматического удаления. Это двусвязный список.
Связные списки используются для возможности быстрого добавления и удаления объектов. Они реализованы с наличием указателей на следующий/предыдущий узел внутри QObjectPrivate::Connection. Заметьте, что указатель prev из senderList это указатель на указатель. Это потому что мы действительно не указываем на предыдущий узел, а, скорее, на следующий, в предыдущем узле. Этот указатель используется только когда соединение разрушается. Это позволяет не иметь специальный случай для первого элемента.
Эмиссия сигнала
Когда мы вызываем сигнал, мы видели, что он вызывает код, сгенерированный MOC, который уже вызывает QMetaObject::activate. Вот реализация (с примечаниями) этого метода в qobject.cpp:
void QMetaObject::activate(QObject *sender, const QMetaObject *m, int local_signal_index,
void **argv)
{
/* тут просто продвигаемся к следующей функции, передавая смещение сигнала метаобъекта */
activate(sender, QMetaObjectPrivate::signalOffset(m), local_signal_index, argv);
}
void QMetaObject::activate(QObject *sender, int signalOffset, int local_signal_index, void **argv)
{
int signal_index = signalOffset + local_signal_index;
/* быстрая проверка битовой маски из 64 бит, если она 0, мы уверены, что ничего не соединено с этим сигналом и мы можем быстро выйти, что означает эмиссию сигнала без присоединённого слота очень быстрой */
if (!sender->d_func()->isSignalConnected(signal_index))
return; // ничего не соединено с сигналом
/* … пропущены некоторые отладочные и QML перехватчики, проверки данных ... */
/* захват мьютекса, так как все операции в connectionLists потокобезопасны */
QMutexLocker locker(signalSlotLock(sender));
/* получение connectionList из сигнала (упрощённая версия) */
QObjectConnectionListVector *connectionLists = sender->d_func()->connectionLists;
const QObjectPrivate::ConnectionList *list =
&connectionLists->at(signal_index);
QObjectPrivate::Connection *c = list->first;
if (!c) continue;
// мы должны проверить last, чтобы удостоверится, что сигналы добавленные во время эмиссии сигнала, не были вызваны
QObjectPrivate::Connection *last = list->last;
/* итерации, для каждого слота */
do {
if (!c->receiver)
continue;
QObject * const receiver = c->receiver;
const bool receiverInSameThread = QThread::currentThreadId() == receiver->d_func()->threadData->threadId;
// если это соединение должно быть отправлено немедленно, помещаем его в очередь событий
if ((c->connectionType == Qt::AutoConnection && !receiverInSameThread)
|| (c->connectionType == Qt::QueuedConnection)) {
/* базовое копирование аргументов и добавление события */
queued_activate(sender, signal_index, c, argv);
continue;
} else if (c->connectionType == Qt::BlockingQueuedConnection) {
/* ... пропущено ... */
continue;
}
/* вспомогательная структура, которая устанавливает sender() и сбрасывает обратно, когда покидается область видимости */
QConnectionSenderSwitcher sw;
if (receiverInSameThread)
sw.switchSender(receiver, sender, signal_index);
const QObjectPrivate::StaticMetaCallFunction callFunction = c->callFunction;
const int method_relative = c->method_relative;
if (c->isSlotObject) {
/* … пропущено … стиль Qt5 соединения через указатель на функцию ... */
} else if (callFunction && c->method_offset <= receiver->metaObject()->methodOffset()) {
/* если мы имеем callFunction (указатель на qt_static_metacall, сгенерированный MOC), мы её вызываем */
/* также необходима проверка, что сохранённый metodOffset действительный (мы можем вызвать из деструктора) */
locker.unlock(); // мы не можем сохранять блокировку во время вызова метода
callFunction(receiver, QMetaObject::InvokeMetaMethod, method_relative, argv);
locker.relock();
} else {
/* обходной путь для динамических объектов */
const int method = method_relative + c->method_offset;
locker.unlock();
metacall(receiver, QMetaObject::InvokeMetaMethod, method, argv);
locker.relock();
}
// проверка, что объект не был удалён через слот
if (connectionLists->orphaned) break;
} while (c != last && (c = c->nextConnectionList) != 0);
}
От переводчика: это была первая часть и традиционным будет вопрос о необходимости перевода второй части.
Автор: blackmaster