From d77c3dd616384addfcc8c5607860466850fdfcf2 Mon Sep 17 00:00:00 2001 From: Igor Sirotin Date: Mon, 25 May 2026 14:35:25 +0100 Subject: [PATCH] fix: marshal provider events onto the source thread (#68) * fix: marshal provider events onto the source thread ModuleProxy's event listener emitted eventResponse directly on whatever thread the module fired the event from (its worker/FFI thread). QtRemoteObjects then serialized and sent the event from that foreign thread, racing the source socket against a method reply being sent from the source thread, which silently dropped the reply. This is why a method that emits an event mid-call never returns to the caller (e.g. delivery_module start(), which emits connectionStateChanged as the node connects) while a method that emits nothing (createNode) returns fine. Marshal the emission onto the ModuleProxy's own thread via a queued invocation so events and method replies are serialized on the single thread QtRemoteObjects expects to own the source. Co-Authored-By: Claude Opus 4.7 (1M context) * docs: generalize the threading comment * fix: use AutoConnection so same-thread emits stay synchronous QueuedConnection deferred every emission, breaking same-thread callers that emit-then-assert and crashing when a queued lambda outlived the object. AutoConnection invokes synchronously when already on the source thread and only queues cross-thread emissions (the actual fix); passing 'this' as context cancels a queued call if the object is destroyed first. --------- Co-authored-by: Claude Opus 4.7 (1M context) --- cpp/module_proxy.cpp | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/cpp/module_proxy.cpp b/cpp/module_proxy.cpp index 102b389..2bccc1f 100644 --- a/cpp/module_proxy.cpp +++ b/cpp/module_proxy.cpp @@ -9,7 +9,20 @@ ModuleProxy::ModuleProxy(LogosProviderObject* provider, QObject* parent) if (m_provider) { m_provider->setEventListener([this](const QString& eventName, const QVariantList& data) { qDebug() << "[LogosProviderObject] ModuleProxy: forwarding event" << eventName << "as Qt signal"; - emit eventResponse(eventName, data); + // Events may be fired from any thread (e.g. a module's worker/FFI + // thread), but this object is the QtRemoteObjects source and must be + // driven from its own thread. Emitting directly from a foreign + // thread runs QtRO's source serialization there, racing the source + // socket against a reply being sent from the source thread, which + // can silently drop the reply. AutoConnection keeps same-thread + // callers synchronous (the common case) and only queues the + // emission when it arrives from another thread, so events and + // replies stay serialized on the thread QtRO expects to own the + // source. Passing `this` as the context also cancels a queued + // emission if this object is destroyed first. + QMetaObject::invokeMethod(this, [this, eventName, data]() { + emit eventResponse(eventName, data); + }, Qt::AutoConnection); }); qDebug() << "[LogosProviderObject] ModuleProxy: created, wrapping LogosProviderObject" << m_provider->providerName();