Механізм сигналів та слотів в Qt

Матеріал з Вікіпедії — вільної енциклопедії.
Перейти до навігації Перейти до пошуку

Посилання на оригінал[ред. | ред. код]

Це переклад статті опублікованої на сайті woboq.com. Оригінал статті можна знайти за посиланням ось тут [Архівовано 9 жовтня 2017 у Wayback Machine.].

Як працюють сигнали та слоти в Qt[ред. | ред. код]

Qt добре відомий завдяки своїм сигналам та слотам.

Але як вони працюють?

В цій статті я розкрию внутрішню реалізацію QObject та QMetaObject, та досліджу, як насправді працюють сигнали та слоти.

В цій статті використовуються частини коду Qt5, іноді відформатовані та скорочені.

Сигнали та слоти[ред. | ред. код]

Спочатку, давайте згадаємо, як виглядають сигнали та слоти. Подивимося на офіційний приклад [Архівовано 8 жовтня 2017 у Wayback Machine.].

Заголовочний файл класу виглядає приблизно так:

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, який практично не змінився зі створення Qt у 1992 році.

Але, навіть якщо базовий інтерфейс (API) не змінився з моменту створення, його реалізація декілька разів змінювалася.

Додавалися нові можливості, але більш за все змінилася реалізація. І тут не має ніякої магії, ця стаття покаже, як все це працює.

MOC (Meta Object Compiler)[ред. | ред. код]

Сигнали/слоти в Qt та ситема властивостей (property system) базуються на здатності самоаналізу (Introspection) об'єктів під час виконання програми. Під самоаналізом мається на увазі здатність знати про всі методи та властивості об'єкту, володіти повною інформацією про них, наприклад, знати які типи мають аргументи слотів. QtScript та QML навряд чи були б можливі без цих властивостей.

С++ не підтримує самоаналіз, тому Qt поставляється з інструментом для його надання. Цей інструмент - MOC. Це генератор коду (НЕ препроцессор, як деякі його кличуть).

Він аналізує заголовочні файли та генерує додаткові С++ файли, якіо компілюються разом з усією програмою. Ці згенеровані С++ файли містять всю необхідну для самоаналізу інформацію.

Qt іноді критикують за надлишковий генератор коду. Документація Qt вже відповіла на цю критику тут [Архівовано 9 жовтня 2017 у Wayback Machine.]. Насправді, немає нічого неправильного в генераторі коду та MOC, навпаки, вони дуже корисні.

Магічний макрос[ред. | ред. код]

Чи помітили ви ключові слова, які не належать до ключових слів мови С++? signals, slots, Q_OBJECT, emit, SIGNAL, SLOT. Вони відомі як Qt розширення мови С++.

Насправді, це прості макроси, оголошені в qobjectdefs.h [Архівовано 9 жовтня 2017 у Wayback Machine.]:

#define signals public

#define slots /* nothing */

Насправді сигнали та слоти - це прості функції: компілятор обробить їх, як будь-які інші функції. Та все ж, макроси служать певній меті - вони потрібні для MOC компілятора. В Qt4 та раніше сигнали були захищеними (protected). Та вони стали відкритими (public) в Qt5, для того, щоб задіяти новий синтаксис [Архівовано 9 жовтня 2017 у Wayback Machine.].

#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

Наведені вище макроси - SIGNAL, SLOT - просто використовують препроцесор для перетворення параметра в рядок, та для додавання номера перед ним.

В режимі відладки ми також можемо побачити попередження (warning message), в якому вказана адреса файлу, в якому не спрацювало з'єднання сигналу зі слотом.

Ця можливість була додана в Qt 4.5. Щоб дізнатись, які рядки містять інформацію про місце де сталася помилка з'єднання, ми використовуємо qFlagLocation який буде реєструвати рядок-адресу в таблиці з двома записами.

MOC згенерований код[ред. | ред. код]

Тепер ми перейдемо до частини коду, створеної за допомогою MOC в Qt5.

QMetaObject

