/************************************************************************************ * * 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 namespace dpp { struct async_dummy { int* dummy_shared_state = nullptr; }; } #ifdef DPP_CORO #include "coro.h" #include #include #include #include #include #include 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. */ 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; /** * @brief The stored result of the API call, stored as an array of bytes to directly construct in place */ alignas(R) std::array result_storage; /** * @brief Handle to the coroutine co_await-ing on this API call * * @see std::coroutine_handle */ 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)...); } /** * @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. * * @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 */ 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); } /** * @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 */ 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()); } }; } // namespace async } // namespace detail struct confirmation_callback_t; /** * @class async async.h coro/async.h * @brief A co_await-able object handling an API call in parallel with the caller. * * This class is the return type of the dpp::cluster::co_* methods, but it can also be created manually to wrap any async call. * * @remark - The coroutine may be resumed in another thread, do not rely on thread_local variables. * @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 The return type of the API call. Defaults to confirmation_callback_t */ template class async : private detail::async::async_base { /** * @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() */ friend class detail::async::async_base; 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 /** * @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. * * @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 */ template #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)...} {} /** * @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 */ template #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; /** * @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 &operator=(async &&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; #endif /** * @brief Suspend the caller until the request completes. * * @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 request completes. * * @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 request completes. * * @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); } }; DPP_CHECK_ABI_COMPAT(async<>, async_dummy); } // namespace dpp #endif /* DPP_CORO */