Skip to content

Commit

Permalink
docs(coro): update docs for PR brainboxdotcc#763
Browse files Browse the repository at this point in the history
  • Loading branch information
Mishura4 committed Aug 21, 2023
1 parent 54eaffa commit b656360
Show file tree
Hide file tree
Showing 4 changed files with 191 additions and 25 deletions.
25 changes: 11 additions & 14 deletions docpages/advanced_reference/coroutines.md
Original file line number Diff line number Diff line change
@@ -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?

Expand All @@ -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<void> {
bot.on_message_create.co_attach([](dpp::message_create_t event) -> dpp::job {
dpp::cluster *cluster = event.from->creator;
if (event.msg.content == "!file") {
Expand All @@ -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<void>` 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<R>`, and `dpp::coroutine<R>`, 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 <dpp/dpp.h>
Expand All @@ -70,7 +67,7 @@ int main() {
bot.on_log(dpp::utility::cout_logger());
bot.on_slashcommand.co_attach([](dpp::slashcommand_t event) -> dpp::task<void> {
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
Expand Down Expand Up @@ -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<R>` and `dpp::task<R>`. They both take their return type as a template parameter, which may be void. Both `dpp::job` and `dpp::task<R>` 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<R>` is destroyed before it ends, it is cancelled and will stop when it is resumed from the next `co_await`. `dpp::coroutine<R>` 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<R>` to retrieve the avatar of a specified user, or if missing, the sender :

~~~~~~~~~~{.cpp}
#include <dpp/dpp.h>
Expand All @@ -145,7 +142,7 @@ int main() {
bot.on_log(dpp::utility::cout_logger());
bot.on_slashcommand.co_attach([](dpp::slashcommand_t event) -> dpp::task<void>{
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<std::optional<dpp::guild_member>> {
Expand Down
64 changes: 57 additions & 7 deletions include/dpp/coro/async.h
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,9 @@ struct async_callback_data {
};

/**
* @brief Base class of dpp::async<R>. This class should not be used directly by a user, use dpp::async<R> instead.
* @brief Base class of dpp::async<R>.
*
* @warning This class should not be used directly by a user, use dpp::async<R> instead.
* @note This class contains all the functions used internally by co_await. It is intentionally opaque and a private base of dpp::async<R> so a user cannot call await_suspend and await_resume directly.
*/
template <typename R>
Expand Down Expand Up @@ -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 {
Expand All @@ -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;
Expand Down Expand Up @@ -372,7 +370,10 @@ struct confirmation_callback_t;
template <typename R>
class async : private detail::async_base<R> {
/**
* @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<R>;

Expand Down Expand Up @@ -406,6 +407,55 @@ class async : private detail::async_base<R> {
#endif
explicit async(Fun &&fun, Args&&... args) : detail::async_base<R>{std::forward<Fun>(fun), std::forward<Args>(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.
*
Expand Down
57 changes: 55 additions & 2 deletions include/dpp/coro/coroutine.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,9 @@ template <typename R>
using coroutine_handle = std_coroutine::coroutine_handle<coroutine_promise<R>>;

/**
* @brief Base class of dpp::coroutine<R>. This class should not be used directly by a user, use dpp::coroutine<R> instead.
* @brief Base class of dpp::coroutine<R>.
*
* @warn This class should not be used directly by a user, use dpp::coroutine<R> instead.
* @note This class contains all the functions used internally by co_await. It is intentionally opaque and a private base of dpp::coroutine<R> so a user cannot call await_suspend and await_resume directly.
*/
template <typename R>
Expand Down Expand Up @@ -181,7 +182,10 @@ class coroutine_base {
template <typename R>
class coroutine : private detail::coroutine_base<R> {
/**
* @brief Base class has friend access for CRTP downcast
* @brief Internal use only base class containing common logic between coroutine<R> and coroutine<void>. 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<R>;

Expand All @@ -207,9 +211,54 @@ class coroutine : private detail::coroutine_base<R> {
}

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<R>::coroutine_base; // use coroutine_base's constructors
using detail::coroutine_base<R>::operator=; // use coroutine_base's assignment operators
using detail::coroutine_base<R>::await_ready; // expose await_ready as public
#endif

/**
* @brief Suspend the caller until the coroutine completes.
Expand Down Expand Up @@ -242,6 +291,7 @@ class coroutine : private detail::coroutine_base<R> {
}
};

#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.
*
Expand Down Expand Up @@ -293,6 +343,7 @@ class coroutine<void> : private detail::coroutine_base<void> {
return static_cast<detail::coroutine_base<void>&&>(*this);
}
};
#endif /* _DOXYGEN_ */

namespace detail {
template <typename R>
Expand Down Expand Up @@ -493,10 +544,12 @@ namespace detail {

} // namespace detail

#ifndef _DOXYGEN_
inline void coroutine<void>::await_resume_impl() const {
if (handle.promise().exception)
std::rethrow_exception(handle.promise().exception);
}
#endif /* _DOXYGEN_ */

} // namespace dpp

Expand Down
Loading

0 comments on commit b656360

Please sign in to comment.