const QMetaObject Counter::staticMetaObject = {
    { &QObject::staticMetaObject, qt_meta_stringdata_Counter.data,  qt_meta_data_Counter,  qt_static_metacall, Q_NULLPTR, Q_NULLPTR}
};


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 [Архівовано 9 жовтня 2017 у Wayback Machine.] та виглядає приблизно так:

struct QMetaObject
{
    /* ... Skiped all the public functions ... */

    enum Call { InvokeMetaMethod, ReadProperty, WriteProperty, /*...*/ };

    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 **;
        void *; //reserved for future use
    } d;
};

Назва d символізує, що всі члени мають бути приватними. Але на справді вони не приватні. Це потрібно для того, щоб зберегти структуру POD (Plain Old Data Structure) та дозволяти статичну ініціалізацію.

POD - це агрегат, клас або структура, який не містить жодного користувацького конструктора, жодного приватного чи не статичного поля, не наслідується ні від кого та не містить віртуальних функцій. Вони використовуються в С++ для того щоб гарантувати, що в даный структурі не буде ніякої прихованої поведінки - вказівників на віртуальні таблиці, зсуви в пам'яті, при приведенні типів. Тобто такий клас має бути просто об'єднанням полів стандартних типів даних.

QMetaObject ініціалізується мета-об'єктом батьківського об'єкта (в даному випадкуQObject::staticMetaObject) як супер-даними (superdata).

stringdata та data ініціалізуються деякими даними, про які буде розказано пізніше в цій статті.

static_metacall це покажчик на функцію, ініціалізований Counter::qt_static_metacall.

Таблиці самоаналізу (Introspection Tables)[ред. | ред. код]

Спочатку, давайте проаналізуємо числові поля класу 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, 0x06 /* Public */,

 // slots: name, argc, parameters, tag, flags
       4,    1,   27,    2, 0x0a /* Public */,

 // signals: parameters
    QMetaType::Void, QMetaType::Int,    3,

 // slots: parameters
    QMetaType::Void, QMetaType::Int,    5,

       0        // eod
};

Перші 13 чисел типу int містять заголовок. Коли є 2 стовпчика, перший стовпчик - це кількість, а другий - це індекс в цьому массиві, де починається опис. В даному випадку ми маємо 2 функції, і опис цих функцій починається з індексу 14:

2,   14, // methods

Опис методу складається з 5 чисел типу int.

Перше - це ім'я, а саме індекс в таблиці рядків (ми розглянемо деталі пізніше).

Друге число - це кількість параметрів. Далі йде індекс, за яким можна знайти опис параметрів. Поки що ми проігноруємо теги та прапорці.

Для кожної функції MOC також зберігає return type для кожного параметру, типи параметрів та індекс, по якому можна знайти ім'я функції.

Таблиця рядків (String Table)[ред. | ред. код]

struct qt_meta_stringdata_Counter_t {
    QByteArrayData data[6];
    char stringdata0[46];
};
#define QT_MOC_LITERAL(idx, ofs, len) \
    Q_STATIC_BYTE_ARRAY_DATA_HEADER_INITIALIZER_WITH_OFFSET(len, \
    qptrdiff(offsetof(qt_meta_stringdata_Counter_t, stringdata0) + ofs \
        - idx * sizeof(QByteArrayData)) \
    )
static const qt_meta_stringdata_Counter_t qt_meta_stringdata_Counter = {
    {
QT_MOC_LITERAL(0, 0, 7), // "Counter"
QT_MOC_LITERAL(1, 8, 12), // "valueChanged"
QT_MOC_LITERAL(2, 21, 0), // ""
QT_MOC_LITERAL(3, 22, 8), // "newValue"
QT_MOC_LITERAL(4, 31, 8), // "setValue"
QT_MOC_LITERAL(5, 40, 5) // "value"

    },
    ""Counter\0valueChanged\0\0newValue\0setValue\0""
    ""value""
};
#undef QT_MOC_LITERAL

Взагалі це статичний массив типу QByteArray.

Макрос QT_MOC_LITERAL створює статичний масив типу QByteArray, що посилається на конкретний індекс у рядку нижче.

