aboutsummaryrefslogtreecommitdiffstats
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
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>
-rw-r--r--CMakeLists.txt2
-rw-r--r--src/webchannel/qmetaobjectpublisher.cpp103
-rw-r--r--src/webchannel/qwebchannel.cpp3
-rw-r--r--tests/auto/webchannel/CMakeLists.txt7
-rw-r--r--tests/auto/webchannel/tst_webchannel.cpp113
-rw-r--r--tests/auto/webchannel/tst_webchannel.h20
6 files changed, 244 insertions, 4 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 629180a..f898948 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -14,7 +14,7 @@ project(QtWebChannel
set(QT_USE_FIXED_QT_ADD_RESOURCE_BASE TRUE)
find_package(Qt6 ${PROJECT_VERSION} CONFIG REQUIRED COMPONENTS BuildInternals Core) # special case
-find_package(Qt6 ${PROJECT_VERSION} CONFIG OPTIONAL_COMPONENTS Quick Test QuickTest WebSockets) # special case
+find_package(Qt6 ${PROJECT_VERSION} CONFIG OPTIONAL_COMPONENTS Concurrent Quick Test QuickTest WebSockets) # special case
if(INTEGRITY)
message(NOTICE "Skipping the build as the condition \"NOT INTEGRITY\" is not met.")
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
diff --git a/tests/auto/webchannel/CMakeLists.txt b/tests/auto/webchannel/CMakeLists.txt
index fea5db4..56921b1 100644
--- a/tests/auto/webchannel/CMakeLists.txt
+++ b/tests/auto/webchannel/CMakeLists.txt
@@ -23,3 +23,10 @@ qt_internal_extend_target(tst_webchannel CONDITION TARGET Qt::Qml
PUBLIC_LIBRARIES
Qt::Qml
)
+
+qt_internal_extend_target(tst_webchannel CONDITION TARGET Qt::Concurrent
+ DEFINES
+ WEBCHANNEL_TESTS_CAN_USE_CONCURRENT
+ PUBLIC_LIBRARIES
+ Qt::Concurrent
+)
diff --git a/tests/auto/webchannel/tst_webchannel.cpp b/tests/auto/webchannel/tst_webchannel.cpp
index c19bf68..da5f77c 100644
--- a/tests/auto/webchannel/tst_webchannel.cpp
+++ b/tests/auto/webchannel/tst_webchannel.cpp
@@ -38,6 +38,13 @@
#include <QJSEngine>
#endif
+#include <QPromise>
+#include <QTimer>
+
+#ifdef WEBCHANNEL_TESTS_CAN_USE_CONCURRENT
+#include <QtConcurrent>
+#endif
+
QT_USE_NAMESPACE
#ifdef WEBCHANNEL_TESTS_CAN_USE_JS_ENGINE
@@ -198,6 +205,59 @@ QVariantList convert_to_js(const TestStructVector &list)
}
}
+#if QT_CONFIG(future)
+QFuture<int> TestObject::futureIntResult() const
+{
+ return QtFuture::makeReadyFuture(42);
+}
+
+QFuture<int> TestObject::futureDelayedIntResult() const
+{
+ QPromise<int> p;
+ const auto f = p.future();
+ p.start();
+ QTimer::singleShot(10, this, [p=std::move(p)]() mutable {
+ p.addResult(7);
+ p.finish();
+ });
+ return f;
+}
+
+#ifdef WEBCHANNEL_TESTS_CAN_USE_CONCURRENT
+QFuture<int> TestObject::futureIntResultFromThread() const
+{
+ return QtConcurrent::run([] {
+ return 1337;
+ });
+}
+#endif
+
+QFuture<void> TestObject::futureVoidResult() const
+{
+ return QtFuture::makeReadyFuture();
+}
+
+QFuture<QString> TestObject::futureStringResult() const
+{
+ return QtFuture::makeReadyFuture<QString>("foo");
+}
+
+QFuture<int> TestObject::cancelledFuture() const
+{
+ QPromise<int> p;
+ auto f = p.future();
+ p.start();
+ f.cancel();
+ Q_ASSERT(f.isCanceled());
+ return f;
+}
+
+QFuture<int> TestObject::failedFuture() const
+{
+ return QtFuture::makeExceptionalFuture<int>(QException{});
+}
+#endif
+
TestWebChannel::TestWebChannel(QObject *parent)
: QObject(parent)
, m_dummyTransport(new DummyTransport(this))
@@ -412,6 +472,17 @@ void TestWebChannel::testInfoForObject()
addMethod(QStringLiteral("bindStringPropertyToStringProperty2"), "bindStringPropertyToStringProperty2()");
addMethod(QStringLiteral("setStringProperty2"), "setStringProperty2(QString)");
addMethod(QStringLiteral("method1"), "method1()");
+#if QT_CONFIG(future)
+ addMethod(QStringLiteral("futureIntResult"), "futureIntResult()");
+ addMethod(QStringLiteral("futureDelayedIntResult"), "futureDelayedIntResult()");
+#ifdef WEBCHANNEL_TESTS_CAN_USE_CONCURRENT
+ addMethod(QStringLiteral("futureIntResultFromThread"), "futureIntResultFromThread()");
+#endif
+ addMethod(QStringLiteral("futureVoidResult"), "futureVoidResult()");
+ addMethod(QStringLiteral("futureStringResult"), "futureStringResult()");
+ addMethod(QStringLiteral("cancelledFuture"), "cancelledFuture()");
+ addMethod(QStringLiteral("failedFuture"), "failedFuture()");
+#endif
QCOMPARE(info["methods"].toArray(), expected);
}
@@ -1309,6 +1380,48 @@ void TestWebChannel::testDeletionDuringMethodInvocation()
QCOMPARE(transport->messagesSent().size(), deleteChannel ? 0 : 1);
}
+#if QT_CONFIG(future)
+void TestWebChannel::testAsyncMethodReturningFuture_data()
+{
+ QTest::addColumn<QString>("methodName");
+ QTest::addColumn<QJsonValue>("result");
+
+ QTest::addRow("int") << "futureIntResult" << QJsonValue{42};
+ QTest::addRow("int-delayed") << "futureDelayedIntResult" << QJsonValue{7};
+#ifdef WEBCHANNEL_TESTS_CAN_USE_CONCURRENT
+ QTest::addRow("int-thread") << "futureIntResultFromThread" << QJsonValue{1337};
+#endif
+ QTest::addRow("void") << "futureVoidResult" << QJsonValue{};
+ QTest::addRow("QString") << "futureStringResult" << QJsonValue{"foo"};
+
+ QTest::addRow("cancelled") << "cancelledFuture" << QJsonValue{};
+ QTest::addRow("failed") << "failedFuture" << QJsonValue{};
+}
+
+void TestWebChannel::testAsyncMethodReturningFuture()
+{
+ QFETCH(QString, methodName);
+ QFETCH(QJsonValue, result);
+
+ QWebChannel channel;
+ TestObject obj;
+ channel.registerObject("testObject", &obj);
+
+ DummyTransport transport;
+ channel.connectTo(&transport);
+
+ transport.emitMessageReceived({
+ {"type", TypeInvokeMethod},
+ {"object", "testObject"},
+ {"method", methodName},
+ {"id", 1}
+ });
+
+ QTRY_COMPARE(transport.messagesSent().size(), 1);
+ QCOMPARE(transport.messagesSent().first().value("data"), result);
+}
+#endif
+
static QHash<QString, QObject*> createObjects(QObject *parent)
{
const int num = 100;
diff --git a/tests/auto/webchannel/tst_webchannel.h b/tests/auto/webchannel/tst_webchannel.h
index 071498f..c7b0a0e 100644
--- a/tests/auto/webchannel/tst_webchannel.h
+++ b/tests/auto/webchannel/tst_webchannel.h
@@ -36,6 +36,9 @@
#include <QJsonValue>
#include <QJsonObject>
#include <QJsonArray>
+#if QT_CONFIG(future)
+#include <QFuture>
+#endif
#include <QtWebChannel/QWebChannelAbstractTransport>
@@ -134,6 +137,18 @@ public:
Q_INVOKABLE void method1() {}
+#if QT_CONFIG(future)
+ Q_INVOKABLE QFuture<int> futureIntResult() const;
+ Q_INVOKABLE QFuture<int> futureDelayedIntResult() const;
+#ifdef WEBCHANNEL_TESTS_CAN_USE_CONCURRENT
+ Q_INVOKABLE QFuture<int> futureIntResultFromThread() const;
+#endif
+ Q_INVOKABLE QFuture<void> futureVoidResult() const;
+ Q_INVOKABLE QFuture<QString> futureStringResult() const;
+ Q_INVOKABLE QFuture<int> cancelledFuture() const;
+ Q_INVOKABLE QFuture<int> failedFuture() const;
+#endif
+
protected:
Q_INVOKABLE void method2() {}
@@ -364,6 +379,11 @@ private slots:
void testDeletionDuringMethodInvocation_data();
void testDeletionDuringMethodInvocation();
+#if QT_CONFIG(future)
+ void testAsyncMethodReturningFuture_data();
+ void testAsyncMethodReturningFuture();
+#endif
+
void benchClassInfo();
void benchInitializeClients();
void benchPropertyUpdates();