Qtでのシグナルとスロットの動作(パート1)





Qtは、信号とスロットのメカニズムでよく知られています。 しかし、それはどのように機能しますか? この投稿では、 QObjectQMetaObjectの内部を調査し、それらの背後での作業を明らかにします。 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システムは、プログラム実行中のオブジェクトの自己分析の可能性に基づいています。 イントロスペクションとは、オブジェクトのメソッドとプロパティをリストし、それらに関するすべての情報、特にそれらの引数の型に関する情報を取得する機能を意味します。 QtScriptQMLは、これなしではほとんど実現できませんでした。



C ++はネイティブイントロスペクションのサポートを提供しないため、Qtにはこれを提供するツールが付属しています。 このツールはMOCと呼ばれます。 これはコードジェネレーターです(一部の人々が考えるように、プリプロセッサーではありません)。



ヘッダーファイルを解析し、プログラムの残りでコンパイルされる追加のC ++ファイルを生成します。 この生成されたC ++ファイルには、イントロスペクションに必要なすべての情報が含まれています。

Qtは追加のコードジェネレーターであるため、言語の純粋主義者から批判されることがあります。 Qtのドキュメントでこの批判に答えることができます。 コードジェネレーターに問題はなく、 MOCは優れたヘルパーです。



マジックマクロ


C ++キーワードではないキーワードを見つけることができますか? シグナルスロットQ_OBJECT放出シグナルスロット 。 これらは、C ++のQt拡張機能として知られています。 これらは、実際にはqobjectdefs.hで定義されている単純なマクロです。



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





確かに、シグナルとスロットは単純な関数です。コンパイラーは他の関数と同様にそれらを処理します。 マクロは依然として特定の目的を果たします。MOCはマクロを認識します。 シグナルは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を使用します 。これは、テーブルの行のアドレスを2つのインクルードとともに登録します。



それでは、 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は 、親クラスのスーパーデータ(この場合は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がヘッダーを構成します。 これには2つの列があり、最初の列は数量で、2番目の列は説明が始まる配列のインデックスです。 現在のケースでは、2つのメソッドがあり、メソッドの説明はインデックス14から始まります。

メソッドの説明は5 intで構成されています。 最初の名前は、行テーブルのインデックスです(詳細については後ほど説明します)。 2番目の整数はパラメーターの数であり、その後にインデックスが続き、そこでパラメーターの説明を見つけることができます。 ここで、タグとフラグを無視します。 各関数について、 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に渡すだけの関数です。 配列の最初の要素は戻り値です。 この例では、戻り値がvoidであるため、これは0です。 アクティブ化するために関数に渡される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のソースコードを見ている間、これら3つのインデックスの違いを知っておく必要があります。



接続の仕組み


接続時にQtが最初に行うことは、シグナルとスロットのインデックスを探すことです。 Qtは、対応するインデックスを検索して、メタオブジェクトの行テーブルをスキャンします。 次に、QObjectPrivate :: Connectionオブジェクトが作成され、内部リストに追加されます。



各接続を保存するにはどのような情報が必要ですか? 特定の信号インデックスの接続にすばやくアクセスする方法が必要です。 同じ信号に複数のスロットが接続される可能性があるため、各信号に接続されたスロットのリストが必要です。 各接続には、受信者オブジェクトとスロットインデックスが含まれている必要があります。 また、受信者が削除されたときに接続が自動的に削除されるようにするため、各受信者は接続を削除できるように誰が接続しているのかを知る必要があります。



qobject_p.hで定義されているQObjectPrivate :: Connectionは次のとおりです。



 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内の次/前のノードへのポインターで実装されます。 senderListからのprevポインターはポインターへのポインターであることに注意してください。 これは、実際には前のノードではなく、前のノードの次のノードを指しているためです。 このポインターは、接続が切断された場合にのみ使用されます。 これにより、最初の要素に特別なケースを持たないようにすることができます。



信号放射


信号を呼び出すと、既にQMetaObject :: activateを呼び出しているMOCによって生成されたコードを呼び出すことがわかりました 。 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ここの2番目の部分の翻訳。



All Articles