Skip to content

Commit

Permalink
co: prepare for using C++20 coroutines
Browse files Browse the repository at this point in the history
  • Loading branch information
MaxKellermann committed Sep 10, 2024
1 parent 37707b5 commit 42d9f52
Show file tree
Hide file tree
Showing 8 changed files with 704 additions and 0 deletions.
2 changes: 2 additions & 0 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,7 @@ if host_machine.system() == 'windows'
endif

subdir('src/util')
subdir('src/co')
subdir('src/lib/fmt')
subdir('src/io')
subdir('src/system')
Expand Down Expand Up @@ -428,6 +429,7 @@ ncmpc = executable('ncmpc',
include_directories: inc,
dependencies: [
util_dep,
coroutines_dep,
thread_dep,
event_dep,
pcre_dep,
Expand Down
214 changes: 214 additions & 0 deletions src/co/All.hxx
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
// SPDX-License-Identifier: BSD-2-Clause
// Copyright CM4all GmbH
// author: Max Kellermann <[email protected]>

#pragma once

#include "UniqueHandle.hxx"

#include <cassert>
#include <exception> // for std::terminate()
#include <tuple>
#include <type_traits>
#include <utility>

namespace Co {

/**
* A task that becomes ready when all tasks are ready. It does not
* pay attention to exceptions thrown by these tasks, and it will not
* obtain the results. After this task completes, it is up to the
* caller to co_wait all individual tasks.
*/
template<typename... Tasks>
class All final {

/**
* A task for Item::OnReady(). It allows set a continuation
* for final_suspend(), which is needed to resume the calling
* coroutine after all parameter tasks are done.
*/
class CompletionTask final {
public:
struct promise_type final {
std::coroutine_handle<> continuation;

[[nodiscard]]
auto initial_suspend() noexcept {
return std::suspend_always{};
}

void return_void() noexcept {
}

struct final_awaitable {
[[nodiscard]]
bool await_ready() const noexcept {
return false;
}

[[nodiscard]]
std::coroutine_handle<> await_suspend(std::coroutine_handle<promise_type> coro) noexcept {
const auto &promise = coro.promise();
return promise.continuation;
}

void await_resume() noexcept {
}
};

[[nodiscard]]
auto final_suspend() noexcept {
return final_awaitable{};
}

[[nodiscard]]
auto get_return_object() noexcept {
return CompletionTask(std::coroutine_handle<promise_type>::from_promise(*this));
}

void unhandled_exception() noexcept {
std::terminate();
}
};

private:
UniqueHandle<promise_type> coroutine;

[[nodiscard]]
explicit CompletionTask(std::coroutine_handle<promise_type> _coroutine) noexcept
:coroutine(_coroutine) {}

public:
[[nodiscard]]
CompletionTask() = default;

operator std::coroutine_handle<>() const noexcept {
return coroutine.get();
}

/**
* Set the coroutine that shall be resumed when this
* task finishes.
*/
void SetContinuation(std::coroutine_handle<> c) noexcept {
assert(c);
assert(!c.done());

auto &promise = coroutine->promise();
promise.continuation = c;
}
};

/**
* An individual task that was passed to this class; it
* contains the awaitable obtained by "operator co_await".
*/
template<typename Task>
struct Item {
All *parent;

using Awaitable = decltype(std::declval<Task>().operator co_await());
Awaitable awaitable;

CompletionTask task;

bool ready;

explicit Item(Awaitable _awaitable) noexcept
:awaitable(_awaitable),
ready(awaitable.await_ready()) {}

void SetParent(All &_parent) noexcept {
parent = &_parent;
}

[[nodiscard]]
bool await_ready() const noexcept {
return ready;
}

void await_suspend() noexcept {
if (ready)
return;

/* construct a callback task that will be
invoked as soon as the given task
completes */
task = OnReady();
const auto c = awaitable.await_suspend(task);
c.resume();
}

private:
/**
* Completion callback for the given task. It calls
* All::OnReady() to obtain a continuation that will
* be resumed by CompletionTask::final_suspend().
*/
[[nodiscard]]
CompletionTask OnReady() noexcept {
assert(!ready);

ready = true;

task.SetContinuation(parent->OnReady());
co_return;
}
};

std::tuple<Item<Tasks>...> awaitables;

std::coroutine_handle<> continuation;

public:
template<typename... Args>
[[nodiscard]]
All(Tasks&... tasks) noexcept
:awaitables(tasks.operator co_await()...) {

/* this kludge is necessary because we can't pass
another parameter to std::tuple */
std::apply([&](auto &...i){
(i.SetParent(*this), ...);
}, awaitables);
}

[[nodiscard]]
bool await_ready() const noexcept {
/* this task is ready when all given tasks are
ready */
return std::apply([&](const auto &...i){
return (i.await_ready() && ...);
}, awaitables);
}

void await_suspend(std::coroutine_handle<> _continuation) noexcept {
/* at least one task is not yet ready - call
await_suspend() on not-yet-ready tasks to install
the completion callback */

continuation = _continuation;

std::apply([&](auto &...i){
(i.await_suspend(), ...);
}, awaitables);
}

void await_resume() noexcept {
}

private:
[[nodiscard]]
std::coroutine_handle<> OnReady() noexcept {
assert(continuation);

/* if all tasks are ready, we can resume our
continuation, otherwise do nothing */
return await_ready()
? continuation
: std::noop_coroutine();
}
};

} // namespace Co
48 changes: 48 additions & 0 deletions src/co/AwaitableHelper.hxx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// SPDX-License-Identifier: BSD-2-Clause
// Copyright CM4all GmbH
// author: Max Kellermann <[email protected]>

#pragma once

#include "Compat.hxx"

#include <exception> // for std::rethrow_exception()

namespace Co {

/**
* This class provides some common boilerplate code for implementing
* an awaitable for a coroutine task. The task must have the field
* "continuation" and the methods IsReady() and TakeValue(). If
* #rethrow_error is true, then it must also have an "error" field.
*/
template<typename T,
bool rethrow_error=true>
class AwaitableHelper {
protected:
T &task;

public:
constexpr AwaitableHelper(T &_task) noexcept
:task(_task) {}

[[nodiscard]]
constexpr bool await_ready() const noexcept {
return task.IsReady();
}

void await_suspend(std::coroutine_handle<> _continuation) noexcept {
task.continuation = _continuation;
}

decltype(auto) await_resume() {
if constexpr (rethrow_error)
if (this->task.error)
std::rethrow_exception(this->task.error);

return this->task.TakeValue();
}
};

} // namespace Co

31 changes: 31 additions & 0 deletions src/co/Compat.hxx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// SPDX-License-Identifier: BSD-2-Clause
// Copyright CM4all GmbH
// author: Max Kellermann <[email protected]>

#pragma once

#include <utility>

#if defined(_LIBCPP_VERSION) && defined(__clang__) && (__clang_major__ < 14 || defined(__APPLE__))
/* libc++ until 14 has the coroutine definitions in the
std::experimental namespace */
/* the standard header is also missing in the Android NDK and on Apple
Xcode, even though LLVM upstream has them */

#include <experimental/coroutine>

namespace std {
using std::experimental::coroutine_handle;
using std::experimental::suspend_never;
using std::experimental::suspend_always;
using std::experimental::noop_coroutine;
}

#else /* not clang */

#include <coroutine>
#ifndef __cpp_impl_coroutine
#error Need -fcoroutines
#endif

#endif /* not clang */
Loading

0 comments on commit 42d9f52

Please sign in to comment.