diff --git a/meson.build b/meson.build index e230d904..d2c2fccb 100644 --- a/meson.build +++ b/meson.build @@ -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') @@ -428,6 +429,7 @@ ncmpc = executable('ncmpc', include_directories: inc, dependencies: [ util_dep, + coroutines_dep, thread_dep, event_dep, pcre_dep, diff --git a/src/co/All.hxx b/src/co/All.hxx new file mode 100644 index 00000000..32cb4c3c --- /dev/null +++ b/src/co/All.hxx @@ -0,0 +1,214 @@ +// SPDX-License-Identifier: BSD-2-Clause +// Copyright CM4all GmbH +// author: Max Kellermann + +#pragma once + +#include "UniqueHandle.hxx" + +#include +#include // for std::terminate() +#include +#include +#include + +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 +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 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::from_promise(*this)); + } + + void unhandled_exception() noexcept { + std::terminate(); + } + }; + + private: + UniqueHandle coroutine; + + [[nodiscard]] + explicit CompletionTask(std::coroutine_handle _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 + struct Item { + All *parent; + + using Awaitable = decltype(std::declval().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...> awaitables; + + std::coroutine_handle<> continuation; + +public: + template + [[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 diff --git a/src/co/AwaitableHelper.hxx b/src/co/AwaitableHelper.hxx new file mode 100644 index 00000000..0be577ca --- /dev/null +++ b/src/co/AwaitableHelper.hxx @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: BSD-2-Clause +// Copyright CM4all GmbH +// author: Max Kellermann + +#pragma once + +#include "Compat.hxx" + +#include // 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 +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 + diff --git a/src/co/Compat.hxx b/src/co/Compat.hxx new file mode 100644 index 00000000..62e89ac0 --- /dev/null +++ b/src/co/Compat.hxx @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: BSD-2-Clause +// Copyright CM4all GmbH +// author: Max Kellermann + +#pragma once + +#include + +#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 + +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 +#ifndef __cpp_impl_coroutine +#error Need -fcoroutines +#endif + +#endif /* not clang */ diff --git a/src/co/InvokeTask.hxx b/src/co/InvokeTask.hxx new file mode 100644 index 00000000..0e93e1d8 --- /dev/null +++ b/src/co/InvokeTask.hxx @@ -0,0 +1,233 @@ +// SPDX-License-Identifier: BSD-2-Clause +// Copyright CM4all GmbH +// author: Max Kellermann + +#pragma once + +#include "UniqueHandle.hxx" +#include "Compat.hxx" +#include "util/BindMethod.hxx" + +#include +#include +#include + +namespace Co { + +namespace detail { + +template +class InvokePromise { + friend Task; + + using Callback = BoundMethod; + + Task *task; + + Callback callback; + + std::exception_ptr error; + +public: + InvokePromise() noexcept requires(lazy) = default; + + InvokePromise() noexcept requires(!lazy) + :callback(nullptr) {} + + InvokePromise(const InvokePromise &) = delete; + InvokePromise &operator=(const InvokePromise &) = delete; + + [[nodiscard]] + auto initial_suspend() noexcept { + assert(!error); + + if constexpr (lazy) + return std::suspend_always{}; + else + return std::suspend_never{}; + } + +private: + struct final_awaitable { + [[nodiscard]] + bool await_ready() const noexcept { + return false; + } + + bool await_suspend(std::coroutine_handle coro) noexcept { + assert(coro); + assert(coro.done()); + + auto &p = coro.promise(); + + if constexpr (!lazy) { + if (!p.callback) + return true; + } + + + assert(p.task); + assert(p.task->coroutine); + assert(p.callback); + + /* release the coroutine_handle; it will be + destroyed by our caller */ + (void)p.task->coroutine.release(); + + p.callback(std::move(p.error)); + + /* this resumes the original coroutine which + will then destroy the coroutine_handle */ + return false; + } + + void await_resume() const noexcept { + } + }; + +public: + [[nodiscard]] + auto final_suspend() noexcept { + return final_awaitable{}; + } + + void return_void() noexcept { + assert(!error); + } + + [[nodiscard]] + Task get_return_object() noexcept { + assert(!error); + + return Task{std::coroutine_handle::from_promise(*this)}; + } + + void unhandled_exception() noexcept(lazy) { + assert(!error); + + if constexpr (!lazy) { + if (!callback) { + /* the coroutine_handle will be + destroyed by the compiler after + rethrowing the exception */ + (void)task->coroutine.release(); + throw; + } + } + + error = std::current_exception(); + } +}; + +} // namespace detail + +/** + * A helper task which invokes a coroutine from synchronous code. + */ +class InvokeTask { +public: + using promise_type = detail::InvokePromise; + friend promise_type; + + using Callback = promise_type::Callback; + +private: + UniqueHandle coroutine; + + [[nodiscard]] + explicit InvokeTask(std::coroutine_handle _coroutine) noexcept + :coroutine(_coroutine) + { + } + +public: + [[nodiscard]] + InvokeTask() noexcept { + } + + operator bool() const noexcept { + return coroutine; + } + + void Start(Callback callback) noexcept { + assert(callback); + assert(coroutine); + assert(!coroutine->done()); + assert(!coroutine->promise().error); + + coroutine->promise().task = this; + coroutine->promise().callback = callback; + coroutine->resume(); + } +}; + +/** + * Like #InvokeTask, but the coroutine is not suspended initially. + */ +class EagerInvokeTask { +public: + using promise_type = detail::InvokePromise; + friend promise_type; + + using Callback = promise_type::Callback; + +private: + UniqueHandle coroutine; + + [[nodiscard]] + explicit EagerInvokeTask(std::coroutine_handle _coroutine) noexcept + :coroutine(_coroutine) + { + /* initialize promise.task early because its exception + handler needs to release the coroutine handle */ + coroutine->promise().task = this; + } + +public: + [[nodiscard]] + EagerInvokeTask() noexcept = default; + + EagerInvokeTask(EagerInvokeTask &&src) noexcept + :coroutine(std::move(src.coroutine)) + { + if (coroutine) { + assert(coroutine->promise().task == &src); + coroutine->promise().task = this; + } + } + + EagerInvokeTask &operator=(EagerInvokeTask &&src) noexcept { + coroutine = std::move(src.coroutine); + if (coroutine) { + assert(coroutine->promise().task == &src); + coroutine->promise().task = this; + } + + return *this; + } + + operator bool() const noexcept { + return coroutine; + } + + void Start(Callback callback) noexcept { + assert(callback); + assert(coroutine); + assert(coroutine->promise().task == this); + assert(!coroutine->promise().error); + + if (coroutine->done()) { + coroutine = {}; + callback({}); + } else { + coroutine->promise().callback = callback; + + /* not calling "resume()" here because the + coroutine was already resumed when it was + constructed; since it is not "done" yet, it + must be suspended currently */ + } + } +}; + +} // namespace Co diff --git a/src/co/UniqueHandle.hxx b/src/co/UniqueHandle.hxx new file mode 100644 index 00000000..948f5da0 --- /dev/null +++ b/src/co/UniqueHandle.hxx @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: BSD-2-Clause +// Copyright CM4all GmbH +// author: Max Kellermann + +#pragma once + +#include "Compat.hxx" + +#include + +namespace Co { + +/** + * Manage a std::coroutine_handle<> which is destroyed by the + * destructor. + */ +template +class UniqueHandle { + std::coroutine_handle value; + +public: + UniqueHandle() = default; + + explicit constexpr UniqueHandle(std::coroutine_handle h) noexcept + :value(h) {} + + UniqueHandle(UniqueHandle &&src) noexcept + :value(std::exchange(src.value, nullptr)) + { + } + + /* this overload allows casting a specialized handle to a + std::coroutine_handle */ + template + requires(std::is_void_v && !std::is_void_v

) + UniqueHandle(UniqueHandle

&&src) noexcept + :value(src.release()) + { + } + + ~UniqueHandle() noexcept { + if (value) + value.destroy(); + } + + auto &operator=(UniqueHandle &&src) noexcept { + using std::swap; + swap(value, src.value); + return *this; + } + + operator bool() const noexcept { + return (bool)value; + } + + const auto &get() const noexcept { + return value; + } + + const auto *operator->() const noexcept { + return &value; + } + +#ifdef __clang__ + /* the non-const overload is only needed for clang, because in + libc++11, some methods are not "const" */ + auto *operator->() noexcept { + return &value; + } +#endif + + [[nodiscard]] + auto release() noexcept { + return std::exchange(value, nullptr); + } +}; + +} // namespace Co diff --git a/src/co/meson.build b/src/co/meson.build new file mode 100644 index 00000000..8646a75b --- /dev/null +++ b/src/co/meson.build @@ -0,0 +1,11 @@ +coroutines_compile_args = [] + +if compiler.get_id() == 'gcc' + coroutines_compile_args += '-fcoroutines' +elif compiler.get_id() == 'clang' and compiler.version().version_compare('<15') + coroutines_compile_args += '-fcoroutines-ts' +endif + +coroutines_dep = declare_dependency( + compile_args: coroutines_compile_args, +) diff --git a/src/util/ReturnValue.hxx b/src/util/ReturnValue.hxx new file mode 100644 index 00000000..d04b9599 --- /dev/null +++ b/src/util/ReturnValue.hxx @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: BSD-2-Clause +// Copyright CM4all GmbH +// author: Max Kellermann + +#pragma once + +#include +#include +#include +#include + +/** + * Stores the return value of a function. It does not keep track of + * whether a value has been set already. + */ +template +class ReturnValue { + std::optional value; + +public: + /** + * Set the value. May be called at most once. + */ + template + void Set(U &&_value) noexcept { + assert(!value); + + value.emplace(std::forward(_value)); + } + + /** + * Get (and consume) the value. May be called at most once, + * but only if Set() has been called. + */ + [[nodiscard]] + decltype(auto) Get() && noexcept { + assert(value); + + return std::move(*value); + } +}; + +/** + * Specialization for certain types to eliminate the std::optional + * overhead. + */ +template +requires std::default_initializable && std::movable && std::destructible +class ReturnValue { + T value; + +public: + template + void Set(U &&_value) noexcept { + value = std::forward(_value); + } + + [[nodiscard]] + T &&Get() && noexcept { + return std::move(value); + } +}; + +/** + * This specialization supports returning references. + */ +template +class ReturnValue { + T *value; + +public: + void Set(T &_value) noexcept { + value = &_value; + } + + [[nodiscard]] + T &Get() && noexcept { + return *value; + } +}; + +template<> +class ReturnValue { +public: + void Set() noexcept {} + void Get() && noexcept {} +};