Сигнали[ред. | ред. код]

MOC також реалізує сигнали.

Сигнали - це прості функції, які просто створюють масив покажчиків на аргументи та передають його до QMetaObject::activate.

Перший елемент массиву - це значення, що буде повернене. В нашому прикладі це 0, тому що функція повертає void.

Другий елемент масиву - це аргумент сигналу.

Потім викликається метод QMetaObject::activate до якого передаються індекс сигналу (3-й параметр, в нашому випадку це 0) та масив вказівників на аргументи.

// SIGNAL 0
void Counter::valueChanged(int _t1)
{
    void *_a[] = { Q_NULLPTR, 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) {
        Q_ASSERT(staticMetaObject.cast(_o));
        Counter *_t = static_cast<Counter *>(_o);
        Q_UNUSED(_t)
        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. Вони впорядковані, таким чином, що сигнали ідуть першими, потім слоти, а потім інші методи. Ці індекси всередині називаються відносними індексами (relative index). Вони не включають в себе індекси батьків.

Ну і в цілому нам не потрібно знати більш глобальні індекси, що не відносяться до конкретного класу, але включають всі інші методи в ланцюжку успадкування. Для того, щоб отримати глобальний індекс ми просто додамо зміщення до нашого відносного індексу і отримаємо абсолютний індекс. Це індекс, який використовується у публічному API. Він повертається такими методами як QMetaObject::indexOf{Signal,Slot,Method}.

Механізм з'єднання використовує вектор, індексований сигналами. Але слоти також займають простір у векторі, і в об'єкті, як правило, більше слотів, ніж сигналів. Тому з Qt 4.6 використовується новий внутрішній індекс сигналу, який включає лише індекси сигналів.

Під час розробки з Qt вам потрібно знати лише про абсолютний індекс. Але під час перегляду вихідного коду QBbject Qt ви повинні знати про різницю між цими трьома індексами.

Закріпимо поняття.

Відносний індекс: внутрішній індекс для сигналів, слотів та методів у кожному об'єкті QMetaObject.

Абсолютний індекс: всі індекси в ланцюгу наслідування: від батьків до дітей.

Індекс сигналу: індекс що використовується тільки для сигналів.

Як працює підключення.[ред. | ред. код]

Перше, що Qt робить під час з'єднання - це з'ясовує індекс сигналу та слота. Qt буде шукати в рядках таблиць мета-об'єкта, щоб знайти відповідні індекси.

Потім створюється об'єкт QObjectPrivate::Connection та додається у внутрішній зв'язаний список.

Яку інформацію потрібно зберігати для кожного з'єднання? Нам потрібен спосіб швидко отримати доступ до з'єднань для заданого індексу сигналу. Оскільки може існувати декілька слотів, підключених до одного сигналу, для кожного сигналу потрібно мати список підключених слотів. Кожне з'єднання повинно містити об'єкт-отримувач сигналу та індекс слота. Ми також хочемо, щоб підключення автоматично знищувались, коли об'єкт-отримувач знищується. Тому кожен об'єкт-отримувач повинен знати, хто з ним пов'язаний, щоб він міг очистити з'єднання.

Ось так клас QObjectPrivate::Connectionоголошений в qobject_p.h [Архівовано 9 жовтня 2017 у Wayback Machine.] :

struct QObjectPrivate::Connection
{
    QObject *sender;
    QObject *receiver;
    union {
        StaticMetaCallFunction callFunction;
        QtPrivate::QSlotObjectBase *slotObj;
    };
    // The next pointer for the singly-linked ConnectionList
    Connection *nextConnectionList;
    //senders linked list
    Connection *next;
    Connection **prev;
    QAtomicPointer<const int> argumentTypes;
    QAtomicInt ref_;
    ushort method_offset;
    ushort method_relative;
    uint signal_index : 27; // In signal range (see 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_ is 2 for the use in the internal lists, and for the use in 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.

Зверніть увагу, що покажчик на попередній об'єкт насправді вказує на інший покажчик. Насправді ми вказуємо не на весь попередній вузол, а на покажчик на наступний вузол у попередньому вузлі.

Цей покажчик використовується лише тоді, коли з'єднання знищується, а не для переходу назад. Це дозволяє уникнути збереження окремого списку для знищення.

Публікація (emit) сигналу[ред. | ред. код]

Коли ми викликаємо сигнал ми бачимо що він викликає код згенерований MOC генератором який викликає QMetaObject::activate.

Ось так виглядає метод QMetaObject::activate, реалізований в файлі qobject.cpp [Архівовано 9 жовтня 2017 у Wayback Machine.]:

void QMetaObject::activate(QObject *sender, const QMetaObject *m, int local_signal_index,
                           void **argv)
{
    activate(sender, QMetaObjectPrivate::signalOffset(m), local_signal_index, argv);
    /* We just forward to the next function here. We pass the signal offset of
     * the meta object rather than the QMetaObject itself
     * It is split into two functions because QML internals will call the later. */
}

void QMetaObject::activate(QObject *sender, int signalOffset, int local_signal_index, void **argv)
{
    int signal_index = signalOffset + local_signal_index;

    /* The first thing we do is quickly check a bit-mask of 64 bits. If it is 0,
     * we are sure there is nothing connected to this signal, and we can return
     * quickly, which means emitting a signal connected to no slot is extremely
     * fast. */
    if (!sender->d_func()->isSignalConnected(signal_index))
        return; // nothing connected to these signals, and no spy

    /* ... Skipped some debugging and QML hooks, and some sanity check ... */

    /* We lock a mutex because all operations in the connectionLists are thread safe */
    QMutexLocker locker(signalSlotLock(sender));

    /* Get the ConnectionList for this signal.  I simplified a bit here. The real code
     * also refcount the list and do sanity checks */
    QObjectConnectionListVector *connectionLists = sender->d_func()->connectionLists;
    const QObjectPrivate::ConnectionList *list =
        &connectionLists->at(signal_index);

    QObjectPrivate::Connection *c = list->first;
    if (!c) continue;
    // We need to check against last here to ensure that signals added
    // during the signal emission are not emitted in this emission.
    QObjectPrivate::Connection *last = list->last;

    /* Now iterates, for each slot */
    do {
        if (!c->receiver)
            continue;

        QObject * const receiver = c->receiver;
        const bool receiverInSameThread = QThread::currentThreadId() == receiver->d_func()->threadData->threadId;

        // determine if this connection should be sent immediately or
        // put into the event queue
        if ((c->connectionType == Qt::AutoConnection && !receiverInSameThread)
            || (c->connectionType == Qt::QueuedConnection)) {
            /* Will basically copy the argument and post an event */
            queued_activate(sender, signal_index, c, argv);
            continue;
        } else if (c->connectionType == Qt::BlockingQueuedConnection) {
            /* ... Skipped ... */
            continue;
        }

        /* Helper struct that sets the sender() (and reset it backs when it
         * goes out of scope */
        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) {
            /* ... Skipped....  Qt5-style connection to function pointer */
        } else if (callFunction && c->method_offset <= receiver->metaObject()->methodOffset()) {
            /* If we have a callFunction (a pointer to the qt_static_metacall
             * generated by moc) we will call it. We also need to check the
             * saved metodOffset is still valid (we could be called from the
             * destructor) */
            locker.unlock(); // We must not keep the lock while calling use code
            callFunction(receiver, QMetaObject::InvokeMetaMethod, method_relative, argv);
            locker.relock();
        } else {
            /* Fallback for dynamic objects */
            const int method = method_relative + c->method_offset;
            locker.unlock();
            metacall(receiver, QMetaObject::InvokeMetaMethod, method, argv);
            locker.relock();
        }

        // Check if the object was not deleted by the slot
        if (connectionLists->orphaned) break;
    } while (c != last && (c = c->nextConnectionList) != 0);
}

Висновок[ред. | ред. код]

Ми розглянули як працюють з'єднання та як викликаються сигнали та слоти. Ми не розглянули як реалізований новий синтаксис Qt5, але ми це розглянемо в наступній статті.