Skip to content

Commit

Permalink
refactor: dpp::awaitable now lazily starts on co_await, dpp::async no…
Browse files Browse the repository at this point in the history
…w for parallel awaitable requests
  • Loading branch information
Mishura4 committed Aug 8, 2023
1 parent af6e140 commit c0e8e13
Show file tree
Hide file tree
Showing 9 changed files with 370 additions and 239 deletions.
8 changes: 5 additions & 3 deletions buildtools/classes/Generator/CoroGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,9 @@ public function generateHeaderDef(string $returnType, string $currentFunction, s
*/
public function generateCppDef(string $returnType, string $currentFunction, string $parameters, string $noDefaults, string $parameterTypes, string $parameterNames): string
{
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";
if (substr($parameterNames, 0, 2) === ", ")
$parameterNames = substr($parameterNames, 2);
return "awaitable<confirmation_callback_t> cluster::co_${currentFunction}($noDefaults) {\n\treturn [=, this] (auto &&cc) { this->$currentFunction($parameterNames" . (empty($parameterNames) ? "": ", ") . "cc); };\n}\n\n";
}

/**
Expand All @@ -114,7 +116,7 @@ public function getCommentArray(): array
*/
public function saveHeader(string $content): void
{
$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";
$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\", std::multimap<std::string, std::string> headers = {});\n\n";
file_put_contents('include/dpp/cluster_coro_calls.h', $content);
}

Expand All @@ -123,7 +125,7 @@ public function saveHeader(string $content): void
*/
public function saveCpp(string $cppcontent): void
{
$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}
$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, std::multimap<std::string, std::string> headers) {\n\treturn awaitable<http_request_completion_t>{[=, this, h = std::move(headers)](auto &&cc) { this->request(url, method, cc, postdata, mimetype, h); }};\n}
#endif
";
Expand Down
14 changes: 7 additions & 7 deletions include/dpp/cluster.h
Original file line number Diff line number Diff line change
Expand Up @@ -350,12 +350,12 @@ class DPP_EXPORT cluster {

#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
* @brief Get an awaitable to wait a certain amount of seconds. Use the co_await keyword on its return value to suspend the coroutine until the timer ends
*
* @param seconds How long to wait for
* @return awaitable<timer> Object that can be co_await-ed to suspend the function for a certain time
*/
awaitable<timer> co_timer(uint64_t seconds);
awaitable<timer> co_sleep(uint64_t seconds);
#endif

/**
Expand Down Expand Up @@ -3241,7 +3241,7 @@ class DPP_EXPORT cluster {
* @param callback Function to call when the API call completes.
* On success the callback will contain a dpp::sticker object in confirmation_callback_t::value. On failure, the value is undefined and confirmation_callback_t::is_error() method will return true. You can obtain full error details with confirmation_callback_t::get_error().
*/
void guild_sticker_create(sticker &s, command_completion_event_t callback = utility::log_error());
void guild_sticker_create(const sticker &s, command_completion_event_t callback = utility::log_error());

/**
* @brief Modify a sticker in a guild
Expand All @@ -3251,7 +3251,7 @@ class DPP_EXPORT cluster {
* @param callback Function to call when the API call completes.
* On success the callback will contain a dpp::sticker object in confirmation_callback_t::value. On failure, the value is undefined and confirmation_callback_t::is_error() method will return true. You can obtain full error details with confirmation_callback_t::get_error().
*/
void guild_sticker_modify(sticker &s, command_completion_event_t callback = utility::log_error());
void guild_sticker_modify(const sticker &s, command_completion_event_t callback = utility::log_error());

/**
* @brief Delete a sticker from a guild
Expand Down
6 changes: 3 additions & 3 deletions include/dpp/cluster_coro_calls.h
Original file line number Diff line number Diff line change
Expand Up @@ -1742,7 +1742,7 @@ awaitable<confirmation_callback_t> co_stage_instance_delete(const snowflake chan
* @return sticker returned object on completion
* \memberof dpp::cluster
*/
awaitable<confirmation_callback_t> co_guild_sticker_create(sticker &s);
awaitable<confirmation_callback_t> co_guild_sticker_create(const sticker &s);

/**
* @brief Delete a sticker from a guild
Expand Down Expand Up @@ -1776,7 +1776,7 @@ awaitable<confirmation_callback_t> co_guild_sticker_get(snowflake id, snowflake
* @return sticker returned object on completion
* \memberof dpp::cluster
*/
awaitable<confirmation_callback_t> co_guild_sticker_modify(sticker &s);
awaitable<confirmation_callback_t> co_guild_sticker_modify(const sticker &s);

/**
* @brief Get all guild stickers
Expand Down Expand Up @@ -2390,5 +2390,5 @@ awaitable<confirmation_callback_t> co_get_webhook_with_token(snowflake webhook_i


/* End of auto-generated definitions */
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 = {});
awaitable<http_request_completion_t> co_request(const std::string &url, http_method method, const std::string &postdata = "", const std::string &mimetype = "text/plain", std::multimap<std::string, std::string> headers = {});

