📜 ⬆️ ⬇️

How signals and slots work in Qt (part 1)



Qt is well known for its signal and slot mechanisms. But how does this work? In this post, we explore the insides of QObject and QMetaObject and reveal their work behind the scenes. I will give examples of Qt5 code, sometimes edited for brevity and adding formatting.

Signals and Slots

To begin with, let's remember how the signals and slots look, looking at the official example . The header file looks like this:

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); }; 

Somewhere, in the .cpp file, we implement setValue () :
')
 void Counter::setValue(int value) { if (value != m_value) { m_value = value; emit valueChanged(value); } } 

Then, we can use the Counter object in this way:

 Counter a, b; QObject::connect(&a, SIGNAL(valueChanged(int)), &b, SLOT(setValue(int))); a.setValue(12); // a.value() == 12, b.value() == 12 

This is the original synaxis, which has hardly changed since the beginning of Qt in 1992. But even if the base API was not changed, the implementation changed several times. Under the hood, new features were added and other things were happening. There is no magic here and I will show how it works.

MOC or Meta-Object Compiler

Signals and slots, as well as the Qt property system, are based on the capabilities of self-analysis of objects during program execution. Self-analysis means the ability to list the methods and properties of an object and to have all the information about them, in particular, about the types of their arguments. QtScript and QML would hardly have been possible without this.

C ++ does not provide native support for introspection, so Qt comes with the tool it provides. This tool is called MOC . This is a code generator (but not a preprocessor, as some people think).

It parses the header files and generates an additional C ++ file that is compiled with the rest of the program. This generated C ++ file contains all the information necessary for self-analysis.
Qt is sometimes criticized by language purists, as it is an additional code generator. I will allow Qt documentation to respond to this criticism. There is nothing wrong with the code generator and the MOC is an excellent helper.

Magic Macros

Can you notice keywords that are not C ++ keywords? signals , slots , Q_OBJECT , emit , SIGNAL , SLOT . They are known as the Qt extension for C ++. These are actually simple macros that are defined in qobjectdefs.h.

 #define signals public #define slots /* nothing */ 

True, signals and slots are simple functions: the compiler treats them like any other function. Macros still serve a specific purpose: the MOC sees them. The signals were in the protected section in Qt4 and earlier. But in Qt5, they are already open to support the new syntax .

 #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 defines a bunch of functions and a static QMetaObject . These functions are implemented in the MOC generated file.

 #define emit /* nothing */ 

emit is an empty macro. He does not even parse the MOC . In other words, emit is optional and means nothing (except for the developer’s hint).

 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 

These macros are simply used by the preprocessor to convert the parameter to a string and add code at the beginning. In debug mode, we also supplement the line with the file location with a warning if the connection to the signal does not work. This was added in Qt 4.5 for compatibility. In order to find out which rows contain information about a row, we use qFlagLocation , which registers the address of a row in a table, with two inclusions.

We now turn to the code generated by the 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; } 

Here we see the implementation of Counter :: metaObject () and Counter :: staticMetaObject . They are declared in the Q_OBJECT macro. QObject :: d_ptr-> metaObject is used only for dynamic metaobjects ( QML objects), therefore, in general, the virtual function metaObject () simply returns the class's staticMetaObject. staticMetaObject is built with read-only data. QMetaObject is defined in qobjectdefs.h as:

 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 indirectly symbolizes that all members must be hidden, but they are not hidden in order to preserve the POD and the possibility of static initialization.

QMetaObject is initialized using a metaobject of the superdata parent class (QObject :: staticMetaObject in this case). stringdata and data are initialized with some data, which will be discussed later. static_metacall is a pointer to a function, initialized by Counter :: qt_static_metacall.

Introspection tables

First, let's look at the basic data of a 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 }; 

The first 13 int constitute the header. It provides two columns, the first column is the number, and the second is the array index where the description begins. In the current case, we have two methods, and the description of the methods begins with index 14.
The description of the method consists of 5 int. The first is the name, the index in the string table (we will look at it in detail later). The second integer is the number of parameters, followed by an index, where we can find their description. Now we will ignore the tag and flags. For each function, MOC also saves the return type of each parameter, their type, and the name index.

Row table

 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 

Basically, it is a static QByteArray array (created by the QT_MOC_LITERAL macro), which refers to a specific index in the row below.

Signals

MOC also implements signals. They are functions that simply create an array of pointers to arguments and pass them to QMetaObject :: activate . The first element of the array is the return value. In our example, this is 0, because the return value is void. The third argument passed to the function to activate is the signal index (0 in this case).

 // SIGNAL 0 void Counter::valueChanged(int _t1) { void *_a[] = { 0, const_cast<void*>(reinterpret_cast<const void*>(&_t1)) }; QMetaObject::activate(this, &staticMetaObject, 0, _a); } 

Slot call

It is also possible to call a slot by its index using the qt_static_metacall function:

 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: ; } ... } ... } 


Array of pointers to arguments in the same format as in signals. _a [0] is not touched, because everywhere void returns here.

Index Note

For each QMetaObject , signals, slots, and other called object methods, indices starting from 0 are given. They are ordered in such a way that signals come first, then slots, and then other methods. These indices inside are called relative indices. They do not include parent indexes. But in general, we don’t want to know a more global index that does not belong to a particular class, but includes all other methods in the inheritance chain. Therefore, we simply add the offset to the relative index and get the absolute index. This index, used in the public API, is returned by functions of the form QMetaObject :: indexOf {Signal, Slot, Method}.

The join mechanism uses an array indexed for signals. But all slots occupy a place in this array and usually there are more slots than signals. So, with Qt 4.6, a new internal index for signals appears, which includes only the indices used for signals. If you are developing with Qt, you only need to know about the absolute index for the methods. But while you are viewing QObject source code, you should know the difference between these three indices.

How the connection works

The first thing that Qt does when connecting is looking for signal and slot indices. Qt will look at the metaobject row tables for the corresponding indexes. Then, a QObjectPrivate :: Connection object is created and added to the internal lists.

What information is needed to store each connection? We need a way to quickly access the connection for a given signal index. Since there can be several slots attached to the same signal, we need to have a list of attached slots for each signal. Each connection must contain a recipient object and a slot index. We also want the connections to be automatically deleted when the recipient is deleted, so each recipient object needs to know who is connected to it so that it can delete the connection.

Here is the QObjectPrivate :: Connection defined in 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; } } }; 

Each object has an array of connections: this is an array that links each signal to QObjectPrivate :: Connection lists. Each object also has reverse lists of connections of objects connected for automatic deletion. This is a doubly linked list.

Linked lists are used to enable the quick addition and removal of objects. They are implemented with the presence of pointers to the next / previous node inside QObjectPrivate :: Connection. Note that the prev pointer from senderList is a pointer to a pointer. This is because we really do not point to the previous node, but rather to the next, in the previous node. This pointer is used only when the connection is destroyed. This allows not to have a special case for the first element.

Signal emission

When we call the signal, we saw that it calls the code generated by the MOC , which already calls QMetaObject :: activate . Here is the implementation (with notes) of this method in 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 : translation of the second part here .

Source: https://habr.com/ru/post/214379/


All Articles