Как работают сигналы и слоты в Qt (часть 1)

http://woboq.com/blog/how-qt-signals-slots-work.html
  • Перевод


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 "\0" __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)
    },
    ""Counter\0valueChanged\0\0newValue\0setValue\0""
    ""value\0""
};
#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);
}

UPD: перевод второй части тут.
Поделиться публикацией

Похожие публикации

Комментарии 29

    +7
    Вторая часть нужна, так как там новый синтаксис описан, с практической точки зрения вторая часть является даже более полезной, чем первая.
      +1
      P.S. перевод норм, хотя в некоторых местах всё-таки лучше проработать контекст:
      In order to know which strings have the line information
      вы перевели как
      Для того, чтобы узнать, какие строки содержат информацию о строке
      что не раскрывает суть. Там ещё есть несколько моментов, советую уделить подобному больше внимания.
      –6
      По опыту (правда этот опыт набирался на Qt4) emit все же как-то используется. Я встречался с ошибками, приводящими к SegmetationFault и решавшимися добавлением emit.
        +3
        emit это пустой макрос и влиять он никак не может. А segmentation fault отлавливаются специальными, а не шаманскими средствами ;)
          0
          Я в курсе, но valgrind ничего толкового не показывал, а падало все при входе в сигнал.
            +1
            Такое поведение характерно для ситуации, при которой используются release и debug сборки совместно
              0
              Допустим(я почти уверен, что не так, но было уже давно), ну как тогда объяснить, что добавление пустого макроса решило проблему?
                +2
                Понятия не имею. Вероятнее всего — «просто так, вот, случилось». Так бывает. Именно поэтому я и рекомендовал искать настоящую причину, т.к. от такого «исправления» она никуда не делась.
                  0
                  И так несколько раз в разных ситуациях? «Просто так вот получилось» — это не ответ. Да и шаманизма в программирование не бывает.

                  Как мы оба понимаем, что после этапа препроцессинга пустой макрос просто будет удален и на вход компилятору будет отдан один и тотже код, а значит и результат будет тот же самый. И ничего не должно измениться, но факт — изменилось.

                  Я не копался в моке, но что-то мне подсказывает, что отсутствие в доке кода без emit — это не случайно.
                    0
                    Шаманизма в программировании не бывает, а undefined behavior бывает.

                    Вообще, возможно, что-то не так собралось (make не пересобрал какой-то файл), а добавление emit изменило файл и инициировало пересборку.
                      0
                      С пересборкой возможно, это было уже года 3-4 назад, детали уже позабылись. К сожалению, провести тщательное расследование сейчас не представляется возможным, но мне почему-то помниться, как я убедился, что добавление emit-а убирает ошибку, а удаление — добавляет.
        0
        Кстати, кто какие знает системы позднего связывания и динамических возможностей в статически типизируемых компилируемых языках?
        Наиболее известные:
        1 Виртуальные фукнции С++
        2 Сообщения Objective C
        3 Сигналы и слоты Qt
        Что есть еще? Меня весьма интересует данная тема.
          0
          Еще есть динамические языки, где, условно, a.b === a[«b»] и оно как-то проксируется на хэш таблицу (дерево, список) с методами.
            0
            Ну с динамическими все понятно, там вообще eval() есть. Меня интересуют в основном статические.
            Хотя вы тоже правы, можно добавить четвертым пунктом рефлексию.
              0
              На C++ тоже можно запилить свой eval(). Переменные в текущий скоуп, правда, не получится, но вычисление строки — вполне возможно.
                0
                Ну вообще-то нет. Попробуйте объявить свой тип данных и использовать его. В общем случае, вам потребуется компилятор в рантайме.
                  0
                  Я же сказал, что в текущий скоуп не получится экспортировать переменные. И классы. Я имел ввиду любые имена, кроме, кстати, c-like функций. А вычислить любое корректное выражение на плюсах — пожалуйста :)
                  Кстати, если уж на то пошло, то заэмбеддить clang в приложение не такой уж большой гемор, но можно обойтись и system("...").
            0
            Лямда-функции всякие не подходят? Тогда C++ и Java новые версий.
              0
              Лямда-функции в данном контексте можно рассматривать как указатели на функцию (вырожденный случай виртуальных функций).
                0
                Оно все в конечном итоге сводится к косвенному вызову функции (т.е. к указателям на функции). Интерес представляют более высокоуровневые конструкции.
                  –1
                  Тогда всё равно Java, так как там и полностью динамические вызовы есть (хоть их и нет в самом языке Java 7, но в других языках для JVM они есть и есть в Java 8).
                +2
                Есть еще ручное динамическое связывание: object->onClick = base->onClick;
                +1
                А является ли первая картинка неким посылом к понимаю содержимого статьи?

                На картинке мы видим совершенно избыточный элемент — ленту ФУМ, намотанную под гайки ПНД тройника. Это совершенно ничего не даёт, так как уплотнительным элементом в этом тройнике являются резиновые кольца. Подобная же намотка не поможет и в случае протечкам по кольцам — вода просто вытечет со стороны трубы.
                  0
                  Смотря на картинку, нельзя с 100% уверенностью сказать, что по трубам течёт именно вода, а не другая, более вязкая, жидкость.
                    +1
                    Вода, вода там :)
                    Вот источник фотки: dacha.innsk.ru/444
                      0
                      Эх… убедили))
                        0
                        Google картинки рулят!
                    0
                    Замечательная статья, замечательный перевод, но хочу присоединиться, что явно не хватает второй части для нового синтаксиса (Qt 5+).

                    Впрочем, разобраться в этом можно за полчаса, поколупавшись в выводе MOC.
                      0
                      Добавил в конец поста ссылку на перевод второй части.

                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                      Самое читаемое