Skip to content

Commit

Permalink
Implement coroutine features and guardThis feature (#177)
Browse files Browse the repository at this point in the history
By calling `co_await QCoro::thisCoro()` user can obtain
CoroutineFeatures object specific to the current coroutine, and through
it they can control behavior of the coroutine.

The one feature implemented so far is "guardThis", which monitors
the lifetime of a given QObject-derived object. If the object
is destroyed while the coroutine is suspended, when the time comes
to resume it, it is destroyed instead. This is mostly useful to
watch lifetime of `this`, so that when a member coroutine function
is suspended and the object is deleted in the meantime, it will
prevent the coroutine from continuing with a dangling `this` pointer
when resumed.
  • Loading branch information
danvratil committed Oct 7, 2023
1 parent c013c0d commit 701316b
Show file tree
Hide file tree
Showing 11 changed files with 306 additions and 30 deletions.
57 changes: 57 additions & 0 deletions docs/reference/coro/features.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<!--
SPDX-FileCopyrightText: 2023 Daniel Vrátil <dvratil@kde.org>
SPDX-License-Identifier: GFDL-1.3-or-later
-->

# Coroutine Features

!!! note "This feature is available since QCoro 0.10"

QCoro coroutines can be tuned at runtime to adjust their behavior.

## Obtaining Coroutine Features

```cpp
auto QCoro::thisCoro();
```

Current coroutine's features can be obtained by `co_await`ing on `QCoro::thisCoro()` to
obtain `QCoro::CoroutineFeatures` object:

```cpp
QCoro::Task<> myCoroutine() {
QCoro::CoroutineFeatures &features = co_await QCoro::thisCoro();
// use features here to tune behavior of this coroutine.
}
```

The `co_await` does not actually suspend the current coroutine, the features object is returned
immediatelly synchronously.

The following features can be configured for QCoro coroutines:

## `CoroutineFeatures::guardThis(QObject *)`

When coroutine is a member function of a QObject-derived class, it can happen that the `this` object
is deleted while the coroutine is suspended. When the coroutine is resumed, the program would crash
when it would try to dereference `this` due to use-after-free. To prevent this, a coroutine can
guard the `this` pointer. If the object is destroyed while the coroutine is suspended, it will terminate
immediatelly after resumption.

```cpp
QCoro::Task<> MyButton::onButtonClicked()
{
auto &features = co_await QCoro::thisCoro();
features.guardThis(this);

setLabel(tr("Downloading..."));
const auto result = co_await fetchData();
// If the button is destroyed while the coroutine is suspended waiting for the `fetchData()` coroutine,
// it will immediately terminate here once `fetchData()` finishes.
// If `this` is still a valid pointer at this point, the coroutine will continue as usual.

setLabel(tr("Done"));
}
```

3 changes: 2 additions & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,10 @@ nav:
- Coro:
- reference/coro/index.md
- QCoro::Task&lt;T>: reference/coro/task.md
- QCoro::coro(): reference/coro/coro.md
- qCoro(): reference/coro/coro.md
- QCoro::Generator&lt;T>: reference/coro/generator.md
- QCoro::AsyncGenerator&lt;T>: reference/coro/asyncgenerator.md
- Coroutine Features: reference/coro/features.md
- Core:
- reference/core/index.md
- Qt Signals: reference/core/signals.md
Expand Down
1 change: 1 addition & 0 deletions qcoro/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ add_qcoro_library(
HEADERS
concepts_p.h
coroutine.h
coroutinefeatures.h
macros_p.h
waitoperationbase_p.h
impl/connect.h
Expand Down
81 changes: 81 additions & 0 deletions qcoro/coroutinefeatures.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// SPDX-FileCopyrightText: 2023 Daniel Vrátil <dvratil@kde.org>
//
// SPDX-License-Identifier: MIT

#pragma once

#include "coroutine.h"

#include <optional>
#include <QPointer>

namespace QCoro {

/*! \cond internal */
namespace detail {
class TaskPromiseBase;
struct ThisCoroPromise;
} // namespace detail
/*! \endcond */


//! A special coroutine that returns features of the current coroutine
/*!
* A special coroutine that can be co_awaited inside a current QCoro coroutine
* to obtain QCoro::CoroutineFeatures object that allows the user to tune
* certain features and behavior of the current coroutine.
*
* @see QCoro::CoroutineFeatures
*/
auto thisCoro() -> detail::ThisCoroPromise;

//! Features of the current coroutine
/*!
* Allows configuring behavior of the current coroutine.
*
* Use `co_await QCoro::thisCoro()` to obtain the current coroutine's Features
* and modify them.
*
* @see QCoro::thisCoro()
*/
class CoroutineFeatures : public std::suspend_never
{
public:
CoroutineFeatures(const CoroutineFeatures &) = delete;
CoroutineFeatures(CoroutineFeatures &&) = delete;
CoroutineFeatures &operator=(const CoroutineFeatures &) = delete;
CoroutineFeatures &operator=(CoroutineFeatures &&) = delete;
~CoroutineFeatures() = default;

constexpr auto await_resume() noexcept -> CoroutineFeatures &;

//! Bind the coroutine lifetime to the lifetime of the given object
/*!
* Watches the given \c obj object. If the object is destroyed while the
* current coroutine is suspended, the coroutine will be destroyed immediately
* after resuming to prevent a use-after-free "this" pointer.
*
* \param obj QObject to observe its lifetime. Pass `nullptr` to stop observing.
*/
void guardThis(QObject *obj);

//! Returns the currently guarded QObject
/*!
* Returns the currently guarded QObject set by guardThis(). If no object was being
* guarded, the returned `std::optional` is empty. If the returned `std::optional`
* is not empty, then an object was specified to be guarded. When the QPointer inside
* the `std::optional` is empty it means that the guarded object has already been
* destroyed. Otherwise pointer to the guarded QObject is obtained.
*/
auto guardedThis() const -> const std::optional<QPointer<QObject>> &;

private:
std::optional<QPointer<QObject>> mGuardedThis;

explicit CoroutineFeatures() = default;
friend class QCoro::detail::TaskPromiseBase;
};

} // namespace QCoro

#include "impl/coroutinefeatures.h"
58 changes: 58 additions & 0 deletions qcoro/impl/coroutinefeatures.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// SPDX-FileCopyrightText: 2023 Daniel Vrátil <dvratil@kde.org>
//
// SPDX-License-Identifier: MIT


/*
* Do NOT include this file directly - include the QCoroTask header instead!
*/

#pragma once

#include "../qcorotask.h"

namespace QCoro {

// \cond internal
namespace detail {
struct ThisCoroPromise
{
private:
ThisCoroPromise() = default;
friend ThisCoroPromise QCoro::thisCoro();
};
} // namespace detail

// \endcond

inline auto thisCoro() -> detail::ThisCoroPromise
{
return detail::ThisCoroPromise{};
}

inline constexpr CoroutineFeatures & CoroutineFeatures::await_resume() noexcept
{
return *this;
}

inline void CoroutineFeatures::guardThis(QObject *obj)
{
if (obj == nullptr) {
mGuardedThis.reset();
} else {
mGuardedThis.emplace(obj);
}
}

inline auto CoroutineFeatures::guardedThis() const -> const std::optional<QPointer<QObject>> &
{
return mGuardedThis;
}

} // namespace QCoro






20 changes: 9 additions & 11 deletions qcoro/impl/task.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ namespace QCoro
{

template<typename T>
inline Task<T>::Task(std::coroutine_handle<promise_type> coroutine) : mCoroutine(coroutine) {}
inline Task<T>::Task(std::coroutine_handle<promise_type> coroutine)
: mCoroutine(coroutine)
{
mCoroutine.promise().refCoroutine();
}

template<typename T>
inline Task<T>::Task(Task &&other) noexcept : mCoroutine(other.mCoroutine) {
Expand All @@ -28,10 +32,7 @@ template<typename T>
inline auto Task<T>::operator=(Task &&other) noexcept -> Task & {
if (std::addressof(other) != this) {
if (mCoroutine) {
// The coroutine handle will be destroyed only after TaskFinalSuspend
if (mCoroutine.promise().setDestroyHandle()) {
mCoroutine.destroy();
}
mCoroutine.promise().derefCoroutine();
}

mCoroutine = other.mCoroutine;
Expand All @@ -42,11 +43,8 @@ inline auto Task<T>::operator=(Task &&other) noexcept -> Task & {

template<typename T>
inline Task<T>::~Task() {
if (!mCoroutine) return;

// The coroutine handle will be destroyed only after TaskFinalSuspend
if (mCoroutine.promise().setDestroyHandle()) {
mCoroutine.destroy();
if (mCoroutine) {
mCoroutine.promise().derefCoroutine();
}
}

Expand All @@ -67,7 +65,7 @@ inline auto Task<T>::operator co_await() const noexcept {
/*
* \return the result from the coroutine's promise, factically the
* value co_returned by the coroutine. */
auto await_resume() {
auto await_resume() -> T {
Q_ASSERT(this->mAwaitedCoroutine != nullptr);
if constexpr (!std::is_void_v<T>) {
return std::move(this->mAwaitedCoroutine.promise().result());
Expand Down
17 changes: 11 additions & 6 deletions qcoro/impl/taskfinalsuspend.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,22 @@ inline bool TaskFinalSuspend::await_ready() const noexcept {

template<typename Promise>
inline void TaskFinalSuspend::await_suspend(std::coroutine_handle<Promise> finishedCoroutine) noexcept {
auto &promise = finishedCoroutine.promise();

auto &finishedPromise = finishedCoroutine.promise();
for (auto &awaiter : mAwaitingCoroutines) {
awaiter.resume();
auto handle = std::coroutine_handle<TaskPromiseBase>::from_address(awaiter.address());
auto &awaitingPromise = handle.promise();
const auto &features = awaitingPromise.features();
if (const auto &guardedThis = features.guardedThis(); guardedThis.has_value() && guardedThis->isNull()) {
// We have a QPointer but it's null which means that the observed QObject has been destroyed.
awaitingPromise.derefCoroutine();
} else {
awaiter.resume();
}
}
mAwaitingCoroutines.clear();

// The handle will be destroyed here only if the associated Task has already been destroyed
if (promise.setDestroyHandle()) {
finishedCoroutine.destroy();
}
finishedPromise.derefCoroutine();
}

constexpr void TaskFinalSuspend::await_resume() const noexcept {}
Expand Down
38 changes: 32 additions & 6 deletions qcoro/impl/taskpromisebase.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
namespace QCoro::detail
{

inline TaskPromiseBase::TaskPromiseBase() {
refCoroutine();
}

inline std::suspend_never TaskPromiseBase::initial_suspend() const noexcept {
return {};
}
Expand All @@ -27,25 +31,29 @@ inline auto TaskPromiseBase::await_transform(T &&value) {
}

template<typename T>
inline auto TaskPromiseBase::await_transform(QCoro::Task<T> &&task) {
inline QCoro::Task<T> &&TaskPromiseBase::await_transform(QCoro::Task<T> &&task) {
return std::forward<QCoro::Task<T>>(task);
}

template<typename T>
inline auto &TaskPromiseBase::await_transform(QCoro::Task<T> &task) {
inline QCoro::Task<T> &TaskPromiseBase::await_transform(QCoro::Task<T> &task) {
return task;
}

template<Awaitable T>
inline auto && TaskPromiseBase::await_transform(T &&awaitable) {
inline T && TaskPromiseBase::await_transform(T &&awaitable) {
return std::forward<T>(awaitable);
}

template<Awaitable T>
inline auto &TaskPromiseBase::await_transform(T &awaitable) {
inline T &TaskPromiseBase::await_transform(T &awaitable) {
return awaitable;
}

inline QCoro::CoroutineFeatures &TaskPromiseBase::await_transform(detail::ThisCoroPromise &&) {
return mFeatures;
}

inline void TaskPromiseBase::addAwaitingCoroutine(std::coroutine_handle<> awaitingCoroutine) {
mAwaitingCoroutines.push_back(awaitingCoroutine);
}
Expand All @@ -54,8 +62,26 @@ inline bool TaskPromiseBase::hasAwaitingCoroutine() const {
return !mAwaitingCoroutines.empty();
}

inline bool TaskPromiseBase::setDestroyHandle() noexcept {
return mDestroyHandle.exchange(true, std::memory_order_acq_rel);
inline void TaskPromiseBase::derefCoroutine() {
--mRefCount;
if (mRefCount == 0) {
destroyCoroutine();
}
}

inline void TaskPromiseBase::refCoroutine() {
++mRefCount;
}

inline CoroutineFeatures &TaskPromiseBase::features() {
return mFeatures;
}

inline void TaskPromiseBase::destroyCoroutine() {
mRefCount = 0;

auto handle = std::coroutine_handle<TaskPromiseBase>::from_promise(*this);
handle.destroy();
}

} // namespace QCoro::detail
Loading

0 comments on commit 701316b

Please sign in to comment.