From 6aa9f0cc251d38ae84a7ca73060bb7aa442c3f6f Mon Sep 17 00:00:00 2001 From: Mat Sutcliffe Date: Tue, 26 Feb 2019 01:51:14 +0000 Subject: [PATCH] Issue #15 Created CPromise, a way to set the result of QFuture objects This enables several new features: * singleShot can return a QFuture. * a version of invokeMethod that returns a QFuture. * CGenericDBusInterface::callDBusFuture, which returns a QFuture. --- src/blackmisc/genericdbusinterface.h | 11 ++ src/blackmisc/invoke.h | 17 ++- src/blackmisc/promise.h | 142 ++++++++++++++++++++++ src/blackmisc/worker.h | 15 ++- tests/blackmisc/testworker/testworker.cpp | 59 +++++++++ tests/blackmisc/testworker/testworker.pro | 27 ++++ 6 files changed, 264 insertions(+), 7 deletions(-) create mode 100644 src/blackmisc/promise.h create mode 100644 tests/blackmisc/testworker/testworker.cpp create mode 100644 tests/blackmisc/testworker/testworker.pro diff --git a/src/blackmisc/genericdbusinterface.h b/src/blackmisc/genericdbusinterface.h index 45a1ee79c..64514ed50 100644 --- a/src/blackmisc/genericdbusinterface.h +++ b/src/blackmisc/genericdbusinterface.h @@ -12,12 +12,14 @@ #define BLACKMISC_GENERICDBUSINTERFACE_H #include "logmessage.h" +#include "blackmisc/promise.h" #include #include #include #include #include #include +#include #ifndef Q_MOC_RUN /*! @@ -102,6 +104,15 @@ namespace BlackMisc return pcw; } + //! Call DBus with asynchronous return as a future + template + QFuture callDBusFuture(QLatin1String method, Args&&... args) + { + auto sharedPromise = QSharedPointer>::create(); + this->callDBusAsync(method, [ = ](auto pcw) { sharedPromise->setResult(QDBusPendingReply(*pcw)); }, std::forward(args)...); + return sharedPromise->future(); + } + //! Cancel all asynchronous DBus calls which are currently pending //! \warning Don't call this from inside an async callback! void cancelAllPendingAsyncCalls() diff --git a/src/blackmisc/invoke.h b/src/blackmisc/invoke.h index 6620478f5..6551d4240 100644 --- a/src/blackmisc/invoke.h +++ b/src/blackmisc/invoke.h @@ -11,10 +11,14 @@ #ifndef BLACKMISC_INVOKE_H #define BLACKMISC_INVOKE_H -#include #include "blackmisc/typetraits.h" #include "blackmisc/integersequence.h" +#include "blackmisc/promise.h" +#include #include +#include +#include +#include namespace BlackMisc { @@ -59,6 +63,17 @@ namespace BlackMisc return invokeSlotImpl(std::forward(func), object, std::forward_as_tuple(std::forward(args)...), seq(), std::is_member_pointer>()); } + // Like QMetaObject::invokeMethod but the return value is accessed through a QFuture, and extra arguments can be provided. + template + auto invokeMethod(T *object, F &&func, Ts &&... args) + { + const auto invoker = [](auto &&... x) { return Private::invokeSlot(std::forward(x)...); }; + auto method = std::bind(invoker, std::forward(func), object, std::forward(args)...); + CPromise promise; + QMetaObject::invokeMethod(object, [promise, method = std::move(method)]() mutable { promise.setResultFrom(std::move(method)); }); + return promise.future(); + } + } //! \endcond } diff --git a/src/blackmisc/promise.h b/src/blackmisc/promise.h new file mode 100644 index 000000000..157fe91a9 --- /dev/null +++ b/src/blackmisc/promise.h @@ -0,0 +1,142 @@ +/* Copyright (C) 2017 + * swift Project Community / Contributors + * + * This file is part of swift project. It is subject to the license terms in the LICENSE file found in the top-level + * directory of this distribution. No part of swift project, including this file, may be copied, modified, propagated, + * or distributed except according to the terms contained in the LICENSE file. + */ + +//! \file + +#ifndef BLACKMISC_PROMISE_H +#define BLACKMISC_PROMISE_H + +#include +#include +#include +#include +#include +#include + +namespace BlackMisc +{ + + namespace Private + { + //! \private Shared data for CPromise. + //! \details QFutureInterface is undocumented but public, see also: + //! http://lists.qt-project.org/pipermail/development/2015-July/022572.html + template + class CPromiseData final : public QFutureInterface + { + public: + CPromiseData() { this->reportStarted(); } + ~CPromiseData() { if (this->isRunning()) { this->cancel(); } } + + CPromiseData(const CPromiseData &) = delete; + CPromiseData &operator =(const CPromiseData &) = delete; + }; + + //! \private + template + void doAfter(QFuture future, QObject *context, F &&func) + { + QSharedPointer> watcher(new QFutureWatcher, &QObject::deleteLater); + if (!context) { context = watcher.data(); } + QObject::connect(watcher.data(), &QFutureWatcher::finished, context, [watcher, func = std::forward(func)]() mutable + { + if (!watcher->isCanceled()) { func(watcher->future()); } + watcher.reset(); + }); + watcher->setFuture(future); + QCoreApplication::sendPostedEvents(watcher.data()); + } + } + + /*! + * Connect a slot or function to be invoked in the given context when a QFuture is finished. + */ + template + void doAfter(QFuture future, QObject *context, F &&func) + { + Private::doAfter(future, context, std::forward(func)); + } + + /*! + * Connect a slot or function to be invoked in the given context when a void QFuture is finished. + */ + template + void doAfter(QFuture future, QObject *context, F &&func) + { + Private::doAfter(future, context, [func = std::forward(func)](auto&&) { func(); }); + } + + /*! + * A promise-based interface to QFuture, similar to std::promise for std::future. + */ + template + class CPromise + { + public: + //! Return a future that can be used to access the result. + QFuture future() { return m_data->future(); } + + //! Mark the result as cancelled. + void abandon() { m_data->cancel(); m_data->reportFinished(); } + + //! Set the result value that will be made available through the future. + void setResult(const T &value) { m_data->reportFinished(&value); } + + //! Set the result value from the given future. Will block if future is not ready. + template + void setResult(QFuture future) { future.waitForFinished(); future.isCanceled() ? abandon() : setResult(future.result()); } + + //! When the given future is ready, use its result to set the result of this promise. + template + void chainResult(QFuture future) { doAfter(future, nullptr, [self = *this](auto &&f) mutable { self.setResult(f); }); } + + //! Invoke a functor and use its return value to set the result. + //! \details Useful for uniform syntax in generic code where T could be void. + template + void setResultFrom(F &&func) { setResult(std::forward(func)()); } + + private: + QSharedPointer> m_data { new Private::CPromiseData }; + }; + + /*! + * Specialization of CPromise for void futures. + */ + template <> + class CPromise + { + public: + //! Return a future that can be used to detect when the task is complete. + QFuture future() { return m_data->future(); } + + //! Mark the task as cancelled. + void abandon() { m_data->cancel(); m_data->reportFinished(); } + + //! Mark the task as complete. + void setResult() { m_data->reportFinished(); } + + //! Wait for the given future, then mark the task as complete. + template + void setResult(QFuture future) { future.waitForFinished(); future.isCanceled() ? abandon() : setResult(); } + + //! When the given future is ready, mark this promise as complete. + template + void chainResult(QFuture future) { doAfter(future, nullptr, [this](auto &&f) { setResult(f); }); } + + //! Invoke a functor and mark the task as complete. + //! \details Useful for uniform syntax in generic code where T could be void. + template + void setResultFrom(F &&func) { std::forward(func)(); setResult(); } + + private: + QSharedPointer> m_data { new Private::CPromiseData }; + }; + +} + +#endif diff --git a/src/blackmisc/worker.h b/src/blackmisc/worker.h index e6b70a382..85401480f 100644 --- a/src/blackmisc/worker.h +++ b/src/blackmisc/worker.h @@ -15,10 +15,12 @@ #include "blackmisc/connectionguard.h" #include "blackmisc/logcategorylist.h" #include "blackmisc/invoke.h" +#include "blackmisc/promise.h" #include "blackmisc/stacktrace.h" #include "blackmisc/identifiable.h" #include "blackmisc/variant.h" +#include #include #include #include @@ -43,24 +45,25 @@ namespace BlackMisc /*! * Starts a single-shot timer which will call a task in the thread of the given object when it times out. * - * Differs from QTimer::singleShot in that this implementation interacts better with QObject::moveToThread. + * Differs from QTimer::singleShot in that this implementation interacts better with QObject::moveToThread, + * and returns a QFuture which can be used to detect when the task has finished or obtain its return value. */ - //! @{ template - void singleShot(int msec, QObject *target, F &&task) + auto singleShot(int msec, QObject *target, F &&task) { + CPromise promise; QSharedPointer timer(new QTimer, [](QObject * o) { QMetaObject::invokeMethod(o, &QObject::deleteLater); }); timer->setSingleShot(true); timer->moveToThread(target->thread()); - QObject::connect(timer.data(), &QTimer::timeout, target, [trace = getStackTrace(), task = std::forward(task), timer]() mutable + QObject::connect(timer.data(), &QTimer::timeout, target, [trace = getStackTrace(), task = std::forward(task), timer, promise]() mutable { static_cast(trace); timer.clear(); - task(); + promise.setResultFrom(task); }); QMetaObject::invokeMethod(timer.data(), [t = timer.data(), msec] { t->start(msec); }); + return promise.future(); } - //! @} /*! * Just a subclass of QThread whose destructor waits for the thread to finish. diff --git a/tests/blackmisc/testworker/testworker.cpp b/tests/blackmisc/testworker/testworker.cpp new file mode 100644 index 000000000..160657504 --- /dev/null +++ b/tests/blackmisc/testworker/testworker.cpp @@ -0,0 +1,59 @@ +/* Copyright (C) 2017 + * swift Project Community / Contributors + * + * This file is part of swift project. It is subject to the license terms in the LICENSE file found in the top-level + * directory of this distribution. No part of swift project, including this file, may be copied, modified, propagated, + * or distributed except according to the terms contained in the LICENSE file. + */ + +//! \cond PRIVATE_TESTS + +/*! + * \file + * \ingroup testblackmisc + */ + +#include "blackmisc/worker.h" +#include "blackmisc/eventloop.h" +#include +#include + +using namespace BlackMisc; + +namespace BlackMiscTest +{ + //! Aviation classes basic tests + class CTestWorker : public QObject + { + Q_OBJECT + + public: + //! Standard test case constructor + explicit CTestWorker(QObject *parent = nullptr); + + private slots: + //! Testing single shot + void singleShot(); + }; + + CTestWorker::CTestWorker(QObject *parent) : QObject(parent) + { + // void + } + + void CTestWorker::singleShot() + { + QFuture future = BlackMisc::singleShot(0, this, []() { return 123; }); + CEventLoop::processEventsFor(0); + QVERIFY2(future.isFinished(), "Future is finished after slot has returned"); + QVERIFY2(future.result() == 123, "Future provides access to slot's return value"); + } + +} // namespace + +//! main +BLACKTEST_APPLESS_MAIN(BlackMiscTest::CTestWorker); + +#include "testworker.moc" + +//! \endcond diff --git a/tests/blackmisc/testworker/testworker.pro b/tests/blackmisc/testworker/testworker.pro new file mode 100644 index 000000000..14ef64ca1 --- /dev/null +++ b/tests/blackmisc/testworker/testworker.pro @@ -0,0 +1,27 @@ +load(common_pre) + +QT += core testlib + +TARGET = testslot +CONFIG -= app_bundle +CONFIG += blackconfig +CONFIG += blackmisc +CONFIG += testcase +CONFIG += no_testcase_installs + +TEMPLATE = app + +DEPENDPATH += \ + . \ + $$SourceRoot/src \ + $$SourceRoot/tests \ + +INCLUDEPATH += \ + $$SourceRoot/src \ + $$SourceRoot/tests \ + +SOURCES += testworker.cpp + +DESTDIR = $$DestRoot/bin + +load(common_post)