diff --git a/include/dpp/coro.h b/include/dpp/coro.h index e1670e1acf..03d318119e 100644 --- a/include/dpp/coro.h +++ b/include/dpp/coro.h @@ -21,6 +21,7 @@ #pragma once +#include "coro/awaitable.h" #include "coro/async.h" #include "coro/coroutine.h" #include "coro/job.h" diff --git a/include/dpp/coro/async.h b/include/dpp/coro/async.h index 1863498cac..4295ae1218 100644 --- a/include/dpp/coro/async.h +++ b/include/dpp/coro/async.h @@ -22,10 +22,12 @@ #include +#include + namespace dpp { -struct async_dummy { - int* dummy_shared_state = nullptr; +struct async_dummy : awaitable_dummy { + std::shared_ptr dummy_shared_state = nullptr; }; } @@ -34,7 +36,6 @@ struct async_dummy { #include "coro.h" -#include #include #include #include @@ -45,341 +46,44 @@ namespace dpp { namespace detail { -/** - * @brief Empty struct used for overload resolution. - */ -struct empty_tag_t{}; - namespace async { /** - * @brief Represents the step an std::async is at. - */ -enum class state_t { - /** - * @brief Request was sent but not co_await-ed. handle is nullptr, result_storage is not constructed. - */ - sent, - - /** - * @brief Request was co_await-ed. handle is valid, result_storage is not constructed. - */ - waiting, - - /** - * @brief Request was completed. handle is unknown, result_storage is valid. - */ - done, - - /** - * @brief Request was never co_await-ed. - */ - dangling -}; - -/** - * @brief State of the async and its callback. - * - * Defined outside of dpp::async because this seems to work better with Intellisense. + * @brief Shared state of the async and its callback, to be used across threads. */ template -struct async_callback_data { - /** - * @brief Number of references to this callback state. - */ - std::atomic ref_count{1}; - - /** - * @brief State of the awaitable and the API callback - */ - std::atomic state = state_t::sent; - +struct callback { /** - * @brief The stored result of the API call, stored as an array of bytes to directly construct in place + * @brief Promise object to set the result into */ - alignas(R) std::array result_storage; + std::shared_ptr> promise{nullptr}; /** - * @brief Handle to the coroutine co_await-ing on this API call - * - * @see std::coroutine_handle + * @brief Call operator, sets the value in the promise and notifies any awaiter + * + * @param v Callback value */ - std_coroutine::coroutine_handle<> coro_handle = nullptr; - - /** - * @brief Convenience function to construct the result in the storage and initialize its lifetime - * - * @warning This is only a convenience function, ONLY CALL THIS IN THE CALLBACK, before setting state to done. - */ - template - void construct_result(Ts&&... ts) { - // Standard-compliant type punning yay - std::construct_at(reinterpret_cast(result_storage.data()), std::forward(ts)...); + template + void operator()(const U& v) const requires (std::convertible_to) { + promise->set_value(v); } - - /** - * @brief Destructor. - * - * Also destroys the result if present. - */ - ~async_callback_data() { - if (state.load() == state_t::done) { - std::destroy_at(reinterpret_cast(result_storage.data())); - } - } -}; - -/** - * @brief Base class of dpp::async. - * - * @warning This class should not be used directly by a user, use dpp::async instead. - * @note This class contains all the functions used internally by co_await. It is intentionally opaque and a private base of dpp::async so a user cannot call await_suspend and await_resume directly. - */ -template -class async_base { + /** - * @brief Ref-counted callback, contains the callback logic and manages the lifetime of the callback data over multiple threads. - */ - struct shared_callback { - /** - * @brief Self-managed ref-counted pointer to the state data - */ - async_callback_data *state = new async_callback_data; - - /** - * @brief Callback function. - * - * Constructs the callback data, and if the coroutine was awaiting, resume it - * @param cback The result of the API call. - * @tparam V Forwarding reference convertible to R - */ - template V> - void operator()(V &&cback) const { - state->construct_result(std::forward(cback)); - if (auto previous_state = state->state.exchange(state_t::done); previous_state == state_t::waiting) { - state->coro_handle.resume(); - } - } - - /** - * @brief Main constructor, allocates a new callback_state object. - */ - shared_callback() = default; - - /** - * @brief Empty constructor, holds no state. - */ - explicit shared_callback(detail::empty_tag_t) noexcept : state{nullptr} {} - - /** - * @brief Copy constructor. Takes shared ownership of the callback state, increasing the reference count. - */ - shared_callback(const shared_callback &other) noexcept { - this->operator=(other); - } - - /** - * @brief Move constructor. Transfers ownership from another object, leaving intact the reference count. The other object releases the callback state. - */ - shared_callback(shared_callback &&other) noexcept { - this->operator=(std::move(other)); - } - - /** - * @brief Destructor. Releases the held reference and destroys if no other references exist. - */ - ~shared_callback() { - if (!state) { // Moved-from object - return; - } - - auto count = state->ref_count.fetch_sub(1); - if (count == 0) { - delete state; - } - } - - /** - * @brief Copy assignment. Takes shared ownership of the callback state, increasing the reference count. - */ - shared_callback &operator=(const shared_callback &other) noexcept { - state = other.state; - ++state->ref_count; - return *this; - } - - /** - * @brief Move assignment. Transfers ownership from another object, leaving intact the reference count. The other object releases the callback state. - */ - shared_callback &operator=(shared_callback &&other) noexcept { - state = std::exchange(other.state, nullptr); - return *this; - } - - /** - * @brief Function called by the async when it is destroyed when it was never co_awaited, signals to the callback to abort. - */ - void set_dangling() noexcept { - if (!state) { // moved-from object - return; - } - state->state.store(state_t::dangling); - } - - bool done(std::memory_order order = std::memory_order_seq_cst) const noexcept { - return (state->state.load(order) == state_t::done); - } - - /** - * @brief Convenience function to get the shared callback state's result. - * - * @warning It is UB to call this on a callback whose state is anything else but state_t::done. - */ - R &get_result() noexcept { - assert(state && done()); - return (*reinterpret_cast(state->result_storage.data())); - } - - /** - * @brief Convenience function to get the shared callback state's result. - * - * @warning It is UB to call this on a callback whose state is anything else but state_t::done. - */ - const R &get_result() const noexcept { - assert(state && done()); - return (*reinterpret_cast(state->result_storage.data())); - } - }; - - /** - * @brief Shared state of the async and its callback, to be used across threads. - */ - shared_callback api_callback{nullptr}; - -public: - /** - * @brief Construct an async object wrapping an object method, the call is made immediately by forwarding to std::invoke and can be awaited later to retrieve the result. + * @brief Call operator, sets the value in the promise and notifies any awaiter * - * @param obj The object to call the method on - * @param fun The method of the object to call. Its last parameter must be a callback taking a parameter of type R - * @param args Parameters to pass to the method, excluding the callback + * @param v Callback value */ - template -#ifndef _DOXYGEN_ - requires std::invocable> -#endif - explicit async_base(Obj &&obj, Fun &&fun, Args&&... args) : api_callback{} { - std::invoke(std::forward(fun), std::forward(obj), std::forward(args)..., api_callback); + template + void operator()(U&& v) const requires (std::convertible_to) { + promise->set_value(std::move(v)); } - + /** - * @brief Construct an async object wrapping an invokeable object, the call is made immediately by forwarding to std::invoke and can be awaited later to retrieve the result. - * - * @param fun The object to call using std::invoke. Its last parameter must be a callable taking a parameter of type R - * @param args Parameters to pass to the object, excluding the callback + * @brief Call operator, sets the value in the promise and notifies any awaiter */ - template -#ifndef _DOXYGEN_ - requires std::invocable> -#endif - explicit async_base(Fun &&fun, Args&&... args) : api_callback{} { - std::invoke(std::forward(fun), std::forward(args)..., api_callback); - } - - /** - * @brief Construct an empty async. Using `co_await` on an empty async is undefined behavior. - */ - async_base() noexcept : api_callback{detail::empty_tag_t{}} {} - - /** - * @brief Destructor. If any callback is pending it will be aborted. - */ - ~async_base() { - api_callback.set_dangling(); - } - - /** - * @brief Copy constructor is disabled - */ - async_base(const async_base &) = delete; - - /** - * @brief Move constructor - * - * NOTE: Despite being marked noexcept, this function uses std::lock_guard which may throw. The implementation assumes this can never happen, hence noexcept. Report it if it does, as that would be a bug. - * - * @remark Using the moved-from async after this function is undefined behavior. - * @param other The async object to move the data from. - */ - async_base(async_base &&other) noexcept = default; - - /** - * @brief Copy assignment is disabled - */ - async_base &operator=(const async_base &) = delete; - - /** - * @brief Move assignment operator. - * - * NOTE: Despite being marked noexcept, this function uses std::lock_guard which may throw. The implementation assumes this can never happen, hence noexcept. Report it if it does, as that would be a bug. - * - * @remark Using the moved-from async after this function is undefined behavior. - * @param other The async object to move the data from - */ - async_base &operator=(async_base &&other) noexcept = default; - - /** - * @brief Check whether or not co_await-ing this would suspend the caller, i.e. if we have the result or not - * - * @return bool Whether we already have the result of the API call or not - */ - [[nodiscard]] bool await_ready() const noexcept { - return api_callback.done(); - } - - /** - * @brief Second function called by the standard library when the object is co-awaited, if await_ready returned false. - * - * Checks again for the presence of the result, if absent, signals to suspend and keep track of the calling coroutine for the callback to resume. - * - * @remark Do not call this manually, use the co_await keyword instead. - * @param caller The handle to the coroutine co_await-ing and being suspended - */ - [[nodiscard]] bool await_suspend(detail::std_coroutine::coroutine_handle<> caller) noexcept { - auto sent = state_t::sent; - api_callback.state->coro_handle = caller; - return api_callback.state->state.compare_exchange_strong(sent, state_t::waiting); // true (suspend) if `sent` was replaced with `waiting` -- false (resume) if the value was not `sent` (`done` is the only other option) - } - - /** - * @brief Function called by the standard library when the async is resumed. Its return value is what the whole co_await expression evaluates to - * - * @remark Do not call this manually, use the co_await keyword instead. - * @return The result of the API call as an lvalue reference. - */ - R& await_resume() & noexcept { - return api_callback.get_result(); - } - - - /** - * @brief Function called by the standard library when the async is resumed. Its return value is what the whole co_await expression evaluates to - * - * @remark Do not call this manually, use the co_await keyword instead. - * @return The result of the API call as a const lvalue reference. - */ - const R& await_resume() const& noexcept { - return api_callback.get_result(); - } - - /** - * @brief Function called by the standard library when the async is resumed. Its return value is what the whole co_await expression evaluates to - * - * @remark Do not call this manually, use the co_await keyword instead. - * @return The result of the API call as an rvalue reference. - */ - R&& await_resume() && noexcept { - return std::move(api_callback.get_result()); + void operator()() const requires (std::is_void_v) + { + promise->set_value(); } }; @@ -400,19 +104,21 @@ struct confirmation_callback_t; * @tparam R The return type of the API call. Defaults to confirmation_callback_t */ template -class async : private detail::async::async_base { +class async : public awaitable { /** - * @brief Internal use only base class. It serves to prevent await_suspend and await_resume from being used directly. - * - * @warning For internal use only, do not use. - * @see operator co_await() + * @brief Callable object to pass to API calls */ - friend class detail::async::async_base; + detail::async::callback api_callback{}; + + /** + * @brief Internal promise constructor, grabs a promise object for the callback to use + */ + explicit async(std::shared_ptr> &&promise) : awaitable{promise.get()}, api_callback{std::move(promise)} {} public: - using detail::async::async_base::async_base; // use async_base's constructors. unfortunately on clang this doesn't include the templated ones so we have to delegate below - using detail::async::async_base::operator=; // use async_base's assignment operator - using detail::async::async_base::await_ready; // expose await_ready as public + using awaitable::awaitable; // use awaitable's constructors + using awaitable::operator=; // use async_base's assignment operator + using awaitable::await_ready; // expose await_ready as public /** * @brief The return type of the API call. Defaults to confirmation_callback_t @@ -430,7 +136,9 @@ class async : private detail::async::async_base { #ifndef _DOXYGEN_ requires std::invocable> #endif - explicit async(Obj &&obj, Fun &&fun, Args&&... args) : detail::async::async_base{std::forward(obj), std::forward(fun), std::forward(args)...} {} + explicit async(Obj &&obj, Fun &&fun, Args&&... args) : async{std::make_shared>()} { + std::invoke(std::forward(fun), std::forward(obj), std::forward(args)..., api_callback); + } /** * @brief Construct an async object wrapping an invokeable object, the call is made immediately by forwarding to std::invoke and can be awaited later to retrieve the result. @@ -442,82 +150,35 @@ class async : private detail::async::async_base { #ifndef _DOXYGEN_ requires std::invocable> #endif - explicit async(Fun &&fun, Args&&... args) : detail::async::async_base{std::forward(fun), std::forward(args)...} {} - -#ifdef _DOXYGEN_ // :) - /** - * @brief Construct an empty async. Using `co_await` on an empty async is undefined behavior. - */ - async() noexcept; - - /** - * @brief Destructor. If any callback is pending it will be aborted. - */ - ~async(); - - /** - * @brief Copy constructor is disabled - */ - async(const async &); - - /** - * @brief Move constructor - * - * NOTE: Despite being marked noexcept, this function uses std::lock_guard which may throw. The implementation assumes this can never happen, hence noexcept. Report it if it does, as that would be a bug. - * - * @remark Using the moved-from async after this function is undefined behavior. - * @param other The async object to move the data from. - */ - async(async &&other) noexcept = default; - - /** - * @brief Copy assignment is disabled - */ - async &operator=(const async &) = delete; + explicit async(Fun &&fun, Args&&... args) : async{std::make_shared>()} { + std::invoke(std::forward(fun), std::forward(args)..., api_callback); + } /** - * @brief Move assignment operator. - * - * NOTE: Despite being marked noexcept, this function uses std::lock_guard which may throw. The implementation assumes this can never happen, hence noexcept. Report it if it does, as that would be a bug. - * - * @remark Using the moved-from async after this function is undefined behavior. - * @param other The async object to move the data from + * @brief Copy constructor is disabled. */ - async &operator=(async &&other) noexcept = default; + async(const async&) = delete; /** - * @brief Check whether or not co_await-ing this would suspend the caller, i.e. if we have the result or not - * - * @return bool Whether we already have the result of the API call or not + * @brief Move constructor, moves the awaitable async object */ - [[nodiscard]] bool await_ready() const noexcept; -#endif - + async(async&&) = default; + /** - * @brief Suspend the caller until the request completes. - * - * @return On resumption, this expression evaluates to the result object of type R, as a reference. + * @brief Copy assignment operator is disabled. */ - [[nodiscard]] auto& operator co_await() & noexcept { - return static_cast&>(*this); - } - + async& operator=(const async&) = delete; + /** - * @brief Suspend the caller until the request completes. - * - * @return On resumption, this expression evaluates to the result object of type R, as a const reference. + * @brief Move assignment operator, moves the awaitable async object */ - [[nodiscard]] const auto& operator co_await() const & noexcept { - return static_cast const&>(*this); - } + async& operator=(async&&) = default; /** - * @brief Suspend the caller until the request completes. - * - * @return On resumption, this expression evaluates to the result object of type R, as an rvalue reference. + * @brief Destructor, signals to the callback that the async object is gone and shouldn't be notified of the result */ - [[nodiscard]] auto&& operator co_await() && noexcept { - return static_cast&&>(*this); + ~async() { + this->abandon(); } }; diff --git a/include/dpp/coro/awaitable.h b/include/dpp/coro/awaitable.h new file mode 100644 index 0000000000..2fefb1fd4e --- /dev/null +++ b/include/dpp/coro/awaitable.h @@ -0,0 +1,752 @@ +/************************************************************************************ + * + * D++, A Lightweight C++ library for Discord + * + * Copyright 2022 Craig Edwards and D++ contributors + * (https://github.com/brainboxdotcc/DPP/graphs/contributors) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ************************************************************************************/ + +#pragma once + +#include + +#include + +namespace dpp { + +struct awaitable_dummy { + int *promise_dummy = nullptr; +}; + +} + +#ifdef DPP_CORO + +#include + +// Do not include as coro.h includes or depending on clang version +#include +#include +#include +#include +#include + +namespace dpp { + +namespace detail::promise { + +/** + * @brief State of a promise + */ +enum state_flags { + /** + * @brief Promise is empty + */ + sf_none = 0b0000000, + + /** + * @brief Promise has spawned an awaitable + */ + sf_has_awaitable = 0b00000001, + + /** + * @brief Promise is being awaited + */ + sf_awaited = 0b00000010, + + /** + * @brief Promise has a result + */ + sf_ready = 0b00000100, + + /** + * @brief Promise has completed, no more results are expected + */ + sf_done = 0b00001000, + + /** + * @brief Promise was broken - future or promise is gone + */ + sf_broken = 0b0010000 +}; + +template +class promise_base; + +/** + * @brief Empty result from void-returning awaitable + */ +struct empty{}; + +/** + * @brief Variant for the 3 conceptual values of a coroutine: + */ +template +using result_t = std::variant, empty, T>, std::exception_ptr>; + +template +void spawn_sync_wait_job(auto* awaitable, std::condition_variable &cv, auto&& result); + +} /* namespace detail::promise */ + +template +class basic_awaitable { +protected: + /** + * @brief Implementation for sync_wait. This is code used by sync_wait, sync_wait_for, sync_wait_until. + * + * @tparam Timed Whether the wait function times out or not + * @param do_wait Function to do the actual wait on the cv + * @return If T is void, returns a boolean for which true means the awaitable completed, false means it timed out. + * @return If T is non-void, returns a std::optional for which an absence of value means timed out. + */ + template + auto sync_wait_impl(auto&& do_wait) { + using result_type = decltype(detail::co_await_resolve(std::declval()).await_resume()); + using variant_type = detail::promise::result_t; + variant_type result; + std::condition_variable cv; + + detail::promise::spawn_sync_wait_job(static_cast(this), cv, result); + do_wait(cv, result); + /* + * Note: we use .index() here to support dpp::promise & dpp::promise :D + */ + if (result.index() == 2) { + std::rethrow_exception(std::get<2>(result)); + } + if constexpr (!Timed) { // no timeout + if constexpr (!std::is_void_v) { + return std::get<1>(result); + } + } else { // timeout + if constexpr (std::is_void_v) { + return result.index() == 1 ? true : false; + } else { + return result.index() == 1 ? std::optional{std::get<1>(result)} : std::nullopt; + } + } + } + +public: + /** + * @brief Blocks this thread and waits for the awaitable to finish. + * + * @attention This will BLOCK THE THREAD. It is likely you want to use co_await instead. + * @return If T is void, returns a boolean for which true means the awaitable completed, false means it timed out. + * @return If T is non-void, returns a std::optional for which an absence of value means timed out. + */ + auto sync_wait() { + return sync_wait_impl([](std::condition_variable &cv, auto&& result) { + std::mutex m{}; + std::unique_lock lock{m}; + cv.wait(lock, [&result] { return result.index() != 0; }); + }); + } + + /** + * @brief Blocks this thread and waits for the awaitable to finish. + * + * @attention This will BLOCK THE THREAD. It is likely you want to use co_await instead. + * @param duration Maximum duration to wait for + * @return If T is void, returns a boolean for which true means the awaitable completed, false means it timed out. + * @return If T is non-void, returns a std::optional for which an absence of value means timed out. + */ + template + auto sync_wait_for(const std::chrono::duration& duration) { + return sync_wait_impl([duration](std::condition_variable &cv, auto&& result) { + std::mutex m{}; + std::unique_lock lock{m}; + cv.wait_for(lock, duration, [&result] { return result.index() != 0; }); + }); + } + + /** + * @brief Blocks this thread and waits for the awaitable to finish. + * + * @attention This will BLOCK THE THREAD. It is likely you want to use co_await instead. + * @param time Maximum time point to wait for + * @return If T is void, returns a boolean for which true means the awaitable completed, false means it timed out. + * @return If T is non-void, returns a std::optional for which an absence of value means timed out. + */ + template + auto sync_wait_until(const std::chrono::time_point &time) { + return sync_wait_impl([time](std::condition_variable &cv, auto&& result) { + std::mutex m{}; + std::unique_lock lock{m}; + cv.wait_until(lock, time, [&result] { return result.index() != 0; }); + }); + } +}; + +/** + * @brief Generic awaitable class, represents a future value that can be co_await-ed on. + * + * Roughly equivalent of std::future for coroutines, with the crucial distinction that the future does not own a reference to a "shared state". + * It holds a non-owning reference to the promise, which must be kept alive for the entire lifetime of the awaitable. + * + * @tparam T Type of the asynchronous value + * @see promise + */ +template +class awaitable : public basic_awaitable> { +protected: + friend class detail::promise::promise_base; + + using shared_state = detail::promise::promise_base; + using state_flags = detail::promise::state_flags; + + /** + * @brief The type of the result produced by this task. + */ + using result_type = T; + + /** + * @brief Non-owning pointer to the promise, which must be kept alive for the entire lifetime of the awaitable. + */ + shared_state *state_ptr = nullptr; + + /** + * @brief Construct from a promise. + * + * @param promise The promise to refer to. + */ + awaitable(shared_state *promise) noexcept : state_ptr{promise} {} + + /** + * @brief Abandons the promise. + * + * Set the promise's state to broken and unlinks this awaitable. + * + * @return uint8_t Flags previously held before setting them to broken + */ + uint8_t abandon(); + /** + * @brief Awaiter returned by co_await. + * + * Contains the await_ready, await_suspend and await_resume functions required by the C++ standard. + * This class is CRTP-like, in that it will refer to an object derived from awaitable. + * + * @tparam Derived Type of reference to refer to the awaitable. + */ + template + struct awaiter { + Derived awaitable_obj; + + /** + * @brief First function called by the standard library when co_await-ing this object. + * + * @throws dpp::logic_exception If the awaitable's valid() would return false. + * @return bool Whether the result is ready, in which case we don't need to suspend + */ + bool await_ready() const; + + /** + * @brief Second function called by the standard library when co_await-ing this object. + * + * @throws dpp::logic_exception If the awaitable's valid() would return false. + * At this point the coroutine frame was allocated and suspended. + * + * @return bool Whether we do need to suspend or not + */ + bool await_suspend(detail::std_coroutine::coroutine_handle<> handle); + + /** + * @brief Third and final function called by the standard library when co_await-ing this object, after resuming. + * + * @throw ? Any exception that occured during the retrieval of the value will be thrown + * @return T The result. + */ + T await_resume(); + }; + +public: + /** + * @brief Construct an empty awaitable. + * + * Such an awaitable must be assigned a promise before it can be awaited. + */ + awaitable() = default; + + /** + * @brief Copy construction is disabled. + */ + awaitable(const awaitable&) = delete; + + /** + * @brief Move from another awaitable. + * + * @param rhs The awaitable to move from, left in an unspecified state after this. + */ + awaitable(awaitable&& rhs) noexcept : state_ptr(std::exchange(rhs.state_ptr, nullptr)) { + } + + /** + * @brief Title :) + * + * We use this in the destructor + */ + void if_this_causes_an_invalid_read_your_promise_was_destroyed_before_your_awaitable____check_your_promise_lifetime() { + abandon(); + } + + /** + * @brief Destructor. + * + * May signal to the promise that it was destroyed. + */ + ~awaitable(); + + /** + * @brief Copy assignment is disabled. + */ + awaitable& operator=(const awaitable&) = delete; + + /** + * @brief Move from another awaitable. + * + * @param rhs The awaitable to move from, left in an unspecified state after this. + * @return *this + */ + awaitable& operator=(awaitable&& rhs) noexcept { + abandon(); + state_ptr = std::exchange(rhs.state_ptr, nullptr); + return *this; + } + + /** + * @brief Check whether this awaitable refers to a valid promise. + * + * @return bool Whether this awaitable refers to a valid promise or not + */ + bool valid() const noexcept; + + /** + * @brief Check whether or not co_await-ing this would suspend the caller, i.e. if we have the result or not + * + * @return bool Whether we already have the result or not + */ + bool await_ready() const; + + /** + * @brief Overload of the co_await operator. + * + * @return Returns an @ref awaiter referencing this awaitable. + */ + template + requires (std::is_base_of_v>) + friend awaiter operator co_await(Derived& obj) noexcept { + return {obj}; + } + + /** + * @brief Overload of the co_await operator. Returns an @ref awaiter referencing this awaitable. + * + * @return Returns an @ref awaiter referencing this awaitable. + */ + template + requires (std::is_base_of_v>) + friend awaiter operator co_await(Derived&& obj) noexcept { + return {std::move(obj)}; + } +}; + +namespace detail::promise { + +/** + * @brief Base class defining logic common to all promise types, aka the "write" end of an awaitable. + */ +template +class promise_base { +protected: + friend class awaitable; + + /** + * @brief Variant representing one of either 3 states of the result value : empty, result, exception. + */ + using storage_type = result_t; + + /** + * @brief State of the result value. + * + * @see storage_type + * + * @note use .index() instead of std::holds_alternative to support promise_base and promise_base :) + */ + storage_type value = std::monostate{}; + + /** + * @brief State of the awaitable tied to this promise. + */ + std::atomic state = sf_none; + + /** + * @brief Coroutine handle currently awaiting the completion of this promise. + */ + std_coroutine::coroutine_handle<> awaiter = nullptr; + + /** + * @brief Check if the result is empty, throws otherwise. + * + * @throw dpp::logic_exception if the result isn't empty. + */ + void throw_if_not_empty() { + if (value.index() != 0) [[unlikely]] { + throw dpp::logic_exception("cannot set a value on a promise that already has one"); + } + } + + /** + * @brief Unlinks this promise from its currently linked awaiter and returns it. + * + * At the time of writing this is only used in the case of a serious internal error in dpp::task. + * Avoid using this as this will crash if the promise is used after this. + */ + std_coroutine::coroutine_handle<> release_awaiter() { + return std::exchange(awaiter, nullptr); + } + + /** + * @brief Construct a new promise, with empty result. + */ + promise_base() = default; + + /** + * @brief Copy construction is disabled. + */ + promise_base(const promise_base&) = delete; + + /** + * @brief Move construction is disabled. + * + * awaitable hold a pointer to this object so moving is not possible. + */ + promise_base(promise_base&& rhs) = delete; + +public: + /** + * @brief Copy assignment is disabled. + */ + promise_base &operator=(const promise_base&) = delete; + + /** + * @brief Move assignment is disabled. + */ + promise_base &operator=(promise_base&& rhs) = delete; + + /** + * @brief Set this promise to an exception and resume any awaiter. + * + * @tparam Notify Whether to resume any awaiter or not. + * @throws dpp::logic_exception if the promise is not empty. + * @throws ? Any exception thrown by the coroutine if resumed will propagate + */ + template + void set_exception(std::exception_ptr ptr) { + throw_if_not_empty(); + value.template emplace<2>(std::move(ptr)); + [[maybe_unused]] auto previous_value = this->state.fetch_or(sf_ready, std::memory_order::acq_rel); + if constexpr (Notify) { + if ((previous_value & sf_awaited) != 0) { + this->awaiter.resume(); + } + } + } + + /** + * @brief Notify a currently awaiting coroutine that the result is ready. + * + * @note This may resume the coroutine on the current thread. + * @throws ? Any exception thrown by the coroutine if resumed will propagate + */ + void notify_awaiter() { + if ((state.load(std::memory_order::acquire) & sf_awaited) != 0) { + awaiter.resume(); + } + } + + /** + * @brief Get an awaitable object for this promise. + * + * @throws dpp::logic_exception if get_awaitable has already been called on this object. + * @return awaitable An object that can be co_await-ed to retrieve the value of this promise. + */ + awaitable get_awaitable() { + uint8_t previous_flags = state.fetch_or(sf_has_awaitable, std::memory_order::relaxed); + if (previous_flags & sf_has_awaitable) [[unlikely]] { + throw dpp::logic_exception{"an awaitable was already created from this promise"}; + } + return {this}; + } +}; + +} + +/** + * @brief Generic promise class, represents the owning potion of an asynchronous value. + * + * This class is roughly equivalent to std::promise, with the crucial distinction that the promise *IS* the shared state. + * As such, the promise needs to be kept alive for the entire time a value can be retrieved. + * + * @tparam T Type of the asynchronous value + * @see awaitable + */ +template +class basic_promise : public detail::promise::promise_base { +public: + using detail::promise::promise_base::promise_base; + using detail::promise::promise_base::operator=; + + /** + * @brief Construct the result in place by forwarding the arguments, and by default resume any awaiter. + * + * @tparam Notify Whether to resume any awaiter or not. + * @throws dpp::logic_exception if the promise is not empty. + */ + template + requires (std::constructible_from) + void emplace_value(Args&&... args) { + this->throw_if_not_empty(); + try { + this->value.template emplace<1>(std::forward(args)...); + } catch (...) { + this->value.template emplace<2>(std::current_exception()); + } + [[maybe_unused]] auto previous_value = this->state.fetch_or(detail::promise::sf_ready, std::memory_order::acq_rel); + if constexpr (Notify) { + if (previous_value & detail::promise::sf_awaited) { + this->awaiter.resume(); + } + } + } + + /** + * @brief Construct the result by copy, and resume any awaiter. + * + * @tparam Notify Whether to resume any awaiter or not. + * @throws dpp::logic_exception if the promise is not empty. + */ + template + requires (std::convertible_to) + void set_value(const U& v) { + emplace_value(v); + } + + /** + * @brief Construct the result by move, and resume any awaiter. + * + * @tparam Notify Whether to resume any awaiter or not. + * @throws dpp::logic_exception if the promise is not empty. + */ + template + requires (std::convertible_to) + void set_value(U&& v) { + emplace_value(std::move(v)); + } + + /** + * @brief Construct the result by move, and resume any awaiter. + * + * @tparam Notify Whether to resume any awaiter or not. + * @throws dpp::logic_exception if the promise is not empty. + */ + template + requires (std::is_void_v) + void set_value() { + this->throw_if_not_empty(); + this->value.template emplace<1>(); + [[maybe_unused]] auto previous_value = this->state.fetch_or(detail::promise::sf_ready, std::memory_order::acq_rel); + if constexpr (Notify) { + if (previous_value & detail::promise::sf_awaited) { + this->awaiter.resume(); + } + } + } +}; + +/** + * @brief Generic promise class, represents the owning potion of an asynchronous value. + * + * This class is roughly equivalent to std::promise, with the crucial distinction that the promise *IS* the shared state. + * As such, the promise needs to be kept alive for the entire time a value can be retrieved. + * + * The difference between basic_promise and this object is that this one is moveable as it wraps an underlying basic_promise in a std::unique_ptr. + * + * @see awaitable + */ +template +class moveable_promise { + /** + * @brief Shared state, wrapped in a unique_ptr to allow move without disturbing an awaitable's promise pointer. + */ + std::unique_ptr> shared_state = std::make_unique>(); + +public: + /** + * @copydoc basic_promise::emplace_value + */ + template + requires (std::constructible_from) + void emplace_value(Args&&... args) { + shared_state->template emplace_value(std::forward(args)...); + } + + /** + * @copydoc basic_promise::set_value(const T&) + */ + template + void set_value(const U& v) requires (std::convertible_to) { + shared_state->template set_value(v); + } + + /** + * @copydoc basic_promise::set_value(T&&) + */ + template + void set_value(U&& v) requires (std::convertible_to) { + shared_state->template set_value(std::move(v)); + } + + /** + * @copydoc basic_promise::set_value() + */ + template + void set_value() requires (std::is_void_v) { + shared_state->template set_value(); + } + + /** + * @copydoc basic_promise::set_value(T&&) + */ + template + void set_exception(std::exception_ptr ptr) { + shared_state->template set_exception(std::move(ptr)); + } + + /** + * @copydoc basic_promise::notify_awaiter + */ + void notify_awaiter() { + shared_state->notify_awaiter(); + } + + /** + * @copydoc basic_promise::get_awaitable + */ + awaitable get_awaitable() { + return shared_state->get_awaitable(); + } +}; + +template +using promise = moveable_promise; + +template +auto awaitable::abandon() -> uint8_t { + uint8_t previous_state = state_flags::sf_broken; + if (state_ptr) { + previous_state = state_ptr->state.fetch_or(state_flags::sf_broken, std::memory_order::acq_rel); + state_ptr = nullptr; + } + return previous_state; +} + +template +awaitable::~awaitable() { + if_this_causes_an_invalid_read_your_promise_was_destroyed_before_your_awaitable____check_your_promise_lifetime(); +} + +template +bool awaitable::valid() const noexcept { + return state_ptr != nullptr; +} + +template +bool awaitable::await_ready() const { + if (!this->valid()) { + throw dpp::logic_exception("cannot co_await an empty awaitable"); + } + uint8_t state = this->state_ptr->state.load(std::memory_order::relaxed); + return state & detail::promise::sf_ready; +} + +template +template +bool awaitable::awaiter::await_suspend(detail::std_coroutine::coroutine_handle<> handle) { + auto &promise = *awaitable_obj.state_ptr; + + promise.awaiter = handle; + auto previous_flags = promise.state.fetch_or(detail::promise::sf_awaited, std::memory_order::relaxed); + if (previous_flags & detail::promise::sf_awaited) { + throw dpp::logic_exception("awaitable is already being awaited"); + } + return !(previous_flags & detail::promise::sf_ready); +} + +template +template +T awaitable::awaiter::await_resume() { + auto &promise = *awaitable_obj.state_ptr; + + promise.state.fetch_and(~detail::promise::sf_awaited, std::memory_order::acq_rel); + if (std::holds_alternative(promise.value)) { + std::rethrow_exception(std::get<2>(promise.value)); + } + if constexpr (!std::is_void_v) { + return std::get<1>(std::move(promise.value)); + } else { + return; + } +} + + + +template +template +bool awaitable::awaiter::await_ready() const { + return static_cast(awaitable_obj).await_ready(); +} + +} + +#include + +namespace dpp { + +namespace detail::promise { + +template +void spawn_sync_wait_job(auto* awaitable, std::condition_variable &cv, auto&& result) { + [](auto* awaitable_, std::condition_variable &cv_, auto&& result_) -> dpp::job { + try { + if constexpr (std::is_void_v) { + co_await *awaitable_; + result_.template emplace<1>(); + } else { + result_.template emplace<1>(co_await *awaitable_); + } + } catch (...) { + result_.template emplace<2>(std::current_exception()); + } + cv_.notify_all(); + }(awaitable, cv, std::forward(result)); +} + +} + +} + +#endif /* DPP_CORO */ diff --git a/include/dpp/coro/coro.h b/include/dpp/coro/coro.h index 76f3256ed8..34827debdd 100644 --- a/include/dpp/coro/coro.h +++ b/include/dpp/coro/coro.h @@ -19,8 +19,8 @@ * ************************************************************************************/ -#ifdef DPP_CORO #pragma once +#ifdef DPP_CORO #if (defined(_LIBCPP_VERSION) and !defined(__cpp_impl_coroutine)) // if libc++ experimental implementation (LLVM < 14) # define STDCORO_EXPERIMENTAL_HEADER @@ -114,13 +114,14 @@ decltype(auto) co_await_resolve(T&& expr) noexcept { } #else + /** * @brief Concept to check if a type has a useable `operator co_await()` member * * @note This is actually a C++20 concept but Doxygen doesn't do well with them */ template -bool has_co_await_member; +inline constexpr bool has_co_await_member; /** * @brief Concept to check if a type has a useable overload of the free function `operator co_await(expr)` @@ -128,7 +129,7 @@ bool has_co_await_member; * @note This is actually a C++20 concept but Doxygen doesn't do well with them */ template -bool has_free_co_await; +inline constexpr bool has_free_co_await; /** * @brief Concept to check if a type has useable `await_ready()`, `await_suspend()` and `await_resume()` member functions. @@ -136,7 +137,15 @@ bool has_free_co_await; * @note This is actually a C++20 concept but Doxygen doesn't do well with them */ template -bool has_await_members; +inline constexpr bool has_await_members; + +/** + * @brief Concept to check if a type can be used with co_await + * + * @note This is actually a C++20 concept but Doxygen doesn't do well with them + */ +template +inline constexpr bool awaitable_type; /** * @brief Mimics the compiler's behavior of using co_await. That is, it returns whichever works first, in order : `expr.operator co_await();` > `operator co_await(expr)` > `expr` @@ -144,6 +153,7 @@ bool has_await_members; * This function is conditionally noexcept, if the returned expression also is. */ decltype(auto) co_await_resolve(auto&& expr) {} + #endif /** @@ -154,6 +164,12 @@ using awaitable_result = decltype(co_await_resolve(std::declval()).await_resu } // namespace detail +/** + * @brief Concept to check if a type can be used with co_await + */ +template +concept awaitable_type = requires (T expr) { detail::co_await_resolve(expr).await_ready(); }; + struct confirmation_callback_t; template diff --git a/include/dpp/coro/coroutine.h b/include/dpp/coro/coroutine.h index 18fcac7f1c..a9e7a5d100 100644 --- a/include/dpp/coro/coroutine.h +++ b/include/dpp/coro/coroutine.h @@ -32,7 +32,8 @@ struct coroutine_dummy { #ifdef DPP_CORO -#include "coro.h" +#include +#include #include #include @@ -46,7 +47,7 @@ namespace detail { namespace coroutine { - template +template struct promise_t; template @@ -55,188 +56,95 @@ template */ using handle_t = std_coroutine::coroutine_handle>; +} // namespace coroutine + +} // namespace detail + /** - * @brief Base class of dpp::coroutine. + * @class coroutine coroutine.h coro/coroutine.h + * @brief Base type for a coroutine, starts on co_await. * - * @warning This class should not be used directly by a user, use dpp::coroutine instead. - * @note This class contains all the functions used internally by co_await. It is intentionally opaque and a private base of dpp::coroutine so a user cannot call await_suspend and await_resume directly. + * @warning - This feature is EXPERIMENTAL. The API may change at any time and there may be bugs. + * Please report any to GitHub Issues or to our Discord Server. + * @warning - Using co_await on this object more than once is undefined behavior. + * @tparam R Return type of the coroutine. Can be void, or a complete object that supports move construction and move assignment. */ template -class coroutine_base { -protected: +class [[nodiscard("dpp::coroutine only starts when it is awaited, it will do nothing if discarded")]] coroutine : public basic_awaitable> { /** * @brief Promise has friend access for the constructor */ - friend struct promise_t; + friend struct detail::coroutine::promise_t; /** * @brief Coroutine handle. */ detail::coroutine::handle_t handle{nullptr}; -private: /** * @brief Construct from a handle. Internal use only. */ - coroutine_base(detail::coroutine::handle_t h) : handle{h} {} - -public: - /** - * @brief Default constructor, creates an empty coroutine. - */ - coroutine_base() = default; + coroutine(detail::coroutine::handle_t h) : handle{h} {} - /** - * @brief Copy constructor is disabled - */ - coroutine_base(const coroutine_base &) = delete; - - /** - * @brief Move constructor, grabs another coroutine's handle - * - * @param other Coroutine to move the handle from - */ - coroutine_base(coroutine_base &&other) noexcept : handle(std::exchange(other.handle, nullptr)) {} - - /** - * @brief Destructor, destroys the handle. - */ - ~coroutine_base() { - if (handle) { - handle.destroy(); - } - } - - /** - * @brief Copy assignment is disabled - */ - coroutine_base &operator=(const coroutine_base &) = delete; - - /** - * @brief Move assignment, grabs another coroutine's handle - * - * @param other Coroutine to move the handle from - */ - coroutine_base &operator=(coroutine_base &&other) noexcept { - handle = std::exchange(other.handle, nullptr); - return *this; - } - - /** - * @brief First function called by the standard library when the coroutine is co_await-ed. - * - * @remark Do not call this manually, use the co_await keyword instead. - * @throws invalid_operation_exception if the coroutine is empty or finished. - * @return bool Whether the coroutine is done - */ - [[nodiscard]] bool await_ready() const { - if (!handle) { - throw dpp::logic_exception("cannot co_await an empty coroutine"); - } - return handle.done(); - } - - /** - * @brief Second function called by the standard library when the coroutine is co_await-ed. - * - * Stores the calling coroutine in the promise to resume when this coroutine suspends. - * - * @remark Do not call this manually, use the co_await keyword instead. - * @param caller The calling coroutine, now suspended - */ - template - [[nodiscard]] handle_t await_suspend(detail::std_coroutine::coroutine_handle caller) noexcept { - handle.promise().parent = caller; - return handle; - } - - /** - * @brief Function called by the standard library when the coroutine is resumed. - * - * @remark Do not call this manually, use the co_await keyword instead. - * @throw Throws any exception thrown or uncaught by the coroutine - * @return R The result of the coroutine. It is given to the caller as a result to `co_await` - */ - decltype(auto) await_resume() & { - return static_cast &>(*this).await_resume_impl(); - } - - /** - * @brief Function called by the standard library when the coroutine is resumed. - * - * @remark Do not call this manually, use the co_await keyword instead. - * @throw Throws any exception thrown or uncaught by the coroutine - * @return R The result of the coroutine. It is given to the caller as a result to `co_await` - */ - [[nodiscard]] decltype(auto) await_resume() const & { - return static_cast const&>(*this).await_resume_impl(); - } - - /** - * @brief Function called by the standard library when the coroutine is resumed. - * - * @remark Do not call this manually, use the co_await keyword instead. - * @throw Throws any exception thrown or uncaught by the coroutine - * @return R The result of the coroutine. It is given to the caller as a result to `co_await` - */ - [[nodiscard]] decltype(auto) await_resume() && { - return static_cast &&>(*this).await_resume_impl(); - } -}; - -} // namespace coroutine - -} // namespace detail - -/** - * @class coroutine coroutine.h coro/coroutine.h - * @brief Base type for a coroutine, starts on co_await. - * - * @warning - This feature is EXPERIMENTAL. The API may change at any time and there may be bugs. - * Please report any to GitHub Issues or to our Discord Server. - * @warning - Using co_await on this object more than once is undefined behavior. - * @tparam R Return type of the coroutine. Can be void, or a complete object that supports move construction and move assignment. - */ -template -class coroutine : private detail::coroutine::coroutine_base { - /** - * @brief Internal use only base class containing common logic between coroutine and coroutine. It also serves to prevent await_suspend and await_resume from being used directly. - * - * @warning For internal use only, do not use. - * @see operator co_await() - */ - friend class detail::coroutine::coroutine_base; + struct awaiter { + /** + * @brief Reference to the coroutine object being awaited. + */ + coroutine &coro; - [[nodiscard]] R& await_resume_impl() & { - detail::coroutine::promise_t &promise = this->handle.promise(); - if (promise.exception) { - std::rethrow_exception(promise.exception); + /** + * @brief First function called by the standard library when the coroutine is co_await-ed. + * + * @remark Do not call this manually, use the co_await keyword instead. + * @throws invalid_operation_exception if the coroutine is empty or finished. + * @return bool Whether the coroutine is done + */ + [[nodiscard]] bool await_ready() const { + if (!coro.handle) { + throw dpp::logic_exception("cannot co_await an empty coroutine"); + } + return coro.handle.done(); } - return *promise.result; - } - [[nodiscard]] const R& await_resume_impl() const & { - detail::coroutine::promise_t &promise = this->handle.promise(); - if (promise.exception) { - std::rethrow_exception(promise.exception); + /** + * @brief Second function called by the standard library when the coroutine is co_await-ed. + * + * Stores the calling coroutine in the promise to resume when this coroutine suspends. + * + * @remark Do not call this manually, use the co_await keyword instead. + * @param caller The calling coroutine, now suspended + */ + template + [[nodiscard]] detail::coroutine::handle_t await_suspend(detail::std_coroutine::coroutine_handle caller) noexcept { + coro.handle.promise().parent = caller; + return coro.handle; } - return *promise.result; - } - [[nodiscard]] R&& await_resume_impl() && { - detail::coroutine::promise_t &promise = this->handle.promise(); - if (promise.exception) { - std::rethrow_exception(promise.exception); + /** + * @brief Final function called by the standard library when the coroutine is co_await-ed. + * + * Pops the coroutine's result and returns it. + * @remark Do not call this manually, use the co_await keyword instead. + */ + R await_resume() { + detail::coroutine::promise_t &promise = coro.handle.promise(); + if (promise.exception) { + std::rethrow_exception(promise.exception); + } + if constexpr (!std::is_void_v) { + return *std::exchange(promise.result, std::nullopt); + } else { + return; // unnecessary but makes lsp happy + } } - return *std::move(promise.result); - } + }; public: /** * @brief The type of the result produced by this coroutine. */ using result_type = R; -#ifdef _DOXYGEN_ // :)))) + /** * @brief Default constructor, creates an empty coroutine. */ @@ -252,12 +160,16 @@ class coroutine : private detail::coroutine::coroutine_base { * * @param other Coroutine to move the handle from */ - coroutine(coroutine &&other) noexcept; + coroutine(coroutine &&other) noexcept : handle(std::exchange(other.handle, nullptr)) {} /** * @brief Destructor, destroys the handle. */ - ~coroutine(); + ~coroutine() { + if (handle) { + handle.destroy(); + } + } /** * @brief Copy assignment is disabled @@ -269,111 +181,15 @@ class coroutine : private detail::coroutine::coroutine_base { * * @param other Coroutine to move the handle from */ - coroutine &operator=(coroutine &&other) noexcept; - - /** - * @brief First function called by the standard library when the coroutine is co_await-ed. - * - * @remark Do not call this manually, use the co_await keyword instead. - * @throws invalid_operation_exception if the coroutine is empty or finished. - * @return bool Whether the coroutine is done - */ - [[nodiscard]] bool await_ready() const; -#else - using detail::coroutine::coroutine_base::coroutine_base; // use coroutine_base's constructors - using detail::coroutine::coroutine_base::operator=; // use coroutine_base's assignment operators - using detail::coroutine::coroutine_base::await_ready; // expose await_ready as public -#endif - - /** - * @brief Suspend the caller until the coroutine completes. - * - * @throw On resumption, any exception thrown by the coroutine is propagated to the caller. - * @return On resumption, this expression evaluates to the result object of type R, as a reference. - */ - [[nodiscard]] auto& operator co_await() & noexcept { - return static_cast&>(*this); - } - - /** - * @brief Suspend the caller until the coroutine completes. - * - * @throw On resumption, any exception thrown by the coroutine is propagated to the caller. - * @return On resumption, this expression evaluates to the result object of type R, as a const reference. - */ - [[nodiscard]] const auto& operator co_await() const & noexcept { - return static_cast const&>(*this); - } - - /** - * @brief Suspend the caller until the coroutine completes. - * - * @throw On resumption, any exception thrown by the coroutine is propagated to the caller. - * @return On resumption, this expression evaluates to the result object of type R, as an rvalue reference. - */ - [[nodiscard]] auto&& operator co_await() && noexcept { - return static_cast&&>(*this); - } -}; - -#ifndef _DOXYGEN_ // don't generate this on doxygen because `using` doesn't work and 2 copies of coroutine_base's docs is enough -/** - * @brief Base type for a coroutine, starts on co_await. - * - * @warning - This feature is EXPERIMENTAL. The API may change at any time and there may be bugs. Please report any to GitHub issues or to the D++ Discord server. - * @warning - Using co_await on this object more than once is undefined behavior. - * @tparam R Return type of the coroutine. Can be void, or a complete object that supports move construction and move assignment. - */ -template <> -class coroutine : private detail::coroutine::coroutine_base { - /** - * @brief Base class has friend access for CRTP downcast - */ - friend class detail::coroutine::coroutine_base; - - void await_resume_impl() const; - -public: - using detail::coroutine::coroutine_base::coroutine_base; // use coroutine_base's constructors - using detail::coroutine::coroutine_base::operator=; // use coroutine_base's assignment operators - using detail::coroutine::coroutine_base::await_ready; // expose await_ready as public - - /** - * @brief The type of the result produced by this coroutine. - */ - using result_type = void; - - /** - * @brief Suspend the current coroutine until the coroutine completes. - * - * @throw On resumption, any exception thrown by the coroutine is propagated to the caller. - * @return On resumption, this expression evaluates to the result object of type R, as a reference. - */ - [[nodiscard]] auto& operator co_await() & noexcept { - return static_cast&>(*this); - } - - /** - * @brief Suspend the current coroutine until the coroutine completes. - * - * @throw On resumption, any exception thrown by the coroutine is propagated to the caller. - * @return On resumption, this expression evaluates to the result object of type R, as a const reference. - */ - [[nodiscard]] const auto& operator co_await() const & noexcept { - return static_cast const &>(*this); + coroutine &operator=(coroutine &&other) noexcept { + handle = std::exchange(other.handle, nullptr); + return *this; } - - /** - * @brief Suspend the current coroutine until the coroutine completes. - * - * @throw On resumption, any exception thrown by the coroutine is propagated to the caller. - * @return On resumption, this expression evaluates to the result object of type R, as an rvalue reference. - */ - [[nodiscard]] auto&& operator co_await() && noexcept { - return static_cast&&>(*this); + + [[nodiscard]] auto operator co_await() { + return awaiter{*this}; } }; -#endif /* _DOXYGEN_ */ namespace detail::coroutine { template @@ -574,14 +390,6 @@ namespace detail::coroutine { } // namespace detail -#ifndef _DOXYGEN_ -inline void coroutine::await_resume_impl() const { - if (handle.promise().exception) { - std::rethrow_exception(handle.promise().exception); - } -} -#endif /* _DOXYGEN_ */ - DPP_CHECK_ABI_COMPAT(coroutine, coroutine_dummy) DPP_CHECK_ABI_COMPAT(coroutine, coroutine_dummy) diff --git a/include/dpp/coro/task.h b/include/dpp/coro/task.h index 74c9f981d7..6625bb1f3e 100644 --- a/include/dpp/coro/task.h +++ b/include/dpp/coro/task.h @@ -21,10 +21,11 @@ #pragma once #include +#include namespace dpp { -struct task_dummy { +struct task_dummy : awaitable_dummy { int* handle_dummy = nullptr; }; @@ -32,7 +33,7 @@ struct task_dummy { #ifdef DPP_CORO -#include "coro.h" +#include #include #include @@ -51,28 +52,6 @@ namespace detail { /* Internal cogwheels for dpp::task */ namespace task { -/** - * @brief State of a task - */ -enum class state_t { - /** - * @brief Task was started but never co_await-ed - */ - started, - /** - * @brief Task was co_await-ed and is pending completion - */ - awaited, - /** - * @brief Task is completed - */ - done, - /** - * @brief Task is still running but the actual dpp::task object is destroyed - */ - dangling -}; - /** * @brief A @ref dpp::task "task"'s promise_t type, with special logic for handling nested tasks. * @@ -97,176 +76,6 @@ struct final_awaiter; template using handle_t = std_coroutine::coroutine_handle>; -/** - * @brief Base class of @ref dpp::task. - * - * @warning This class should not be used directly by a user, use @ref dpp::task instead. - * @note This class contains all the functions used internally by co_await. It is intentionally opaque and a private base of @ref dpp::task so a user cannot call await_suspend() and await_resume() directly. - */ -template -class task_base { -protected: - /** - * @brief The coroutine handle of this task. - */ - handle_t handle; - - /** - * @brief Promise type of this coroutine. For internal use only, do not use. - */ - friend struct promise_t; - -private: - /** - * @brief Construct from a coroutine handle. Internal use only - */ - explicit task_base(handle_t handle_) : handle(handle_) {} - -public: - /** - * @brief Default constructor, creates a task not bound to a coroutine. - */ - task_base() = default; - - /** - * @brief Copy constructor is disabled - */ - task_base(const task_base &) = delete; - - /** - * @brief Move constructor, grabs another task's coroutine handle - * - * @param other Task to move the handle from - */ - task_base(task_base &&other) noexcept : handle(std::exchange(other.handle, nullptr)) {} - - /** - * @brief Destructor. - * - * Destroys the handle. - * @warning The coroutine must be finished before this is called, otherwise it runs the risk of being resumed after it is destroyed, resuming in use-after-free undefined behavior. - */ - ~task_base() { - if (handle) { - promise_t &promise = handle.promise(); - state_t previous_state = promise.state.exchange(state_t::dangling); - - if (previous_state == state_t::done) { - handle.destroy(); - } - else { - cancel(); - } - } - } - - /** - * @brief Copy assignment is disabled - */ - task_base &operator=(const task_base &) = delete; - - /** - * @brief Move assignment, grabs another task's coroutine handle - * - * @param other Task to move the handle from - */ - task_base &operator=(task_base &&other) noexcept { - handle = std::exchange(other.handle, nullptr); - return (*this); - } - - /** - * @brief Check whether or not a call to co_await will suspend the caller. - * - * This function is called by the standard library as a first step when using co_await. If it returns true then the caller is not suspended. - * @throws logic_exception if the task is empty. - * @return bool Whether not to suspend the caller or not - */ - [[nodiscard]] bool await_ready() const { - if (!handle) { - throw dpp::logic_exception{"cannot co_await an empty task"}; - } - return handle.promise().state.load() == state_t::done; - } - - /** - * @brief Second function called by the standard library when the task is co_await-ed, if await_ready returned false. - * - * Stores the calling coroutine in the promise to resume when this task suspends. - * - * @remark Do not call this manually, use the co_await keyword instead. - * @param caller The calling coroutine, now suspended - * @return bool Whether to suspend the caller or not - */ - [[nodiscard]] bool await_suspend(std_coroutine::coroutine_handle<> caller) noexcept { - promise_t &my_promise = handle.promise(); - auto previous_state = state_t::started; - - my_promise.parent = caller; - // Replace `sent` state with `awaited` ; if that fails, the only logical option is the state was `done`, in which case return false to resume - if (!handle.promise().state.compare_exchange_strong(previous_state, state_t::awaited) && previous_state == state_t::done) { - return false; - } - return true; - } - - /** - * @brief Function to check if the task has finished its execution entirely - * - * @return bool Whether the task is finished. - */ - [[nodiscard]] bool done() const noexcept { - return handle && handle.promise().state.load(std::memory_order_relaxed) == state_t::done; - } - - /** - * @brief Cancel the task, it will stop the next time it uses co_await. On co_await-ing this task, throws dpp::task_cancelled_exception. - * - * @return *this - */ - dpp::task& cancel() & noexcept { - handle.promise().cancelled.exchange(true, std::memory_order_relaxed); - return static_cast &>(*this); - } - - /** - * @brief Cancel the task, it will stop the next time it uses co_await. On co_await-ing this task, throws dpp::task_cancelled_exception. - * - * @return *this - */ - dpp::task&& cancel() && noexcept { - handle.promise().cancelled.exchange(true, std::memory_order_relaxed); - return static_cast &&>(*this); - } - - /** - * @brief Function called by the standard library when resuming. - * - * @return Return value of the coroutine, handed to the caller of co_await. - */ - decltype(auto) await_resume() & { - return static_cast &>(*this).await_resume_impl(); - } - - /** - * @brief Function called by the standard library when resuming. - * - * @return Return value of the coroutine, handed to the caller of co_await. - */ - decltype(auto) await_resume() const & { - return static_cast &>(*this).await_resume_impl(); - } - - /** - * @brief Function called by the standard library when resuming. - * - * @return Return value of the coroutine, handed to the caller of co_await. - */ - decltype(auto) await_resume() && { - return static_cast &&>(*this).await_resume_impl(); - } -}; - } // namespace task } // namespace detail @@ -283,63 +92,35 @@ template #ifndef _DOXYGEN_ requires (!std::is_reference_v) #endif -class task : private detail::task::task_base { - /** - * @brief Internal use only base class containing common logic between task and task. It also serves to prevent await_suspend and await_resume from being used directly. - * - * @warning For internal use only, do not use. - * @see operator co_await() - */ - friend class detail::task::task_base; +class [[nodiscard("dpp::task cancels itself on destruction. use co_await on it, or its sync_wait method")]] task : public awaitable { + friend struct detail::task::promise_t; - /** - * @brief Function called by the standard library when the coroutine is resumed. - * - * @throw Throws any exception thrown or uncaught by the coroutine - * @return The result of the coroutine. This is returned to the awaiter as the result of co_await - */ - R& await_resume_impl() & { - detail::task::promise_t &promise = this->handle.promise(); - if (promise.exception) { - std::rethrow_exception(promise.exception); - } - return *reinterpret_cast(promise.result_storage.data()); - } + using handle_t = detail::task::handle_t; + using state_flags = detail::promise::state_flags; + + handle_t handle{}; +protected: /** - * @brief Function called by the standard library when the coroutine is resumed. - * - * @throw Throws any exception thrown or uncaught by the coroutine - * @return The result of the coroutine. This is returned to the awaiter as the result of co_await + * @brief Construct from a coroutine handle. Internal use only */ - const R& await_resume_impl() const & { - detail::task::promise_t &promise = this->handle.promise(); - if (promise.exception) { - std::rethrow_exception(promise.exception); - } - return *reinterpret_cast(promise.result_storage.data()); - } + explicit task(handle_t handle_) : awaitable(&handle_.promise()), handle(handle_) {} /** - * @brief Function called by the standard library when the coroutine is resumed. - * - * @throw Throws any exception thrown or uncaught by the coroutine - * @return The result of the coroutine. This is returned to the awaiter as the result of co_await + * @brief Clean up our handle, cancelling any running task */ - R&& await_resume_impl() && { - detail::task::promise_t &promise = this->handle.promise(); - if (promise.exception) { - std::rethrow_exception(promise.exception); + void cleanup() { + if (handle && this->valid()) { + if (this->abandon() & state_flags::sf_done) { + handle.destroy(); + } else { + cancel(); + } + handle = nullptr; } - return *reinterpret_cast(promise.result_storage.data()); } public: - /** - * @brief The type of the result produced by this task. - */ - using result_type = R; -#ifdef _DOXYGEN_ // :) /** * @brief Default constructor, creates a task not bound to a coroutine. */ @@ -355,15 +136,7 @@ class task : private detail::task::task_base { * * @param other Task to move the handle from */ - task(task &&other) noexcept; - - /** - * @brief Destructor. - * - * Destroys the handle. - * @warning The coroutine must be finished before this is called, otherwise it runs the risk of being resumed after it is destroyed, resuming in use-after-free undefined behavior. - */ - ~task(); + task(task &&other) noexcept : awaitable(std::move(other)), handle(std::exchange(other.handle, nullptr)) {} /** * @brief Copy assignment is disabled @@ -375,136 +148,51 @@ class task : private detail::task::task_base { * * @param other Task to move the handle from */ - task &operator=(task &&other) noexcept; - - /** - * @brief Function to check if the task has finished its execution entirely - * - * @return bool Whether the task is finished. - */ - [[nodiscard]] bool done() const noexcept; - - /** - * @brief Cancel the task, it will stop the next time it uses co_await. On co_await-ing this task, throws dpp::task_cancelled_exception. - */ - dpp::task& cancel() & noexcept; - - /** - * @brief Check whether or not a call to co_await will suspend the caller. - * - * This function is called by the standard library as a first step when using co_await. If it returns true then the caller is not suspended. - * @throws logic_exception if the task is empty. - * @return bool Whether not to suspend the caller or not - */ - [[nodiscard]] bool await_ready() const; -#else - using detail::task::task_base::task_base; // use task_base's constructors - using detail::task::task_base::operator=; // use task_base's assignment operators - using detail::task::task_base::done; // expose done() as public - using detail::task::task_base::cancel; // expose cancel() as public - using detail::task::task_base::await_ready; // expose await_ready as public -#endif - - /** - * @brief Suspend the current coroutine until the task completes. - * - * @throw On resumption, any exception thrown by the coroutine is propagated to the caller. - * @return On resumption, this expression evaluates to the result object of type R, as a reference. - */ - [[nodiscard]] auto& operator co_await() & noexcept { - return static_cast&>(*this); - } - - /** - * @brief Suspend the current coroutine until the task completes. - * - * @throw On resumption, any exception thrown by the coroutine is propagated to the caller. - * @return On resumption, this expression evaluates to the result object of type R, as a const reference. - */ - [[nodiscard]] const auto& operator co_await() const & noexcept { - return static_cast&>(*this); + task &operator=(task &&other) noexcept { + cleanup(); + handle = std::exchange(other.handle, nullptr); + awaitable::operator=(std::move(other)); + return *this; } /** - * @brief Suspend the current coroutine until the task completes. + * @brief Destructor. * - * @throw On resumption, any exception thrown by the coroutine is propagated to the caller. - * @return On resumption, this expression evaluates to the result object of type R, as an rvalue reference. + * Destroys the handle. If the task is still running, it will be cancelled. */ - [[nodiscard]] auto&& operator co_await() && noexcept { - return static_cast&&>(*this); + ~task() { + cleanup(); } -}; -#ifndef _DOXYGEN_ // don't generate this on doxygen because `using` doesn't work and 2 copies of coroutine_base's docs is enough -/** - * @brief A coroutine task. It starts immediately on construction and can be co_await-ed, making it perfect for parallel coroutines returning a value. - * - * Can be used in conjunction with coroutine events via dpp::event_router_t::co_attach, or on its own. - * - * @warning - This feature is EXPERIMENTAL. The API may change at any time and there may be bugs. Please report any to GitHub issues or to the D++ Discord server. - * @tparam R Return type of the coroutine. Cannot be a reference, can be void. - */ -template <> -class task : private detail::task::task_base { /** - * @brief Private base class containing common logic between task and task. It also serves to prevent await_suspend and await_resume from being used directly. - * - * @see operator co_await() - */ - friend class detail::task::task_base; - - /** - * @brief Function called by the standard library when the coroutine is resumed. - * - * @remark Do not call this manually, use the co_await keyword instead. - * @throw Throws any exception thrown or uncaught by the coroutine - */ - void await_resume_impl() const; - -public: - using detail::task::task_base::task_base; // use task_base's constructors - using detail::task::task_base::operator=; // use task_base's assignment operators - using detail::task::task_base::done; // expose done() as public - using detail::task::task_base::cancel; // expose cancel() as public - using detail::task::task_base::await_ready; // expose await_ready as public - - /** - * @brief The type of the result produced by this task - */ - using result_type = void; - - /** - * @brief Suspend the current coroutine until the task completes. + * @brief Function to check if the task has finished its execution entirely * - * @throw On resumption, any exception thrown by the coroutine is propagated to the caller. - * @return On resumption, returns a reference to the contained result. + * @return bool Whether the task is finished. */ - auto& operator co_await() & { - return static_cast&>(*this); + [[nodiscard]] bool done() const noexcept { + return handle && (!this->valid() || handle.promise().state.load(std::memory_order_relaxed) == state_flags::sf_done); } /** - * @brief Suspend the current coroutine until the task completes. + * @brief Cancel the task, it will stop the next time it uses co_await. On co_await-ing this task, throws dpp::task_cancelled_exception. * - * @throw On resumption, any exception thrown by the coroutine is propagated to the caller. - * @return On resumption, returns a const reference to the contained result. + * @return *this */ - const auto& operator co_await() const & { - return static_cast&>(*this); + task& cancel() & noexcept { + handle.promise().cancelled.exchange(true, std::memory_order_relaxed); + return *this; } /** - * @brief Suspend the current coroutine until the task completes. + * @brief Cancel the task, it will stop the next time it uses co_await. On co_await-ing this task, throws dpp::task_cancelled_exception. * - * @throw On resumption, any exception thrown by the coroutine is propagated to the caller. - * @return On resumption, returns a reference to the contained result. + * @return *this */ - auto&& operator co_await() && { - return static_cast&&>(*this); + task&& cancel() && noexcept { + handle.promise().cancelled.exchange(true, std::memory_order_relaxed); + return *this; } }; -#endif /* _DOXYGEN_ */ namespace detail::task { /** @@ -536,29 +224,13 @@ struct final_awaiter { /** * @brief Base implementation of task::promise_t, without the logic that would depend on the return type. Meant to be inherited from */ -struct promise_base { - /** - * @brief State of the task, used to keep track of lifetime and status - */ - std::atomic state = state_t::started; - +template +struct promise_base : basic_promise { /** * @brief Whether the task is cancelled or not. */ std::atomic cancelled = false; - /** - * @brief Parent coroutine to return to for nested coroutines. - */ - detail::std_coroutine::coroutine_handle<> parent = nullptr; - - /** - * @brief Exception ptr if any was thrown during the coroutine - * - * @see ::await_resume_impl() const { - if (handle.promise().exception) { - std::rethrow_exception(handle.promise().exception); - } -} -#endif /* _DOXYGEN_ */ - DPP_CHECK_ABI_COMPAT(task, task_dummy) DPP_CHECK_ABI_COMPAT(task, task_dummy) diff --git a/include/dpp/event_router.h b/include/dpp/event_router.h index 1657fbd592..2babc42e29 100644 --- a/include/dpp/event_router.h +++ b/include/dpp/event_router.h @@ -478,7 +478,7 @@ template class event_router_t { * saving it in a variable is recommended to avoid variable lifetime issues. * * @details Example: @code{cpp} - * dpp::job my_handler(dpp::slashcommand_t event) { + * dpp::task<> my_handler(const dpp::slashcommand_t& event) { * co_await event.co_reply(dpp::message().add_component(dpp::component().add_component().set_label("click me!").set_id("test"))); * * dpp::button_click_t b = co_await c->on_button_click.with([](const dpp::button_click_t &event){ return event.custom_id == "test"; }); @@ -514,7 +514,7 @@ template class event_router_t { * * Example: * @details Example: @code{cpp} - * dpp::job my_handler(dpp::slashcommand_t event) { + * dpp::task<> my_handler(const dpp::slashcommand_t& event) { * co_await event.co_reply(dpp::message().add_component(dpp::component().add_component().set_label("click me!").set_id("test"))); * * dpp::button_click_t b = co_await c->on_message_create; @@ -607,7 +607,7 @@ template class event_router_t { * detach the listener from the event later if necessary. */ template - requires (utility::callable_returns || utility::callable_returns, const T&> || utility::callable_returns) + requires (utility::callable_returns, const T&> || utility::callable_returns) [[maybe_unused]] event_handle operator()(F&& fun) { return this->attach(std::forward(fun)); } @@ -649,28 +649,6 @@ template class event_router_t { dispatch_container.emplace(std::piecewise_construct, std::forward_as_tuple(h), std::forward_as_tuple(std::in_place_type_t{}, std::forward(fun))); return h; } - - /** - * @brief Attach a callable to the event, adding a listener. - * The callable should either be of the form `void(const T&)` or - * `dpp::task(const T&)`, where T is the event type for this event router. - * - * @deprecated dpp::job event handlers are deprecated and will be removed in a future version, use dpp::task instead. - * @param fun Callable to attach to event - * @return event_handle An event handle unique to this event, used to - * detach the listener from the event later if necessary. - */ - template - requires (utility::callable_returns) - DPP_DEPRECATED("dpp::job event handlers are deprecated and will be removed in a future version, use dpp::task instead") - [[maybe_unused]] event_handle attach(F&& fun) { - assert(dpp::utility::is_coro_enabled()); - - std::unique_lock l(mutex); - event_handle h = next_handle++; - dispatch_container.emplace(std::piecewise_construct, std::forward_as_tuple(h), std::forward_as_tuple(std::in_place_type_t{}, std::forward(fun))); - return h; - } # else /** * @brief Attach a callable to the event, adding a listener. diff --git a/include/dpp/export.h b/include/dpp/export.h index 24d6bfe81a..e293aafccc 100644 --- a/include/dpp/export.h +++ b/include/dpp/export.h @@ -118,7 +118,9 @@ extern bool DPP_EXPORT validate_configuration(); #ifndef _WIN32 #define SOCKET int #else - #define NOMINMAX + #ifndef NOMINMAX + #define NOMINMAX + #endif #include #endif diff --git a/include/dpp/misc-enum.h b/include/dpp/misc-enum.h index 8ed123d0ef..19fd2fcd9a 100644 --- a/include/dpp/misc-enum.h +++ b/include/dpp/misc-enum.h @@ -21,7 +21,7 @@ ************************************************************************************/ #pragma once #include -#include +#include #include namespace dpp { diff --git a/src/coro_loop/coro_loop.cpp b/src/coro_loop/coro_loop.cpp new file mode 100644 index 0000000000..159c801cf3 --- /dev/null +++ b/src/coro_loop/coro_loop.cpp @@ -0,0 +1,73 @@ +/************************************************************************************ + * + * D++, A Lightweight C++ library for Discord + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2021 Craig Edwards and D++ contributors + * (https://github.com/brainboxdotcc/DPP/graphs/contributors) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ************************************************************************************/ + +#include +#include + +int64_t proc_self_value(const std::string& find_token) { + int64_t ret = 0; + std::ifstream self_status("/proc/self/status"); + while (self_status) { + std::string token; + self_status >> token; + if (token == find_token) { + self_status >> ret; + break; + } + } + self_status.close(); + return ret; +} + +int64_t rss() { + return proc_self_value("VmRSS:") * 1024; +} + +#ifdef DPP_CORO +dpp::task test(dpp::cluster& cluster) { + [[maybe_unused]] int test[102400]{}; + test[60] = 5; + co_return; +} +#endif + +int main() { +#ifdef DPP_CORO + char* t = getenv("DPP_UNIT_TEST_TOKEN"); + if (t) { + dpp::cluster coro_cluster(t, dpp::i_guilds, 1, 0, 1, true, dpp::cache_policy::cpol_none); + coro_cluster.set_websocket_protocol(dpp::ws_etf); + coro_cluster.on_log(dpp::utility::cout_logger()); + coro_cluster.on_ready([&coro_cluster](auto) -> dpp::task { + coro_cluster.start_timer([&coro_cluster](dpp::timer) -> dpp::task { + for (int i = 0; i < 1000; ++i) { + co_await test(coro_cluster); + } + coro_cluster.log(dpp::ll_info, "coro timer ticked. RSS=" + std::to_string(rss())); + co_return; + }, 1); + co_return; + }); + coro_cluster.start(dpp::st_wait); + } +#endif +} diff --git a/src/dpp/cluster.cpp b/src/dpp/cluster.cpp index 83d53a0528..4866e7a67b 100644 --- a/src/dpp/cluster.cpp +++ b/src/dpp/cluster.cpp @@ -190,7 +190,12 @@ void cluster::start(bool return_after) { /* Start up all shards */ gateway g; try { +#ifdef DPP_CORO + confirmation_callback_t cc = co_get_gateway_bot().sync_wait(); + g = std::get(cc.value); +#else g = dpp::sync(this, &cluster::get_gateway_bot); +#endif log(ll_debug, "Cluster: " + std::to_string(g.session_start_remaining) + " of " + std::to_string(g.session_start_total) + " session starts remaining"); if (g.session_start_remaining < g.shards) { throw dpp::connection_exception(err_no_sessions_left, "Discord indicates you cannot start enough sessions to boot this cluster! Cluster startup aborted. Try again later."); diff --git a/src/unittest/coro.cpp b/src/unittest/coro.cpp index c5047a134c..eead3c85b6 100644 --- a/src/unittest/coro.cpp +++ b/src/unittest/coro.cpp @@ -147,6 +147,11 @@ dpp::task task_offline_test() { if (int ret = co_await simple_awaitable{test, 42}; ret != 42) { set_status(test, ts_failed, "failed simple awaitable"); } + + if (int ret = co_await []() -> dpp::task { co_return 42; }(); ret != 42) { + set_status(test, ts_failed, "failed simple task"); + } + std::array, 10> tasks; auto start = clock::now(); for (int i = 0; i < 10; ++i) { @@ -365,7 +370,110 @@ dpp::job async_test() { set_status(test, ts_success); } catch (const std::exception &e) { /* no exception should be caught here */ - set_status(test, ts_failed, "unknown exception thrown"); + set_status(test, ts_failed, std::string{"unknown exception thrown: "} + e.what()); + } +} + +dpp::job coro_awaitable_test() { + try { + { + dpp::promise test; + + test.set_value(42); + if (int res = co_await test.get_awaitable(); res != 42) { + set_status(CORO_AWAITABLE_OFFLINE, ts_failed, "could not retrieve value set before co_await"); + } + } + { + dpp::promise test; + + test.set_value(420); + if (int res = test.get_awaitable().sync_wait(); res != 420) { + set_status(CORO_AWAITABLE_OFFLINE, ts_failed, "could not retrieve value set before sync_wait"); + } + } + { + dpp::promise test; + dpp::awaitable awaitable; + + awaitable = test.get_awaitable(); + test.set_value(420); + if (std::optional res = awaitable.sync_wait_for(std::chrono::seconds(5)); !res || *res != 420) { + set_status(CORO_AWAITABLE_OFFLINE, ts_failed, "could not retrieve value set before sync_wait_for"); + } + } + { + dpp::promise test; + dpp::awaitable awaitable{test.get_awaitable()}; + + if (bool res = awaitable.sync_wait_for(std::chrono::seconds(5)); res) { + set_status(CORO_AWAITABLE_OFFLINE, ts_failed, "could not retrieve time out with sync_wait_for"); + } + } + { + dpp::promise test; + dpp::awaitable awaitable{test.get_awaitable()}; + std::thread th{[&test]() mutable { + std::this_thread::sleep_for(std::chrono::seconds(2)); + test.set_value(69); + }}; + th.detach(); + if (std::optional res = awaitable.sync_wait_for(std::chrono::seconds(5)); !res || *res != 69) { + set_status(CORO_AWAITABLE_OFFLINE, ts_failed, "could not retrieve value set after sync_wait_for"); + } + } + { + dpp::promise test; + dpp::awaitable awaitable{test.get_awaitable()}; + std::thread th{[p = std::move(test)]() mutable { + std::this_thread::sleep_for(std::chrono::seconds(2)); + p.set_value(69420); + }}; + th.detach(); + if (int res = co_await awaitable; res != 69420) { + set_status(CORO_AWAITABLE_OFFLINE, ts_failed, "could not retrieve value set after co_await"); + } + } + { + dpp::promise test; + dpp::awaitable awaitable{test.get_awaitable()}; + std::thread th{[p = std::move(test)]() mutable { + std::this_thread::sleep_for(std::chrono::seconds(2)); + p.set_exception(std::make_exception_ptr(dpp::voice_exception("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"))); + }}; + th.detach(); + bool success = false; + try { + co_await awaitable; + } catch (const dpp::voice_exception &) { + success = true; + } + if (!success) { + set_status(CORO_AWAITABLE_OFFLINE, ts_failed, "retrieval of an exception with co_await failed"); + } + } + { + dpp::promise test; + dpp::awaitable awaitable{test.get_awaitable()}; + std::thread th{[&test]() mutable { + std::this_thread::sleep_for(std::chrono::seconds(2)); + test.set_exception(std::make_exception_ptr(dpp::voice_exception("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"))); + }}; + th.detach(); + bool success = false; + try { + awaitable.sync_wait(); + } catch (const dpp::voice_exception &) { + success = true; + } + if (!success) { + set_status(CORO_AWAITABLE_OFFLINE, ts_failed, "retrieval of an exception with co_await failed"); + } + } + set_status(CORO_AWAITABLE_OFFLINE, ts_success); + } catch (const std::exception &e) { + // no exception should reach this point + set_status(CORO_AWAITABLE_OFFLINE, ts_failed, std::string{"unknown exception thrown: "} + e.what()); } } @@ -378,6 +486,9 @@ void coro_offline_tests() std::fill(job_data.begin(), job_data.end(), -1); job_offline_test(); + start_test(CORO_AWAITABLE_OFFLINE); + coro_awaitable_test(); + start_test(CORO_TASK_OFFLINE); std::fill(task_data.begin(), task_data.end(), -1); []() -> dpp::job { @@ -388,7 +499,7 @@ void coro_offline_tests() } catch (const test_exception<0> &) { // exception thrown at the end of the task test set_status(CORO_TASK_OFFLINE, ts_success); } catch (const std::exception &e) { // anything else should not escape - set_status(CORO_TASK_OFFLINE, ts_failed, "unknown exception thrown"); + set_status(CORO_TASK_OFFLINE, ts_failed, std::string{"unknown exception thrown: "} + e.what()); } }(); @@ -401,7 +512,7 @@ void coro_offline_tests() } catch (const test_exception<0> &) { set_status(CORO_COROUTINE_OFFLINE, ts_success); } catch (const std::exception &e) { // anything else should not escape - set_status(CORO_COROUTINE_OFFLINE, ts_failed, "unknown exception thrown"); + set_status(CORO_COROUTINE_OFFLINE, ts_failed, std::string{"unknown exception thrown: "} + e.what()); } }(); diff --git a/src/unittest/test.h b/src/unittest/test.h index 8bb15af894..9afd22879f 100644 --- a/src/unittest/test.h +++ b/src/unittest/test.h @@ -256,6 +256,7 @@ DPP_TEST(THREAD_MESSAGE_REACT_ADD_EVENT, "cluster::on_reaction_add in thread", t DPP_TEST(THREAD_MESSAGE_REACT_REMOVE_EVENT, "cluster::on_reaction_remove in thread", tf_online | tf_extended); DPP_TEST(CORO_JOB_OFFLINE, "coro: offline job", tf_offline | tf_coro); +DPP_TEST(CORO_AWAITABLE_OFFLINE, "coro: offline promise & awaitable", tf_offline | tf_coro); DPP_TEST(CORO_COROUTINE_OFFLINE, "coro: offline coroutine", tf_offline | tf_coro); DPP_TEST(CORO_TASK_OFFLINE, "coro: offline task", tf_offline | tf_coro); DPP_TEST(CORO_ASYNC_OFFLINE, "coro: offline async", tf_offline | tf_coro);