Details
-
Suggestion
-
Resolution: Unresolved
-
P3: Somewhat important
-
None
-
None
-
None
Description
To have exported class into QObject as singleton cannot be running in separate thread (moveToThread). To overcome this issue, you need to create an intermediate object, expose it to QML and connect it with worker QObject that is running in separate thread.
With this pattern, you get a set of code that cannot be easily tested (connectTo method, for instance, more on example below) and may pose problems when refactoring and changing your API between the intermediate and worker class.
An example code:
// Intermediate class MyIntermediate : public QObject { Q_OBJECT QML_ELEMENT QML_SINGLETON public: explicit MyIntermediate() = default; void connectTo(const MyWorker *worker) const { connect(this, &MyIntermediate::query, worker, &MyWorker::query); connect(this, &MyIntermediate::command, worker, &MyWorker::command); connect(worker, &MyWorker::commandError, this, &MyIntermediate::commandErrorHandler); connect(worker, &MyWorker::commandSuccess, this, &MyIntermediate::commandSuccess); connect(worker, &MyWorker::queryResponse, this, &MyIntermediate::queryResponse); } public slots: void commandErrorHandler(int errorCode) { // Here be some mapping if needed } signals: // Queries void query(); // Commands void command(const QString &input); // Responses void queryResponse(const QStringList &results); void commandSuccess(); void commandError(int erorCode); }; // Worker class MyWorker : public QObject { Q_OBJECT public: explicit MyWorker() = default; public slots: // Queries virtual void query(); // Commands virtual void command(const QString &input); signals: void queryResponse(const QStringList &results); void commandSuccess(); void commandError(int errorCode); };
As you can see, it would be hard to test the connections (unless writing integration tests for each function).
So I've created a few helper methods to verify the connectivity is correct (while you go and change the interaction between both classes).
The tool:
#define INSPECT_CONNECTIONS \ private: \ int countReceivers(const char *signal) const override { \ return receivers(signal); \ } \ const QObject *getQObject() const override { return this; } \ void assertSuperclass(const QMetaObject &superclassMetaObject) override { \ auto errorMsg \ = QString( \ "Inspecting wrong class for connections, inspecting %1 on %2") \ .arg(superclassMetaObject.className()) \ .arg(metaObject()->className()); \ QVERIFY2(metaObject()->inherits(&superclassMetaObject), \ qPrintable(errorMsg)); \ } #define INSPECT_SIGNAL(y, x, c) QCOMPARE(y.numConnections(x), c) class ConnectionInspector; class InspectorAddon { friend class ConnectionInspector; protected: virtual int countReceivers(const char *signal) const = 0; virtual void assertSuperclass(const QMetaObject &superclassMetaObject) = 0; virtual const QObject *getQObject() const = 0; }; class ConnectionInspector : public QMap<QString, int> { public: int sum() { int output = 0; for (auto num : qAsConst(*this)) output += num; return output; } template <typename PointerToMemberFunction> inline int numConnections(PointerToMemberFunction signal) { auto signalMember = QMetaMethod::fromSignal(signal); auto name = methodToString(signalMember); return value(name); } template <class T> void inspect(const InspectorAddon *objectInspector) { auto *object = objectInspector->getQObject(); assertSuperclass(&T::staticMetaObject, object->metaObject()); for (auto objectSignal : listSignals<T>()) { auto signalSignature = objectSignal.methodSignature(); // A magic number from SIGNAL macro auto signalDescriptor = signalSignature.prepend('2'); insert(ConnectionInspector::methodToString(objectSignal), objectInspector->countReceivers(signalDescriptor)); } } private: static QString methodToString(const QMetaMethod &method) { return QString("%1:%2").arg(method.enclosingMetaObject()->className(), QString(method.methodSignature())); } static void assertSuperclass(const QMetaObject *superclassMetaObject, const QMetaObject *metaObject) { auto errorMsg = QString("Inspecting wrong class for connections, inspecting %1 on %2") .arg(superclassMetaObject->className(), metaObject->className()); QVERIFY2(metaObject->inherits(superclassMetaObject), qPrintable(errorMsg)); } template <class T> static QList<QMetaMethod> listSignals() { QList<QMetaMethod> objectSignals; auto numberOfMethods = T::staticMetaObject.methodCount(); for (auto methodIndex = T::staticMetaObject.methodOffset(); methodIndex < numberOfMethods; ++methodIndex) { auto method = T::staticMetaObject.method(methodIndex); if (method.methodType() == QMetaMethod::Signal) { objectSignals << method; } } return objectSignals; } };
And this is how the test looks like:
class MonitoredMyWorker : public MyWorker, public InspectorAddon { Q_OBJECT INSPECT_CONNECTIONS }; class MonitoredMyIntermediate : public MyIntermediate, public InspectorAddon { Q_OBJECT INSPECT_CONNECTIONS }; void MyIntermediateTest::test_connectTo() { InspectedMyIntermediate intermediate; InspectedMyWorker worker; intermediate.connectTo(&worker); ConnectionInspector inspector; inspector.inspect<MyWorker>(&worker); inspector.inspect<MyIntermediate>(&intermediate); INSPECT_SIGNAL(inspector, &MyIntermediate::query, 1); // Cannot assert the other end, but can assert receivers count // All other signals ... QCOMPARE(inspector.sum(), 5); // "Guard" assert if you add new connections }
It is crude and feels a bit of "bad magic", but wanted to share if anyone sees this useful in any way. It could require some feature (like asserting all signals are connected, or number of all available signals, etc.).