Skip to content

Commit 3122a95

Browse files
committed
docs(coro): update docs for PR brainboxdotcc#763
1 parent e042133 commit 3122a95

File tree

4 files changed

+191
-25
lines changed

4 files changed

+191
-25
lines changed

docpages/advanced_reference/coroutines.md

+11-14
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
\page coroutines Advanced commands with coroutines
22

3-
\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).
3+
\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).
44

55
### What is a coroutine?
66

@@ -19,7 +19,7 @@ int main() {
1919
2020
/* Message handler to look for a command called !file */
2121
/* Make note of passing the event by value, this is important (explained below) */
22-
bot.on_message_create.co_attach([](dpp::message_create_t event) -> dpp::task<void> {
22+
bot.on_message_create.co_attach([](dpp::message_create_t event) -> dpp::job {
2323
dpp::cluster *cluster = event.from->creator;
2424
2525
if (event.msg.content == "!file") {
@@ -45,22 +45,19 @@ int main() {
4545
~~~~~~~~~~~~~~~
4646

4747

48-
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.
48+
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.
4949

50-
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.
50+
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.
5151

52-
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.
52+
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.
5353

54-
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.
55-
56-
\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.
57-
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.
54+
\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**.
5855

5956
### Several steps in one
6057

6158
\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).
6259

63-
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.
60+
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 :
6461

6562
~~~~~~~~~~{.cpp}
6663
#include <dpp/dpp.h>
@@ -70,7 +67,7 @@ int main() {
7067
7168
bot.on_log(dpp::utility::cout_logger());
7269
73-
bot.on_slashcommand.co_attach([](dpp::slashcommand_t event) -> dpp::task<void> {
70+
bot.on_slashcommand.co_attach([](dpp::slashcommand_t event) -> dpp::job {
7471
if (event.command.get_command_name() == "addemoji") {
7572
dpp::cluster *cluster = event.from->creator;
7673
// Retrieve parameter values
@@ -133,9 +130,9 @@ int main() {
133130

134131
\note This next example is fairly advanced and makes use of many of both C++ and D++'s advanced features.
135132

136-
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.
133+
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 and **must** be co_await-ed. This allows you to retrieve its return value. `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.
137134

138-
Here is an example of a command making use of that to retrieve the avatar of a specified user, or if missing, the sender :
135+
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 :
139136

140137
~~~~~~~~~~{.cpp}
141138
#include <dpp/dpp.h>
@@ -145,7 +142,7 @@ int main() {
145142
146143
bot.on_log(dpp::utility::cout_logger());
147144
148-
bot.on_slashcommand.co_attach([](dpp::slashcommand_t event) -> dpp::task<void>{
145+
bot.on_slashcommand.co_attach([](dpp::slashcommand_t event) -> dpp::job {
149146
if (event.command.get_command_name() == "avatar") {
150147
// Make a nested coroutine to fetch the guild member requested, that returns it as an optional
151148
constexpr auto resolve_member = [](const dpp::slashcommand_t &event) -> dpp::task<std::optional<dpp::guild_member>> {

include/dpp/coro/async.h

+57-7
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,9 @@ struct async_callback_data {
103103
};
104104

105105
/**
106-
* @brief Base class of dpp::async<R>. This class should not be used directly by a user, use dpp::async<R> instead.
106+
* @brief Base class of dpp::async<R>.
107107
*
108+
* @warning This class should not be used directly by a user, use dpp::async<R> instead.
108109
* @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.
109110
*/
110111
template <typename R>
@@ -299,11 +300,8 @@ class async_base {
299300
async_base &operator=(async_base &&other) noexcept = default;
300301

301302
/**
302-
* @brief First function called by the standard library when the object is co-awaited.
303+
* @brief Check whether or not co_await-ing this would suspend the caller, i.e. if we have the result or not
303304
*
304-
* Returns whether we already have the result of the API call and don't need to suspend the caller.
305-
*
306-
* @remark Do not call this manually, use the co_await keyword instead.
307305
* @return bool Whether we already have the result of the API call or not
308306
*/
309307
bool await_ready() const noexcept {
@@ -316,7 +314,7 @@ class async_base {
316314
* 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.
317315
*
318316
* @remark Do not call this manually, use the co_await keyword instead.
319-
* @param handle The handle to the coroutine co_await-ing and being suspended
317+
* @param caller The handle to the coroutine co_await-ing and being suspended
320318
*/
321319
bool await_suspend(detail::std_coroutine::coroutine_handle<> caller) noexcept {
322320
auto sent = detail::async_state_t::sent;
@@ -372,7 +370,10 @@ struct confirmation_callback_t;
372370
template <typename R>
373371
class async : private detail::async_base<R> {
374372
/**
375-
* @brief Base class has friend access for CRTP downcast
373+
* @brief Internal use only base class. It serves to prevent await_suspend and await_resume from being used directly.
374+
*
375+
* @warning For internal use only, do not use.
376+
* @see operator co_await()
376377
*/
377378
friend class detail::async_base<R>;
378379

@@ -406,6 +407,55 @@ class async : private detail::async_base<R> {
406407
#endif
407408
explicit async(Fun &&fun, Args&&... args) : detail::async_base<R>{std::forward<Fun>(fun), std::forward<Args>(args)...} {}
408409

410+
#ifdef _DOXYGEN_ // :)
411+
/**
412+
* @brief Construct an empty async. Using `co_await` on an empty async is undefined behavior.
413+
*/
414+
async() noexcept;
415+
416+
/**
417+
* @brief Destructor. If any callback is pending it will be aborted.
418+
*/
419+
~async();
420+
421+
/**
422+
* @brief Copy constructor is disabled
423+
*/
424+
async(const async &);
425+
426+
/**
427+
* @brief Move constructor
428+
*
429+
* 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.
430+
*
431+
* @remark Using the moved-from async after this function is undefined behavior.
432+
* @param other The async object to move the data from.
433+
*/
434+
async(async &&other) noexcept = default;
435+
436+
/**
437+
* @brief Copy assignment is disabled
438+
*/
439+
async &operator=(const async &) = delete;
440+
441+
/**
442+
* @brief Move assignment operator.
443+
*
444+
* 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.
445+
*
446+
* @remark Using the moved-from async after this function is undefined behavior.
447+
* @param other The async object to move the data from
448+
*/
449+
async &operator=(async &&other) noexcept = default;
450+
451+
/**
452+
* @brief Check whether or not co_await-ing this would suspend the caller, i.e. if we have the result or not
453+
*
454+
* @return bool Whether we already have the result of the API call or not
455+
*/
456+
bool await_ready() const noexcept;
457+
#endif
458+
409459
/**
410460
* @brief Suspend the caller until the request completes.
411461
*

include/dpp/coro/coroutine.h

+55-2
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,9 @@ template <typename R>
4444
using coroutine_handle = std_coroutine::coroutine_handle<coroutine_promise<R>>;
4545

4646
/**
47-
* @brief Base class of dpp::coroutine<R>. This class should not be used directly by a user, use dpp::coroutine<R> instead.
47+
* @brief Base class of dpp::coroutine<R>.
4848
*
49+
* @warn This class should not be used directly by a user, use dpp::coroutine<R> instead.
4950
* @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.
5051
*/
5152
template <typename R>
@@ -181,7 +182,10 @@ class coroutine_base {
181182
template <typename R>
182183
class coroutine : private detail::coroutine_base<R> {
183184
/**
184-
* @brief Base class has friend access for CRTP downcast
185+
* @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.
186+
*
187+
* @warning For internal use only, do not use.
188+
* @see operator co_await()
185189
*/
186190
friend class detail::coroutine_base<R>;
187191

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

209213
public:
214+
#ifdef _DOXYGEN_ // :))))
215+
/**
216+
* @brief Default constructor, creates an empty coroutine.
217+
*/
218+
coroutine() = default;
219+
220+
/**
221+
* @brief Copy constructor is disabled
222+
*/
223+
coroutine(const coroutine &) = delete;
224+
225+
/**
226+
* @brief Move constructor, grabs another coroutine's handle
227+
*
228+
* @param other Coroutine to move the handle from
229+
*/
230+
coroutine(coroutine &&other) noexcept;
231+
232+
/**
233+
* @brief Destructor, destroys the handle.
234+
*/
235+
~coroutine();
236+
237+
/**
238+
* @brief Copy assignment is disabled
239+
*/
240+
coroutine &operator=(const coroutine &) = delete;
241+
242+
/**
243+
* @brief Move assignment, grabs another coroutine's handle
244+
*
245+
* @param other Coroutine to move the handle from
246+
*/
247+
coroutine &operator=(coroutine &&other) noexcept;
248+
249+
/**
250+
* @brief First function called by the standard library when the coroutine is co_await-ed.
251+
*
252+
* @remark Do not call this manually, use the co_await keyword instead.
253+
* @throws invalid_operation_exception if the coroutine is empty or finished.
254+
* @return bool Whether the coroutine is done
255+
*/
256+
bool await_ready() const;
257+
#else
210258
using detail::coroutine_base<R>::coroutine_base; // use coroutine_base's constructors
211259
using detail::coroutine_base<R>::operator=; // use coroutine_base's assignment operators
212260
using detail::coroutine_base<R>::await_ready; // expose await_ready as public
261+
#endif
213262

214263
/**
215264
* @brief Suspend the caller until the coroutine completes.
@@ -242,6 +291,7 @@ class coroutine : private detail::coroutine_base<R> {
242291
}
243292
};
244293

294+
#ifndef _DOXYGEN_ // don't generate this on doxygen because `using` doesn't work and 2 copies of coroutine_base's docs is enough
245295
/**
246296
* @brief Base type for a coroutine, starts on co_await.
247297
*
@@ -293,6 +343,7 @@ class coroutine<void> : private detail::coroutine_base<void> {
293343
return static_cast<detail::coroutine_base<void>&&>(*this);
294344
}
295345
};
346+
#endif /* _DOXYGEN_ */
296347

297348
namespace detail {
298349
template <typename R>
@@ -493,10 +544,12 @@ namespace detail {
493544

494545
} // namespace detail
495546

547+
#ifndef _DOXYGEN_
496548
inline void coroutine<void>::await_resume_impl() const {
497549
if (handle.promise().exception)
498550
std::rethrow_exception(handle.promise().exception);
499551
}
552+
#endif /* _DOXYGEN_ */
500553

501554
} // namespace dpp
502555

0 commit comments

Comments
 (0)