Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

Как работают сигналы и слоты в Qt

Anna | 24.06.2014 | нет комментариев

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 ? signalsslots,Q_OBJECTemitSIGNALSLOT. Они вестимы как 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. 1-й – это имя, индекс в таблице строк (мы подробно разглядим её позже). Второе целое – число параметров, следом за которым идёт индекс, где мы может обнаружить их изложение. Теперь мы будет игнорировать тег и флаги. Для всякой функции 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. 1-й элемент массива это возращаемое значение. В нашем примере это 0, потому что возращаемое значение void. 3-й довод, передаваемый функции для активизации, это индекс сигнала (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);
}

От переводчика: это была первая часть и традиционным будет вопрос о необходимости перевода 2-й части.

Источник: programmingmaster.ru

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB