Skip to content

Commit

Permalink
feat: coroutines, still experimental but useable (#724)
Browse files Browse the repository at this point in the history
  • Loading branch information
Mishura4 authored Jul 19, 2023
1 parent 7433586 commit a33ecb5
Show file tree
Hide file tree
Showing 12 changed files with 2,054 additions and 662 deletions.
18 changes: 10 additions & 8 deletions buildtools/classes/Generator/CoroGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ public function generateHeaderStart(): string
public function generateCppStart(): string
{
return $this->generateHeaderStart() . <<<EOT
#ifdef DPP_CORO
#include <dpp/export.h>
#include <dpp/snowflake.h>
Expand All @@ -80,6 +81,7 @@ public function checkForChanges(): bool
}

echo "-- Autogenerating include/dpp/cluster_coro_calls.h\n";
echo "-- Autogenerating src/dpp/cluster_coro_calls.cpp\n";
return true;
}

Expand All @@ -88,19 +90,15 @@ public function checkForChanges(): bool
*/
public function generateHeaderDef(string $returnType, string $currentFunction, string $parameters, string $noDefaults, string $parameterTypes, string $parameterNames): string
{
$parameterNames = preg_replace('/^, /', '', $parameterNames);
if (!empty($parameterNames)) {
$parameterNames .= ', ';
}
return "auto inline co_{$currentFunction}($noDefaults) {\n\treturn dpp::awaitable(this, [&] (auto cc) { this->$currentFunction({$parameterNames}cc); }); \n}\n\n";
return "awaitable<confirmation_callback_t> co_{$currentFunction}($parameters);\n\n";
}

/**
* @inheritDoc
*/
public function generateCppDef(string $returnType, string $currentFunction, string $parameters, string $noDefaults, string $parameterTypes, string $parameterNames): string
{
return '';
return "awaitable<confirmation_callback_t> cluster::co_${currentFunction}($noDefaults) {\n\treturn {this, static_cast<void (cluster::*)($parameterTypes". (!empty($parameterTypes) ? ", " : "") . "command_completion_event_t)>(&cluster::$currentFunction)$parameterNames};\n}\n\n";
}

/**
Expand All @@ -116,7 +114,7 @@ public function getCommentArray(): array
*/
public function saveHeader(string $content): void
{
$content .= "auto inline co_request(const std::string &url, http_method method, const std::string &postdata = \"\", const std::string &mimetype = \"text/plain\", const std::multimap<std::string, std::string> &headers = {}) {\n\treturn dpp::awaitable(this, [&] (auto cc) { this->request(url, method, cc, mimetype, headers); }); \n}\n\n";
$content .= "awaitable<http_request_completion_t> co_request(const std::string &url, http_method method, const std::string &postdata = \"\", const std::string &mimetype = \"text/plain\", const std::multimap<std::string, std::string> &headers = {});\n\n";
file_put_contents('include/dpp/cluster_coro_calls.h', $content);
}

