diff options
author | Arno Rehn <a.rehn@menlosystems.com> | 2021-08-18 13:46:11 +0200 |
---|---|---|
committer | Arno Rehn <a.rehn@menlosystems.com> | 2021-09-27 15:58:16 +0200 |
commit | d711fc874dacb2eeeed085dafc2a07d870cae8ba (patch) | |
tree | e09bab05dbf4a3d4f7746ed51d85d7db00360491 /src/webchannel | |
parent | c45b1c4f73ec70ce990574b66eff47cb94a80ea6 (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.cpp | 103 | ||||
-rw-r--r-- | src/webchannel/qwebchannel.cpp | 3 |
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 |