145 changes: 133 additions & 12 deletions include/dpp/coro.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ namespace std {

#include <memory>
#include <utility>
#include <tuple>
#include <type_traits>
#include <concepts>
#include <optional>
Expand Down Expand Up @@ -174,7 +175,7 @@ namespace dpp {
* @return bool Whether not to suspend the caller or not
*/
bool await_ready() {
return handle.done();
return handle.promise().is_sync;
}

/**
Expand All @@ -192,8 +193,13 @@ namespace dpp {

if (my_promise.is_sync)
return false;

std::lock_guard lock{my_promise.mutex};

if (handle.done())
return (false);
my_promise.parent = caller;
caller.promise().is_sync = false;
caller.promise().is_sync = false;
return true;
}

Expand Down Expand Up @@ -414,12 +420,117 @@ namespace dpp {
if (handle.promise().exception) // If we have an exception, rethrow
std::rethrow_exception(handle.promise().exception);
if constexpr (!std::is_same_v<ReturnType, void>) // If we have a return type, return it and clean up our stored value
return std::forward<ReturnType>(*std::exchange(handle.promise().value, std::nullopt));
return *std::exchange(handle.promise().value, std::nullopt);
}

template <typename ReturnType = confirmation_callback_t>
class async;

/**
* @brief A co_await-able object handling an API 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 <a href="https://github.com/brainboxdotcc/DPP/issues">GitHub issues</a> or to the <a href="https://discord.gg/dpp">D++ Discord server</a>.
* @tparam ReturnType The return type of the API call. Defaults to confirmation_callback_t
*/
template <typename ReturnType = confirmation_callback_t>
struct awaitable {
/**
* @brief Construct an awaitable object from a callable. This can be used to manually wrap an async call.
*
* Callable should be an invokeable object, taking a parameter that is the callback to be passed to the async call.
* For example : `[cluster, message](auto &&cb) { cluster->message_create(message, cb); }
*
* @warning This callback is to be executed <b>later</b>, on co_await. <a href="/lambdas-and-locals.html">Be mindful of reference captures</a>.
*/
awaitable(std::invocable<std::function<void(ReturnType)>> auto &&fun) : callable{fun} {}

/**
* @brief Awaitable is copyable.
*/
awaitable(const awaitable&) = default;

/**
* @brief Awaitable is moveable.
*/
awaitable(awaitable&&) noexcept = default;

/**
* @brief Awaitable is copyable.
*/
awaitable& operator=(const awaitable&) = default;

/**
* @brief Awaitable is moveable.
*/
awaitable& operator=(awaitable&&) noexcept = default;

using request_fun_t = std::function<void(std::function<void(ReturnType)>)>;

/**
* @brief Callable object that will be responsible for the API call when co_await-ing.
*/
request_fun_t callable;

/**
* @brief Awaitable object returned by operator co_await.
*
* Do not use this directly, it is designed to be used with co_await.
*/
struct awaiter {
/**
* @brief Reference to the callable object that will be responsible for the API call when co_await-ing.
*/
const request_fun_t& fun;

/**
* @brief Optional containing the result of the API call.
*/
std::optional<ReturnType> result = std::nullopt;

/**
* @brief First function called by the standard library when this object is co_await-ed. Returns whether or not we can skip suspending the caller.
*
* @return false Always return false, we send the API call on suspend.
*/
bool await_ready() const noexcept {
return false;
}

/**
* @brief Second function called by the standard library when this object is co_await-ed. Suspends and sends the API call.
*/
template <typename T>
void await_suspend(detail::std_coroutine::coroutine_handle<T> caller) noexcept(noexcept(std::invoke(fun, std::declval<std::function<void(ReturnType)>&&>()))) {
if constexpr (requires (T promise) {{promise.is_sync} -> std::same_as<bool&>;})
caller.promise().is_sync = false;
std::invoke(fun, [this, caller](auto &&api_result) {
result = api_result;
caller.resume();
});
}

/**
* @brief Function called by the standard library when the handle is resumed. Returns the API result as an rvalue.
*/
ReturnType await_resume() {
return *std::exchange(result, std::nullopt);
}
};

/**
* @brief Overload of co_await for this object, the caller is suspended and the API call is executed. On completion the whole co_await expression evaluates to the result of the API call as an rvalue.
*
* In contrast with dpp::async, it is fine to co_await this object more than once.
*/
auto operator co_await() const noexcept {
return awaiter{callable};
}
};

/**
* @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 - This object's methods, other than constructors and operators, should not be called directly. It is designed to be used with coroutine keywords such as co_await.
Expand All @@ -428,8 +539,8 @@ namespace dpp {
* @warning This feature is EXPERIMENTAL. The API may change at any time and there may be bugs. Please report any to <a href="https://github.com/brainboxdotcc/DPP/issues">GitHub issues</a> or to the <a href="https://discord.gg/dpp">D++ Discord server</a>.
* @tparam ReturnType The return type of the API call. Defaults to confirmation_callback_t
*/
template <typename ReturnType = confirmation_callback_t>
class awaitable {
template <typename ReturnType>
class async {
/**
* @brief Ref-counted callback, contains the callback logic and manages the lifetime of the callback data over multiple threads.
*/
Expand Down Expand Up @@ -596,7 +707,7 @@ namespace dpp {
#ifndef _DOXYGEN_
requires std::invocable<Fun, Obj, Args..., std::function<void(ReturnType)>>
#endif
awaitable(Obj &&obj, Fun &&fun, Args&&... args) : api_callback{} {
async(Obj &&obj, Fun &&fun, Args&&... args) : api_callback{} {
std::invoke(std::forward<Fun>(fun), std::forward<Obj>(obj), std::forward<Args>(args)..., api_callback);
}

Expand All @@ -610,22 +721,31 @@ namespace dpp {
#ifndef _DOXYGEN_
requires std::invocable<Fun, Args..., std::function<void(ReturnType)>>
#endif
awaitable(Fun &&fun, Args&&... args) : api_callback{} {
async(Fun &&fun, Args&&... args) : api_callback{} {
std::invoke(std::forward<Fun>(fun), std::forward<Args>(args)..., api_callback);
}

/**
* @brief Construct an async wrapping an awaitable, the call is made immediately by forwarding to <a href="https://en.cppreference.com/w/cpp/utility/functional/invoke">std::invoke</a> and can be awaited later to retrieve the result.
*
* @param callable The awaitable object whose API call to execute.
*/
async(const awaitable<ReturnType> &awaitable) : api_callback{} {
std::invoke(awaitable.callable, api_callback);
}

/**
* @brief Destructor. If any callback is pending it will be aborted.
*
*/
~awaitable() {
~async() {
api_callback.set_dangling();
}

/**
* @brief Copy constructor is disabled
*/
awaitable(const awaitable &) = delete;
async(const async &) = delete;

/**
* @brief Move constructor
Expand All @@ -635,12 +755,12 @@ namespace dpp {
* @remark Using the moved-from awaitable after this function is undefined behavior.
* @param other The awaitable object to move the data from.
*/
awaitable(awaitable &&other) noexcept = default;
async(async &&other) noexcept = default;

/**
* @brief Copy assignment is disabled
*/
awaitable &operator=(const awaitable &) = delete;
async &operator=(const async &) = delete;

/**
* @brief Move assignment operator.
Expand All @@ -650,7 +770,7 @@ namespace dpp {
* @remark Using the moved-from awaitable after this function is undefined behavior.
* @param other The awaitable object to move the data from
*/
awaitable &operator=(awaitable &&other) noexcept = default;
async &operator=(async &&other) noexcept = default;

/**
* @brief First function called by the standard library when the object is co-awaited.
Expand Down Expand Up @@ -692,6 +812,7 @@ namespace dpp {
* @return ReturnType The result of the API call.
*/
ReturnType await_resume() {
// no locking needed here as the callback has already executed
return std::move(*api_callback.get_result());
}
};
Expand Down
42 changes: 25 additions & 17 deletions include/dpp/event_router.h
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ template<class T> class event_router_t {
*
* Note: keep a listener's parameter as a value type, the event passed can die while a coroutine is suspended
*/
std::map<event_handle, std::function<dpp::task<void>(T)>> coroutine_container;
std::map<event_handle, std::function<dpp::task<void>(const T&)>> coroutine_container;
#else
#ifndef _DOXYGEN_
/**
Expand Down Expand Up @@ -164,22 +164,30 @@ template<class T> class event_router_t {
}
});
#ifdef DPP_CORO
auto coro_exception_handler = [from = event.from](std::exception_ptr ptr) {
try {
std::rethrow_exception(ptr);
}
catch (const std::exception &exception) {
if (from && from->creator)
from->creator->log(dpp::loglevel::ll_error, std::string{"Uncaught exception in event coroutine: "} + exception.what());
}
};
std::for_each(coroutine_container.begin(), coroutine_container.end(), [&](auto &ev) {
if (!event.is_cancelled()) {
dpp::task<void> task = ev.second(event);
if (!coroutine_container.empty()) {
[](const event_router_t<T> *me, T event) -> dpp::task<void> {
std::vector<dpp::task<void>> coroutines;
dpp::cluster *cluster = event.from ? event.from->creator : nullptr;

task.on_exception(coro_exception_handler);
}
});
coroutines.reserve(me->coroutine_container.size());
for (const auto &elem : me->coroutine_container) {
if (event.is_cancelled())
break;
coroutines.emplace_back(elem.second(event));
}
for (auto &coro : coroutines) {
try {
co_await coro;
}
catch (const std::exception &e)
{
if (cluster)
cluster->log(dpp::loglevel::ll_error, std::string{"Uncaught exception in event coroutine: "} + e.what());
}
}
std::cout << "done" << std::endl;
}(this, event);
}
#endif /* DPP_CORO */
};

Expand Down Expand Up @@ -252,7 +260,7 @@ template<class T> class event_router_t {
* @return event_handle An event handle unique to this event, used to
* detach the listener from the event later if necessary.
*/
event_handle co_attach(std::function<dpp::task<void>(T)> func) {
event_handle co_attach(std::function<dpp::task<void>(const T&)> func) {
std::unique_lock l(lock);
event_handle h = next_handle++;
coroutine_container.emplace(h, func);
Expand Down
Loading

0 comments on commit c0e8e13

Please sign in to comment.