aboutsummaryrefslogtreecommitdiffstats
path: root/src/webchannel
diff options
context:
space:
mode:
authorArno Rehn <a.rehn@menlosystems.com>2021-08-18 13:46:11 +0200
committerArno Rehn <a.rehn@menlosystems.com>2021-09-27 15:58:16 +0200
commitd711fc874dacb2eeeed085dafc2a07d870cae8ba (patch)
treee09bab05dbf4a3d4f7746ed51d85d7db00360491 /src/webchannel
parentc45b1c4f73ec70ce990574b66eff47cb94a80ea6 (diff)
Transparently handle QFuture<T> method return types
When a client invokes a method returning a QFuture<T>, QWebChannel will now automatically attach a continuation and send the contained result after the QFuture<T> has finished. [ChangeLog] Transparently handle QFuture<T> method return types Task-number: QTBUG-92903 Change-Id: I4069d51e79447dee249bb8af52a16e4496484093 Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org> Reviewed-by: Arno Rehn <a.rehn@menlosystems.com>
Diffstat (limited to 'src/webchannel')
-rw-r--r--src/webchannel/qmetaobjectpublisher.cpp103
-rw-r--r--src/webchannel/qwebchannel.cpp3
2 files changed, 103 insertions, 3 deletions
diff --git a/src/webchannel/qmetaobjectpublisher.cpp b/src/webchannel/qmetaobjectpublisher.cpp
index b6a9e11..d81b67c 100644
--- a/src/webchannel/qmetaobjectpublisher.cpp
+++ b/src/webchannel/qmetaobjectpublisher.cpp
@@ -44,6 +44,9 @@
#include "qwebchannelabstracttransport.h"
#include <QEvent>
+#if QT_CONFIG(future)
+#include <QFuture>
+#endif
#include <QJsonDocument>
#include <QDebug>
#include <QJsonObject>
@@ -177,6 +180,80 @@ QJsonObject createResponse(const QJsonValue &id, const QJsonValue &data)
return response;
}
+#if QT_CONFIG(future)
+QMetaType resultTypeOfQFuture(QByteArrayView typeName)
+{
+ if (!typeName.startsWith("QFuture<") || !typeName.endsWith('>'))
+ return {};
+
+ return QMetaType::fromName(typeName.sliced(8, typeName.length() - 9));
+}
+
+template<typename Func>
+void attachContinuationToFutureInVariant(const QVariant &result, QPointer<QObject> contextObject,
+ Func continuation)
+{
+ Q_ASSERT(result.canConvert<QFuture<void>>());
+
+ // QMetaObject::invokeMethod() indirection to work around an issue with passing
+ // a context object to QFuture::then(). See below.
+ const auto safeContinuation = [contextObject, continuation=std::move(continuation)]
+ (const QVariant &result)
+ {
+ if (!contextObject)
+ return;
+
+ QMetaObject::invokeMethod(contextObject.get(), [continuation, result] {
+ continuation(result);
+ });
+ };
+
+ auto f = result.value<QFuture<void>>();
+
+ // Be explicit about what we capture so that we don't accidentally run into
+ // threading issues.
+ f.then([resultType=resultTypeOfQFuture(result.typeName()), f, continuation=safeContinuation]
+ {
+ if (!resultType.isValid() || resultType == QMetaType::fromType<void>()) {
+ continuation(QVariant{});
+ return;
+ }
+
+ auto iface = QFutureInterfaceBase::get(f);
+ // If we pass a context object to f.then() and the future originates in a
+ // different thread, this assertions fails. Why?
+ // For the time being, work around that with QMetaObject::invokeMethod()
+ // in safeSendResponse().
+ Q_ASSERT(iface.resultCount() > 0);
+
+ QMutexLocker<QMutex> locker(&iface.mutex());
+ if (iface.resultStoreBase().resultAt(0).isVector()) {
+ locker.unlock();
+ // This won't work because we cannot generically get a QList<T> into
+ // a QVariant with T only known at runtime.
+ // TBH, I don't know how to trigger this.
+ qWarning() << "Result lists in a QFuture return value are not supported!";
+ continuation(QVariant{});
+ return;
+ }
+
+ // pointer<void>() wouldn't compile because of the isVector-codepath
+ // using QList<T> in that method. We're not taking that path anyway (see the
+ // above check), so we can use char instead to not break strict aliasing
+ // requirements.
+ const auto data = iface.resultStoreBase().resultAt(0).pointer<char>();
+ locker.unlock();
+
+ const QVariant result(resultType, data);
+ continuation(result);
+ }).onCanceled([continuation=safeContinuation] {
+ // Will catch both failure and cancellation.
+ // Maybe send something more meaningful?
+ continuation(QVariant{});
+ });
+}
+#endif
+
}
Q_DECLARE_TYPEINFO(OverloadResolutionCandidate, Q_RELOCATABLE_TYPE);
@@ -1043,9 +1120,29 @@ void QMetaObjectPublisher::handleMessage(const QJsonObject &message, QWebChannel
method.toInt(-1),
message.value(KEY_ARGS).toArray());
}
- if (!publisherExists || !transportExists)
- return;
- transport->sendMessage(createResponse(message.value(KEY_ID), wrapResult(result, transport)));
+
+ auto sendResponse = [publisherExists, transportExists, id=message.value(KEY_ID)]
+ (const QVariant &result)
+ {
+ if (!publisherExists || !transportExists)
+ return;
+
+ Q_ASSERT(QThread::currentThread() == publisherExists->thread());
+
+ const auto wrappedResult =
+ publisherExists->wrapResult(result, transportExists.get());
+ transportExists->sendMessage(createResponse(id, wrappedResult));
+ };
+
+#if QT_CONFIG(future)
+ if (result.canConvert<QFuture<void>>()) {
+ attachContinuationToFutureInVariant(result, publisherExists.get(), sendResponse);
+ } else {
+ sendResponse(result);
+ }
+#else
+ sendResponse(result);
+#endif
} else if (type == TypeConnectToSignal) {
signalHandlerFor(object)->connectTo(object, message.value(KEY_SIGNAL).toInt(-1));
} else if (type == TypeDisconnectFromSignal) {
diff --git a/src/webchannel/qwebchannel.cpp b/src/webchannel/qwebchannel.cpp
index 4752891..b268e95 100644
--- a/src/webchannel/qwebchannel.cpp
+++ b/src/webchannel/qwebchannel.cpp
@@ -66,6 +66,9 @@ QT_BEGIN_NAMESPACE
On the client side, a JavaScript object will be created for any published C++ QObject. It mirrors the
C++ object's API and thus is intuitively useable.
+ QWebChannel transparently supports QFuture. When a client calls a method that returns a QFuture,
+ QWebChannel will send a response with the QFuture result only after the QFuture has finished.
+
The C++ QWebChannel API makes it possible to talk to any HTML client, which could run on a local
or even remote machine. The only limitation is that the HTML client supports the JavaScript
features used by \c{qwebchannel.js}. As such, one can interact