Expand All @@ -125,7 +123,11 @@ public function saveHeader(string $content): void
*/
public function saveCpp(string $cppcontent): void
{
/* No cpp file to save, code is all inline */
$cppcontent .= "dpp::awaitable<dpp::http_request_completion_t> dpp::cluster::co_request(const std::string &url, http_method method, const std::string &postdata, const std::string &mimetype, const std::multimap<std::string, std::string> &headers) {\n\treturn awaitable<http_request_completion_t>{[&](auto &&cc) { this->request(url, method, cc, postdata, mimetype, headers); }};\n}
#endif
";
file_put_contents('src/dpp/cluster_coro_calls.cpp', $cppcontent);
}

}
1 change: 1 addition & 0 deletions docpages/04_advanced_reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
* \subpage coding-standards "Coding Style Standards"
* \subpage unit-tests "Unit Tests"
* \subpage lambdas-and-locals "Ownership of local variables and safely transferring into a lambda"
* \subpage coroutines "Advanced commands with coroutines"
228 changes: 228 additions & 0 deletions docpages/advanced_reference/coroutines.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
\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, pease report any to [GitHub Issues](https://github.com/brainboxdotcc/DPP/issues) or to our [Discord Server](https://discord.gg/dpp).

### What is a coroutine?

Introduced in C++20, coroutines are the solution to the impracticality of callbacks. In short, a coroutine is a function that can be paused and resumed later : they are an extremely powerful alternative to callbacks for asynchronous APIs in particular, as the function can be paused when waiting for an API response, and resumed when it is received.

Let's revisit [attaching a downloaded file](/attach-file.html), but this time with a coroutine :


~~~~~~~~~~~~~~~{.cpp}
#include <dpp/dpp.h>
int main() {
dpp::cluster bot("token", dpp::i_default_intents | dpp::i_message_content);
bot.on_log(dpp::utility::cout_logger());
/* 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> {
dpp::cluster *cluster = event.from->creator;
if (event.msg.content == "!file") {
// request an image and co_await the response
dpp::http_request_completion_t result = co_await cluster->co_request("https://dpp.dev/DPP-Logo.png", dpp::m_get);
// create a message
dpp::message msg(event.msg.channel_id, "This is my new attachment:");
// attach the image on success
if (result.status == 200) {
msg.add_file("logo.png", result.body);
}
// send the message
cluster->message_create(msg);
}
});
bot.start(dpp::st_wait);
return 0;
}
~~~~~~~~~~~~~~~


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.

When an awaitable is `co_await`-ed, 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.

Inside of a `dpp::task`, someone can use `co_return` in place of `return`.

\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.

### 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.

~~~~~~~~~~{.cpp}
#include <dpp/dpp.h>
int main() {
dpp::cluster bot("token", dpp::i_default_intents | dpp::i_message_content);
bot.on_log(dpp::utility::cout_logger());
bot.on_slashcommand.co_attach([](dpp::slashcommand_t event) -> dpp::task<void> {
if (event.command.get_command_name() == "addemoji") {
dpp::cluster *cluster = event.from->creator;
// Retrieve parameter values
dpp::snowflake file_id = std::get<dpp::snowflake>(event.get_parameter("file"));
std::string emoji_name = std::get<std::string>(event.get_parameter("name"));
// Get the attachment from the resolved list
const dpp::attachment &attachment = event.command.get_resolved_attachment(file_id);
// For simplicity for this example we only support PNG
if (attachment.content_type != "image/png") {
// While event.co_reply is available, we can just use event.reply, as we will exit the command anyway and don't need to wait on the result
event.reply("Error: type " + attachment.content_type + " not supported");
co_return;
}
// Send a "<bot> is thinking..." message, to wait on later so we can edit
dpp::awaitable thinking = event.co_thinking(false);
// Download and co_await the result
dpp::http_request_completion_t response = co_await cluster->co_request(attachment.url, dpp::m_get);
if (response.status != 200) { // Page didn't send the image
co_await thinking; // Wait for the thinking response to arrive so we can edit
event.edit_response("Error: could not download the attachment");
}
else {
// Load the image data in a dpp::emoji
dpp::emoji emoji(emoji_name);
emoji.load_image(response.body, dpp::image_type::i_png);
// Create the emoji and co_await the response
dpp::confirmation_callback_t confirmation = co_await cluster->co_guild_emoji_create(event.command.guild_id, emoji);
co_await thinking; // Wait for the thinking response to arrive so we can edit
if (confirmation.is_error())
event.edit_response("Error: could not add emoji: " + confirmation.get_error().message);
else // Success
event.edit_response("Successfully added " + confirmation.get<dpp::emoji>().get_mention()); // Show the new emoji
}
}
});
bot.on_ready([&bot](const dpp::ready_t & event) {
if (dpp::run_once<struct register_bot_commands>()) {
dpp::slashcommand command("addemoji", "Add an emoji", bot.me.id);
// Add file and name as required parameters
command.add_option(dpp::command_option(dpp::co_attachment, "file", "Select an image", true));
command.add_option(dpp::command_option(dpp::co_string, "name", "Name of the emoji to add", true));
bot.global_command_create(command);
}
});
bot.start(dpp::st_wait);
}
~~~~~~~~~~

### I heard you liked tasks

\note This next example is fairly advanced and makes uses 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.

Here is an example of a command making use of that to retrieve the avatar of a specified user, or if missing, the sender :

~~~~~~~~~~{.cpp}
#include <dpp/dpp.h>
int main() {
dpp::cluster bot("token", dpp::i_default_intents | dpp::i_message_content);
bot.on_log(dpp::utility::cout_logger());
bot.on_slashcommand.co_attach([](dpp::slashcommand_t event) -> dpp::task<void>{
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>> {
const dpp::command_value &user_param = event.get_parameter("user");
dpp::snowflake user_id;
if (std::holds_alternative<std::monostate>(user_param))
user_id = event.command.usr.id; // Parameter is empty so user is sender
else if (std::holds_alternative<dpp::snowflake>(user_param))
user_id = std::get<dpp::snowflake>(user_param); // Parameter has a user
// If we have the guild member in the command's resolved data, return it
const auto &member_map = event.command.resolved.members;
if (auto member = member_map.find(user_id); member != member_map.end())
co_return member->second;
// Try looking in guild cache
dpp::guild *guild = dpp::find_guild(event.command.guild_id);
if (guild) {
// Look in guild's member cache
if (auto member = guild->members.find(user_id); member != guild->members.end()) {
co_return member->second;
}
}
// Finally if everything else failed, request API
dpp::confirmation_callback_t confirmation = co_await event.from->creator->co_guild_get_member(event.command.guild_id, user_id);
if (confirmation.is_error())
co_return std::nullopt; // Member not found, return empty
else
co_return confirmation.get<dpp::guild_member>();
};
// Send a "<bot> is thinking..." message, to wait on later so we can edit
dpp::awaitable thinking = event.co_thinking(false);
// Call our coroutine defined above to retrieve the member requested
std::optional<dpp::guild_member> member = co_await resolve_member(event);
if (!member.has_value()) {
// Wait for the thinking response to arrive to make sure we can edit
co_await thinking;
event.edit_original_response(dpp::message{"User not found in this server!"});
co_return;
}
std::string avatar_url = member->get_avatar_url(512);
if (avatar_url.empty()) { // Member does not have a custom avatar for this server, get their user avatar
dpp::confirmation_callback_t confirmation = co_await event.from->creator->co_user_get_cached(member->user_id);
if (confirmation.is_error())
{
// Wait for the thinking response to arrive to make sure we can edit
co_await thinking;
event.edit_original_response(dpp::message{"User not found!"});
co_return;
}
avatar_url = confirmation.get<dpp::user_identified>().get_avatar_url(512);
}
// Wait for the thinking response to arrive to make sure we can edit
co_await thinking;
event.edit_original_response(dpp::message{avatar_url});
}
});
bot.on_ready([&bot](const dpp::ready_t & event) {
if (dpp::run_once<struct register_bot_commands>()) {
dpp::slashcommand command("avatar", "Get your or another user's avatar image", bot.me.id);
command.add_option(dpp::command_option(dpp::co_user, "user", "User to fetch the avatar from"));
bot.global_command_create(command);
}
});
bot.start(dpp::st_wait);
}
~~~~~~~~~~
10 changes: 10 additions & 0 deletions include/dpp/cluster.h
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,16 @@ class DPP_EXPORT cluster {
*/
bool stop_timer(timer t);

#ifdef DPP_CORO
/**
* @brief Start a one-time timer. Use the co_await keyword on its return value to suspend the coroutine until the timer ends
*
* @param seconds How long to run the timer for
* @return awaitable<timer> co_await-able object holding the timer_handle
*/
awaitable<timer> co_timer(uint64_t seconds);
#endif

/**
* @brief Get the dm channel for a user id
*
Expand Down
Loading

0 comments on commit a33ecb5

Please sign in to comment.