From b6563602259495c46723dc4289966e1f9f0c4ad8 Mon Sep 17 00:00:00 2001 From: Amber Ehrlich Date: Mon, 21 Aug 2023 12:45:25 -0400 Subject: [PATCH] docs(coro): update docs for PR #763 --- docpages/advanced_reference/coroutines.md | 25 ++++---- include/dpp/coro/async.h | 64 ++++++++++++++++++--- include/dpp/coro/coroutine.h | 57 +++++++++++++++++- include/dpp/coro/task.h | 70 ++++++++++++++++++++++- 4 files changed, 191 insertions(+), 25 deletions(-) diff --git a/docpages/advanced_reference/coroutines.md b/docpages/advanced_reference/coroutines.md index 6d21002a9d..846af6c3ad 100644 --- a/docpages/advanced_reference/coroutines.md +++ b/docpages/advanced_reference/coroutines.md @@ -1,6 +1,6 @@ \page coroutines Advanced commands with coroutines -\warning D++ Coroutines are a very new feature and are currently only supported by D++ on g++ 13.1 and MSVC 19.37, and the CMake option DPP_CORO must be enabled. They are experimental and may have bugs or even crashes, please report any to [GitHub Issues](https://github.com/brainboxdotcc/DPP/issues) or to our [Discord Server](https://discord.gg/dpp). +\warning D++ Coroutines are a very new feature and are currently only supported by D++ on g++ 11, clang/LLVM 14, and MSVC 19.37 or above. Additionnally, D++ must be built with the CMake option DPP_CORO, and your program must both define the macro DPP_CORO and use C++20 or above. The feature is experimental and may have bugs or even crashes, please report any to [GitHub Issues](https://github.com/brainboxdotcc/DPP/issues) or to our [Discord Server](https://discord.gg/dpp). ### What is a coroutine? @@ -19,7 +19,7 @@ int main() { /* Message handler to look for a command called !file */ /* Make note of passing the event by value, this is important (explained below) */ - bot.on_message_create.co_attach([](dpp::message_create_t event) -> dpp::task { + bot.on_message_create.co_attach([](dpp::message_create_t event) -> dpp::job { dpp::cluster *cluster = event.from->creator; if (event.msg.content == "!file") { @@ -45,22 +45,19 @@ int main() { ~~~~~~~~~~~~~~~ -Coroutines can make commands simpler by eliminating callbacks, which can be very handy in the case of complex commands that rely on a lot of different data or steps. +Coroutines can make commands simpler by eliminating callbacks, which can be very handy in the case of complex commands that rely on a lot of different data or steps. -In order to be a coroutine, a function has to return a special type with special functions; D++ offers `dpp::task` which is designed to work seamlessly with asynchronous calls through `dpp::awaitable`, which all the functions starting with `co_` such as `dpp::cluster::co_message_create` return. To turn a function into a coroutine, simply make it return `dpp::task` as seen in the example at line 10. Inside of a `dpp::task`, someone can use `co_return` in place of `return` to return a value. +In order to be a coroutine, a function has to return a special type with special functions; D++ offers `dpp::job`, `dpp::task`, and `dpp::coroutine`, which are designed to work seamlessly with asynchronous calls through `dpp::async`, which all the functions starting with `co_` such as `dpp::cluster::co_message_create` return. Event routers can have a `dpp::job` attached to them, as this object allows to create coroutines that can execute on their own, asynchronously. More on that and the difference between it and the other two types later. To turn a function into a coroutine, simply make it return `dpp::job` as seen in the example at line 10, then use `co_await` on awaitable types or `co_return`. The moment the execution encounters one of these two keywords, the function is transformed into a coroutine. -When an awaitable is `co_await`-ed, the request is sent, the coroutine suspends (pauses) and returns back to its caller : in other words, the program is free to go and do other things while the data is being retrieved, D++ will resume your coroutine when it has the data you need which will be returned from the `co_await` expression. +When using a co_* function such as `co_message_create`, the request is sent immediately and the returned `dpp::async` can be `co_await`-ed, at which point the coroutine suspends (pauses) and returns back to its caller : in other words, the program is free to go and do other things while the data is being retrieved and D++ will resume your coroutine when it has the data you need, which will be returned from the `co_await` expression. -Awaitable objects can be wrapped with `dpp::async` : this will send the call immediately but not suspend the coroutine, allowing to execute several requests in parallel. The async object can then be co_awaited later when it is depended on. - -\attention As a rule of thumb when making dpp::task objects and in general coroutines, always prefer taking parameters by value and avoid capture : this may be confusing but a coroutine is *not* the lambda creating it, the captures are not bound to it and the code isn't ran inside the lambda. The lambda that returns a dpp::task simply returns a task object containing the code, which goes on to live on its own, separate from the lambda. -Similarly, with reference parameters, the object they reference to might be destroyed while the coroutine is suspended and resumed in another thread, which is why you want to pass by value. See also [lambdas and locals](/lambdas-and-locals.html) except this also applies to parameters in the case of coroutines. +\attention You may hear that coroutines are "writing async code as if it was sync", while this is sort of correct, it may limit your understandings and especially of the dangers of coroutines. I find **they are best thought of as a shortcut for a state machine**. If you've ever written one, you know what this means : think of the lambda as *its constructor*, in which captures are variable parameters. Think of the parameters passed to your lambda as data members in your state machine. References are kept as references, and by the time the state machine is resumed, the reference may be dangling : [this is not good](/lambdas-and-locals.html)! As a rule of thumb when making coroutines, **always prefer taking parameters by value and avoid lambda capture**. ### Several steps in one \note The next example assumes you are already familiar with how to use [slash commands](/firstbot.html), [parameters](/slashcommands.html), and [sending files through a command](/discord-application-command-file-upload.html). -Coroutines allow to write asynchronous functions almost as if they were executed synchronously, without the need for callbacks, which can save a lot of pain with keeping track of different data. Here is another example of what is made easier with coroutines : an "addemoji" command taking a file and a name as a parameter. This means downloading the emoji, submitting it to Discord, and finally replying, with some error handling along the way. +Here is another example of what is made easier with coroutines : an "addemoji" command taking a file and a name as a parameter. This means downloading the emoji, submitting it to Discord, and finally replying, with some error handling along the way. Normally we would have to use callbacks and some sort of object keeping track of our state, but with coroutines, it becomes much simpler : ~~~~~~~~~~{.cpp} #include @@ -70,7 +67,7 @@ int main() { bot.on_log(dpp::utility::cout_logger()); - bot.on_slashcommand.co_attach([](dpp::slashcommand_t event) -> dpp::task { + bot.on_slashcommand.co_attach([](dpp::slashcommand_t event) -> dpp::job { if (event.command.get_command_name() == "addemoji") { dpp::cluster *cluster = event.from->creator; // Retrieve parameter values @@ -133,9 +130,9 @@ int main() { \note This next example is fairly advanced and makes use of many of both C++ and D++'s advanced features. -Lastly, `dpp::task` takes its return type as a template parameter, which allows you to use tasks inside tasks and return a result from them. +Earlier we mentioned two other types of coroutines provided by dpp : `dpp::coroutine` and `dpp::task`. They both take their return type as a template parameter, which may be void. Both `dpp::job` and `dpp::task` start on the constructor for asynchronous execution, however only the latter can be co_await-ed, this allows you to retrieve its return value. If a `dpp::task` is destroyed before it ends, it is cancelled and will stop when it is resumed from the next `co_await`. `dpp::coroutine` also has a return value and can be co_await-ed, however it only starts when co_await-ing, meaning it is executed synchronously. -Here is an example of a command making use of that to retrieve the avatar of a specified user, or if missing, the sender : +Here is an example of a command making use of `dpp::task` to retrieve the avatar of a specified user, or if missing, the sender : ~~~~~~~~~~{.cpp} #include @@ -145,7 +142,7 @@ int main() { bot.on_log(dpp::utility::cout_logger()); - bot.on_slashcommand.co_attach([](dpp::slashcommand_t event) -> dpp::task{ + bot.on_slashcommand.co_attach([](dpp::slashcommand_t event) -> dpp::job { if (event.command.get_command_name() == "avatar") { // Make a nested coroutine to fetch the guild member requested, that returns it as an optional constexpr auto resolve_member = [](const dpp::slashcommand_t &event) -> dpp::task> { diff --git a/include/dpp/coro/async.h b/include/dpp/coro/async.h index 7b4e3ac5da..2eb5f18e6b 100644 --- a/include/dpp/coro/async.h +++ b/include/dpp/coro/async.h @@ -103,8 +103,9 @@ struct async_callback_data { }; /** - * @brief Base class of dpp::async. This class should not be used directly by a user, use dpp::async instead. + * @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 @@ -299,11 +300,8 @@ class async_base { async_base &operator=(async_base &&other) noexcept = default; /** - * @brief First function called by the standard library when the object is co-awaited. + * @brief Check whether or not co_await-ing this would suspend the caller, i.e. if we have the result or not * - * Returns whether we already have the result of the API call and don't need to suspend the caller. - * - * @remark Do not call this manually, use the co_await keyword instead. * @return bool Whether we already have the result of the API call or not */ bool await_ready() const noexcept { @@ -316,7 +314,7 @@ class async_base { * 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 handle The handle to the coroutine co_await-ing and being suspended + * @param caller The handle to the coroutine co_await-ing and being suspended */ bool await_suspend(detail::std_coroutine::coroutine_handle<> caller) noexcept { auto sent = detail::async_state_t::sent; @@ -372,7 +370,10 @@ struct confirmation_callback_t; template class async : private detail::async_base { /** - * @brief Base class has friend access for CRTP downcast + * @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_base; @@ -406,6 +407,55 @@ class async : private detail::async_base { #endif explicit async(Fun &&fun, Args&&... args) : detail::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 + */ + bool await_ready() const noexcept; +#endif + /** * @brief Suspend the caller until the request completes. * diff --git a/include/dpp/coro/coroutine.h b/include/dpp/coro/coroutine.h index 5cb20905f6..d16facb7ee 100644 --- a/include/dpp/coro/coroutine.h +++ b/include/dpp/coro/coroutine.h @@ -44,8 +44,9 @@ template using coroutine_handle = std_coroutine::coroutine_handle>; /** - * @brief Base class of dpp::coroutine. This class should not be used directly by a user, use dpp::coroutine instead. + * @brief Base class of dpp::coroutine. * + * @warn 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. */ template @@ -181,7 +182,10 @@ class coroutine_base { template class coroutine : private detail::coroutine_base { /** - * @brief Base class has friend access for CRTP downcast + * @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_base; @@ -207,9 +211,54 @@ class coroutine : private detail::coroutine_base { } public: +#ifdef _DOXYGEN_ // :)))) + /** + * @brief Default constructor, creates an empty coroutine. + */ + coroutine() = default; + + /** + * @brief Copy constructor is disabled + */ + coroutine(const coroutine &) = delete; + + /** + * @brief Move constructor, grabs another coroutine's handle + * + * @param other Coroutine to move the handle from + */ + coroutine(coroutine &&other) noexcept; + + /** + * @brief Destructor, destroys the handle. + */ + ~coroutine(); + + /** + * @brief Copy assignment is disabled + */ + coroutine &operator=(const coroutine &) = delete; + + /** + * @brief Move assignment, grabs another coroutine's handle + * + * @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 + */ + bool await_ready() const; +#else using detail::coroutine_base::coroutine_base; // use coroutine_base's constructors using detail::coroutine_base::operator=; // use coroutine_base's assignment operators using detail::coroutine_base::await_ready; // expose await_ready as public +#endif /** * @brief Suspend the caller until the coroutine completes. @@ -242,6 +291,7 @@ class coroutine : private detail::coroutine_base { } }; +#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. * @@ -293,6 +343,7 @@ class coroutine : private detail::coroutine_base { return static_cast&&>(*this); } }; +#endif /* _DOXYGEN_ */ namespace detail { template @@ -493,10 +544,12 @@ namespace detail { } // namespace detail +#ifndef _DOXYGEN_ inline void coroutine::await_resume_impl() const { if (handle.promise().exception) std::rethrow_exception(handle.promise().exception); } +#endif /* _DOXYGEN_ */ } // namespace dpp diff --git a/include/dpp/coro/task.h b/include/dpp/coro/task.h index 14228ce8f4..811aa4677f 100644 --- a/include/dpp/coro/task.h +++ b/include/dpp/coro/task.h @@ -68,8 +68,9 @@ template using task_handle = detail::std_coroutine::coroutine_handle>; /** - * @brief Base class of dpp::task. This class should not be used directly by a user, use dpp::task instead. + * @brief Base class of dpp::task. * + * @warning This class should not be used directly by a user, use dpp::task instead. * @note This class contains all the functions used internally by co_await. It is intentionally opaque and a private base of dpp::task so a user cannot call await_suspend and await_resume directly. */ template @@ -244,8 +245,9 @@ requires (!std::is_reference_v) #endif class task : private detail::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. + * @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_base; @@ -290,11 +292,71 @@ class task : private detail::task_base { } public: +#ifdef _DOXYGEN_ // :) + /** + * @brief Default constructor, creates a task not bound to a coroutine. + */ + task() = default; + + /** + * @brief Copy constructor is disabled + */ + task(const task &) = delete; + + /** + * @brief Move constructor, grabs another task's coroutine handle + * + * @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(); + + /** + * @brief Copy assignment is disabled + */ + task &operator=(const task &) = delete; + + /** + * @brief Move assignment, grabs another task's coroutine handle + * + * @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 + */ + bool await_ready() const; +#else using detail::task_base::task_base; // use task_base's constructors using detail::task_base::operator=; // use task_base's assignment operators using detail::task_base::done; // expose done() as public using detail::task_base::cancel; // expose cancel() as public using detail::task_base::await_ready; // expose await_ready as public +#endif /** * @brief Suspend the current coroutine until the task completes. @@ -327,6 +389,7 @@ class task : private detail::task_base { } }; +#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. * @@ -386,6 +449,7 @@ class task : private detail::task_base { return static_cast&&>(*this); } }; +#endif /* _DOXYGEN_ */ namespace detail { /** @@ -622,10 +686,12 @@ std_coroutine::coroutine_handle<> detail::task_chain_final_awaiter::await_sus } // namespace detail +#ifndef _DOXYGEN_ inline void task::await_resume_impl() const { if (handle.promise().exception) std::rethrow_exception(handle.promise().exception); } +#endif /* _DOXYGEN_ */ } // namespace dpp