Skip to content

Commit

Permalink
feat(coro): dpp::job, a lightweight async coroutine
Browse files Browse the repository at this point in the history
  • Loading branch information
Mishura4 committed Aug 10, 2023
1 parent 4374c9c commit 43945f5
Show file tree
Hide file tree
Showing 2 changed files with 130 additions and 30 deletions.
128 changes: 122 additions & 6 deletions include/dpp/coro.h
Original file line number Diff line number Diff line change
@@ -1,3 +1,24 @@
/************************************************************************************
*
* D++, A Lightweight C++ library for Discord
*
* Copyright 2022 Craig Edwards and D++ contributors
* (https://github.com/brainboxdotcc/DPP/graphs/contributors)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
************************************************************************************/

#ifdef DPP_CORO
#pragma once

Expand Down Expand Up @@ -188,7 +209,7 @@ namespace dpp {
* @return bool Whether to suspend the caller or not
*/
template <typename T>
bool await_suspend(detail::task_handle<T> caller) noexcept {
bool await_suspend(detail::std_coroutine::coroutine_handle<T> caller) {
auto &my_promise = handle.promise();

if (my_promise.is_sync)
Expand All @@ -198,8 +219,9 @@ namespace dpp {

if (handle.done())
return (false);
if constexpr (requires (T t) { t.is_sync = false; })
caller.promise().is_sync = false;
my_promise.parent = caller;
caller.promise().is_sync = false;
return true;
}

Expand Down Expand Up @@ -423,6 +445,85 @@ namespace dpp {
return *std::exchange(handle.promise().value, std::nullopt);
}

/**
* @brief Extremely light coroutine object designed to send off a coroutine to execute on its own.
*
* This object is extremely light, and is the preferred way to use coroutines if you do not need to co_await the result.
*
* @warning It cannot be co_awaited, which means the second it co_awaits something, the program jumps back to the calling function, which continues executing.
* At this point, if the function returns, every object declared in the function including its parameters are destroyed, which causes dangling references.
* This is exactly the same problem as references in lambdas : https://dpp.dev/lambdas-and-locals.html.
* For this reason, `co_await` will error if any parameters are passed by reference.
* If you must pass a reference, pass it as a pointer or with std::ref, but you must fully understand the reason behind this warning, and what to avoid.
* If you prefer a safer type, use `coroutine` for synchronous execution, or `task` for parallel tasks, and co_await them.
*/
class job {};

namespace detail {
/**
* @brief Coroutine promise type for a job
*/
template <bool has_reference_params>
struct job_promise {
/*
* @brief Function called when the job is done.
*
* @return Do not suspend at the end, destroying the handle immediately
*/
std_coroutine::suspend_never final_suspend() const noexcept {
return {};
}

/*
* @brief Function called when the job is started.
*
* @return Do not suspend at the start, starting the job immediately
*/
std_coroutine::suspend_never initial_suspend() const noexcept {
return {};
}

/**
* @brief Function called to get the job object
*
* @return job
*/
dpp::job get_return_object() const noexcept {
return {};
}

/**
* @brief Function called when an exception is thrown and not caught.
*
* @throw Immediately rethrows the exception to the caller / resumer
*/
void unhandled_exception() const noexcept(false) {
throw;
}

/**
* @brief Function called when the job returns. Does nothing.
*/
void return_void() const noexcept {}

template <typename T>
T await_transform(T &&expr) const noexcept {
/**
* `job` is extremely efficient as a coroutine but this comes with drawbacks :
* It cannot be co_awaited, which means the second it co_awaits something, the program jumps back to the calling function, which continues executing.
* At this point, if the function returns, every object declared in the function including its parameters are destroyed, which causes dangling references.
* This is exactly the same problem as references in lambdas : https://dpp.dev/lambdas-and-locals.html.
*
* If you must pass a reference, pass it as a pointer or with std::ref, but you must fully understand the reason behind this warning, and what to avoid.
* If you prefer a safer type, use `coroutine` for synchronous execution, or `task` for parallel tasks, and co_await them.
*/
static_assert(!has_reference_params, "co_await is disabled in dpp::job when taking parameters by reference. read comment above this line for more info");

return std::forward<T>(expr);
}
};
}

template <typename ReturnType = confirmation_callback_t>
class async;

Expand Down Expand Up @@ -734,7 +835,7 @@ namespace dpp {
* @param callable The awaitable object whose API call to execute.
*/
async(const awaitable<ReturnType> &awaitable) : api_callback{} {
std::invoke(awaitable.callable, api_callback);
std::invoke(awaitable.request, api_callback);
}

/**
Expand Down Expand Up @@ -798,13 +899,14 @@ namespace dpp {
* @param handle The handle to the coroutine co_await-ing and being suspended
*/
template <typename T>
bool await_suspend(detail::task_handle<T> handle) {
bool await_suspend(detail::std_coroutine::coroutine_handle<T> caller) {
std::lock_guard lock{api_callback.get_mutex()};

if (api_callback.get_result().has_value())
return false; // immediately resume the coroutine as we already have the result of the api call
handle.promise().is_sync = false;
api_callback.state->coro_handle = handle;
if constexpr (requires (T t) { t.is_sync = false; })
caller.promise().is_sync = false;
api_callback.state->coro_handle = caller;
return true; // suspend the caller, the callback will resume it
}

Expand All @@ -829,4 +931,18 @@ struct dpp::detail::std_coroutine::coroutine_traits<dpp::task<T>, Args...> {
using promise_type = dpp::detail::task_promise<T>;
};

/**
* @brief Specialization of std::coroutine_traits, helps the standard library figure out a promise type from a coroutine function.
*/
template<typename T, typename... Args>
struct dpp::detail::std_coroutine::coroutine_traits<dpp::job, T, Args...> {
/**
* @brief Promise type for this coroutine signature.
*
* When the coroutine is created from a lambda, that lambda is passed as a first parameter.
* Not ideal but we'll allow any callable that takes the rest of the arguments passed
*/
using promise_type = dpp::detail::job_promise<(std::is_reference_v<Args> || ... || (std::is_reference_v<T> && !std::is_invocable_v<T, Args...>))>;
};

#endif
32 changes: 8 additions & 24 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>(const T&)>> coroutine_container;
std::map<event_handle, std::function<dpp::job(const T&)>> coroutine_container;
#else
#ifndef _DOXYGEN_
/**
Expand Down Expand Up @@ -164,27 +164,10 @@ template<class T> class event_router_t {
}
};
#ifdef DPP_CORO
if (!coroutine_container.empty()) {
[](const event_router_t<T> *me, T event) -> dpp::task<void> {
std::vector<dpp::task<void>> coroutines;
auto *cluster = event.from ? event.from->creator : nullptr;

coroutines.reserve(me->coroutine_container.size());
for (const auto& [_, listener] : me->coroutine_container) {
if (event.is_cancelled())
break;
coroutines.emplace_back(listener(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());
}
}
}(this, event);
for (const auto& [_, listener] : coroutine_container) {
if (!event.is_cancelled()) {
listener(event);
}
}
#endif /* DPP_CORO */
};
Expand Down Expand Up @@ -254,11 +237,12 @@ template<class T> class event_router_t {
* the event object and should take exactly one parameter derived
* from event_dispatch_t.
*
* @param func Coroutine task to attack to the event
* @param func Coroutine task to attack to the event. <b>It MUST take the event by value.</b>
* @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>(const T&)> func) {
event_handle co_attach(std::function<job(T)> func) {
// ^ If this errors here - your event handler must take its parameter by VALUE
std::unique_lock l(lock);
event_handle h = next_handle++;
coroutine_container.emplace(h, func);
Expand Down

0 comments on commit 43945f5

Please sign in to comment.