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 | |
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>
-rw-r--r-- | CMakeLists.txt | 2 | ||||
-rw-r--r-- | src/webchannel/qmetaobjectpublisher.cpp | 103 | ||||
-rw-r--r-- | src/webchannel/qwebchannel.cpp | 3 | ||||
-rw-r--r-- | tests/auto/webchannel/CMakeLists.txt | 7 | ||||
-rw-r--r-- | tests/auto/webchannel/tst_webchannel.cpp | 113 | ||||
-rw-r--r-- | tests/auto/webchannel/tst_webchannel.h | 20 |
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(); |