Added support for forwarding exceptions via Future.

This commit is contained in:
Patrick Wuttke 2025-09-22 21:39:03 +02:00
parent 4d19752964
commit 7d6fcc60fc
2 changed files with 187 additions and 48 deletions

View File

@ -5,10 +5,6 @@
#define MIJIN_ASYNC_COROUTINE_HPP_INCLUDED 1 #define MIJIN_ASYNC_COROUTINE_HPP_INCLUDED 1
#if !defined(MIJIN_COROUTINE_ENABLE_DEBUG_INFO)
# define MIJIN_COROUTINE_ENABLE_DEBUG_INFO 0 // Capture stack each time a coroutine is started. Warning, expensive! // TODO: maybe implement a lighter version only storing the return address?
#endif
#include <any> #include <any>
#include <chrono> #include <chrono>
#include <coroutine> #include <coroutine>
@ -27,6 +23,15 @@
#include "../util/misc.hpp" #include "../util/misc.hpp"
#include "../util/scope_guard.hpp" #include "../util/scope_guard.hpp"
#include "../util/traits.hpp" #include "../util/traits.hpp"
#if !defined(MIJIN_COROUTINE_ENABLE_DEBUG_INFO)
# define MIJIN_COROUTINE_ENABLE_DEBUG_INFO 0 // Capture stack each time a coroutine is started. Warning, expensive! // TODO: maybe implement a lighter version only storing the return address?
#endif
#if !defined(MIJIN_COROUTINE_ENABLE_EXCEPTIONS)
# define MIJIN_COROUTINE_ENABLE_EXCEPTIONS 0
#endif
#if MIJIN_COROUTINE_ENABLE_DEBUG_INFO #if MIJIN_COROUTINE_ENABLE_DEBUG_INFO
#include "../debug/stacktrace.hpp" #include "../debug/stacktrace.hpp"
#endif #endif
@ -186,10 +191,16 @@ struct TaskReturn<void, TPromise>
}; };
} }
template<typename TValue> template<typename TValue, template<typename> typename TAllocator = MIJIN_DEFAULT_ALLOCATOR>
using TaskFuture = Future<TValue, TAllocator, MIJIN_COROUTINE_ENABLE_EXCEPTIONS>;
template<typename TValue, template<typename> typename TAllocator = MIJIN_DEFAULT_ALLOCATOR>
using TaskFuturePtr = FuturePtr<TValue, TAllocator, MIJIN_COROUTINE_ENABLE_EXCEPTIONS>;
template<typename TValue, template<typename> typename TAllocator>
struct TaskAwaitableFuture struct TaskAwaitableFuture
{ {
FuturePtr<TValue> future; TaskFuturePtr<TValue, TAllocator> future;
[[nodiscard]] constexpr bool await_ready() const MIJIN_NOEXCEPT { return future->ready(); } [[nodiscard]] constexpr bool await_ready() const MIJIN_NOEXCEPT { return future->ready(); }
constexpr void await_suspend(std::coroutine_handle<>) const MIJIN_NOEXCEPT {} constexpr void await_suspend(std::coroutine_handle<>) const MIJIN_NOEXCEPT {}
@ -329,12 +340,12 @@ struct TaskPromise : impl::TaskReturn<typename TTraits::result_t, TaskPromise<TT
// constexpr void unhandled_exception() MIJIN_NOEXCEPT {} // constexpr void unhandled_exception() MIJIN_NOEXCEPT {}
template<typename TValue> template<typename TValue, template<typename> typename TAllocator>
auto await_transform(FuturePtr<TValue> future, std::source_location sourceLoc = std::source_location::current()) MIJIN_NOEXCEPT auto await_transform(TaskFuturePtr<TValue, TAllocator> future, std::source_location sourceLoc = std::source_location::current()) MIJIN_NOEXCEPT
{ {
MIJIN_ASSERT(loop_ != nullptr, "Cannot await future outside of a loop!"); MIJIN_ASSERT(loop_ != nullptr, "Cannot await future outside of a loop!");
sharedState_->sourceLoc = std::move(sourceLoc); sharedState_->sourceLoc = std::move(sourceLoc);
TaskAwaitableFuture<TValue> awaitable{future}; TaskAwaitableFuture<TValue, TAllocator> awaitable{future};
if (!awaitable.await_ready()) if (!awaitable.await_ready())
{ {
state_.status = TaskStatus::WAITING; state_.status = TaskStatus::WAITING;
@ -635,10 +646,10 @@ public:
void setUncaughtExceptionHandler(exception_handler_t handler) MIJIN_NOEXCEPT { uncaughtExceptionHandler_ = std::move(handler); } void setUncaughtExceptionHandler(exception_handler_t handler) MIJIN_NOEXCEPT { uncaughtExceptionHandler_ = std::move(handler); }
template<typename TResult> template<typename TResult>
FuturePtr<TResult> addTaskImpl(TaskBase<TResult, TAllocator> task, TaskHandle* outHandle) MIJIN_NOEXCEPT; TaskFuturePtr<TResult, TAllocator> addTaskImpl(TaskBase<TResult, TAllocator> task, TaskHandle* outHandle) MIJIN_NOEXCEPT;
template<typename TResult> template<typename TResult>
FuturePtr<TResult> addTask(TaskBase<TResult, TAllocator> task, TaskHandle* outHandle = nullptr) MIJIN_NOEXCEPT TaskFuturePtr<TResult, TAllocator> addTask(TaskBase<TResult, TAllocator> task, TaskHandle* outHandle = nullptr) MIJIN_NOEXCEPT
{ {
static_assert(TaskAllocatorTraits<TAllocator>::default_is_valid_v, "Allocator is not valid when default constructed, use makeTask() instead."); static_assert(TaskAllocatorTraits<TAllocator>::default_is_valid_v, "Allocator is not valid when default constructed, use makeTask() instead.");
return addTaskImpl(std::move(task), outHandle); return addTaskImpl(std::move(task), outHandle);
@ -664,7 +675,7 @@ protected:
protected: protected:
static inline TaskLoop*& currentLoopStorage() MIJIN_NOEXCEPT; static inline TaskLoop*& currentLoopStorage() MIJIN_NOEXCEPT;
template<typename TResult> template<typename TResult>
static inline void setFutureHelper(StoredTask& storedTask) MIJIN_NOEXCEPT; static inline void setFutureHelper(StoredTask& storedTask) MIJIN_NOEXCEPT_IF(!MIJIN_COROUTINE_ENABLE_EXCEPTIONS);
}; };
template<typename TResult = void> template<typename TResult = void>
@ -811,12 +822,12 @@ TaskBase<TResult, TAllocator>::~TaskBase() MIJIN_NOEXCEPT
template<template<typename> typename TAllocator> template<template<typename> typename TAllocator>
template<typename TResult> template<typename TResult>
FuturePtr<TResult> TaskLoop<TAllocator>::addTaskImpl(TaskBase<TResult, TAllocator> task, TaskHandle* outHandle) MIJIN_NOEXCEPT TaskFuturePtr<TResult, TAllocator> TaskLoop<TAllocator>::addTaskImpl(TaskBase<TResult, TAllocator> task, TaskHandle* outHandle) MIJIN_NOEXCEPT
{ {
MIJIN_ASSERT(!task.getLoop(), "Attempting to add task that already has a loop!"); MIJIN_ASSERT(!task.getLoop(), "Attempting to add task that already has a loop!");
task.setLoop(this); task.setLoop(this);
FuturePtr<TResult> future = std::allocate_shared<Future<TResult>>(TAllocator<Future<TResult>>(allocator_), allocator_); TaskFuturePtr<TResult, TAllocator> future = std::allocate_shared<TaskFuture<TResult, TAllocator>>(TAllocator<Future<TResult, TAllocator>>(allocator_), allocator_);
auto setFuture = &setFutureHelper<TResult>; auto setFuture = &setFutureHelper<TResult>;
if (outHandle != nullptr) if (outHandle != nullptr)
@ -855,7 +866,7 @@ TaskStatus TaskLoop<TAllocator>::tickTask(StoredTask& task)
while (status == TaskStatus::RUNNING); while (status == TaskStatus::RUNNING);
impl::gCurrentTaskState = nullptr; impl::gCurrentTaskState = nullptr;
#if MIJIN_COROUTINE_ENABLE_EXCEPTION_HANDLING #if MIJIN_COROUTINE_ENABLE_EXCEPTION_HANDLING && !MIJIN_COROUTINE_ENABLE_EXCEPTIONS
if (task.task && task.task->exception()) if (task.task && task.task->exception())
{ {
try try
@ -882,9 +893,24 @@ TaskStatus TaskLoop<TAllocator>::tickTask(StoredTask& task)
} }
#endif // MIJIN_COROUTINE_ENABLE_EXCEPTION_HANDLING #endif // MIJIN_COROUTINE_ENABLE_EXCEPTION_HANDLING
if (status == TaskStatus::YIELDED || status == TaskStatus::FINISHED) if (status == TaskStatus::YIELDED || status == TaskStatus::FINISHED)
{
try
{ {
task.setFuture(task); task.setFuture(task);
} }
catch(TaskCancelled&) {}
catch(...)
{
if (uncaughtExceptionHandler_)
{
uncaughtExceptionHandler_(std::current_exception());
}
else
{
throw;
}
}
}
return status; return status;
} }
@ -910,10 +936,22 @@ template<template<typename> typename TAllocator>
template<template<typename> typename TAllocator> template<template<typename> typename TAllocator>
template<typename TResult> template<typename TResult>
/* static */ inline void TaskLoop<TAllocator>::setFutureHelper(StoredTask& storedTask) MIJIN_NOEXCEPT /* static */ inline void TaskLoop<TAllocator>::setFutureHelper(StoredTask& storedTask) MIJIN_NOEXCEPT_IF(!MIJIN_COROUTINE_ENABLE_EXCEPTIONS)
{ {
TaskBase<TResult, TAllocator>& task = *static_cast<TaskBase<TResult, TAllocator>*>(storedTask.task->raw()); TaskBase<TResult, TAllocator>& task = *static_cast<TaskBase<TResult, TAllocator>*>(storedTask.task->raw());
auto future = std::any_cast<FuturePtr<TResult>>(storedTask.resultData); const auto& future = std::any_cast<TaskFuturePtr<TResult, TAllocator>&>(storedTask.resultData);
#if MIJIN_COROUTINE_ENABLE_EXCEPTIONS
if (task.state().exception)
{
if (future.use_count() < 2)
{
// future has been discarded, but someone must handle the exception
std::rethrow_exception(task.state().exception);
}
future->setException(task.state().exception);
return;
}
#endif
if constexpr (!std::is_same_v<TResult, void>) if constexpr (!std::is_same_v<TResult, void>)
{ {
@ -1249,14 +1287,14 @@ inline TaskAwaitableSuspend c_suspend() {
return TaskAwaitableSuspend(); return TaskAwaitableSuspend();
} }
template<template<typename...> typename TCollection, typename TType, typename... TTemplateArgs> template<template<typename...> typename TCollection, FutureType TFuture, typename... TTemplateArgs>
Task<> c_allDone(const TCollection<FuturePtr<TType>, TTemplateArgs...>& futures) Task<> c_allDone(const TCollection<TFuture, TTemplateArgs...>& futures)
{ {
bool allDone = true; bool allDone = true;
do do
{ {
allDone = true; allDone = true;
for (const FuturePtr<TType>& future : futures) for (const TFuture& future : futures)
{ {
if (future && !future->ready()) { if (future && !future->ready()) {
allDone = false; allDone = false;

View File

@ -4,6 +4,7 @@
#if !defined(MIJIN_ASYNC_FUTURE_HPP_INCLUDED) #if !defined(MIJIN_ASYNC_FUTURE_HPP_INCLUDED)
#define MIJIN_ASYNC_FUTURE_HPP_INCLUDED 1 #define MIJIN_ASYNC_FUTURE_HPP_INCLUDED 1
#include <exception>
#include <memory> #include <memory>
#include <optional> #include <optional>
#include <tuple> #include <tuple>
@ -27,43 +28,113 @@ namespace mijin
// //
// public types // public types
// //
template<typename TValue, template<typename> typename TAllocator = MIJIN_DEFAULT_ALLOCATOR> template<typename TValue, template<typename> typename TAllocator = MIJIN_DEFAULT_ALLOCATOR, bool exceptions = false>
class Future; class Future;
// TODO: add support for mutexes and waiting for futures // TODO: add support for mutexes and waiting for futures
namespace impl namespace impl
{ {
template<typename TValue> template<typename TValue, bool exceptions>
struct FutureStorage struct FutureStorage
{ {
Optional<TValue> value; Optional<TValue> value;
[[nodiscard]]
bool hasValue() const MIJIN_NOEXCEPT
{
return !value.empty();
}
void setValue(TValue value_) MIJIN_NOEXCEPT { value = std::move(value_); } void setValue(TValue value_) MIJIN_NOEXCEPT { value = std::move(value_); }
[[nodiscard]] TValue& getValue() MIJIN_NOEXCEPT { return value.get(); } [[nodiscard]] TValue& getValue() MIJIN_NOEXCEPT { return value.get(); }
}; };
// template<typename TValue> template<>
// struct FutureStorage<TValue&> struct FutureStorage<void, false>
// { {
// Optional<TValue*> value; bool isSet = false;
//
// void setValue(TValue& value_) MIJIN_NOEXCEPT { value = &value_; } [[nodiscard]]
// [[nodiscard]] TValue& getValue() const MIJIN_NOEXCEPT { return *value.get(); } bool hasValue() const MIJIN_NOEXCEPT
// }; {
return isSet;
}
void setValue() MIJIN_NOEXCEPT
{
isSet = true;
}
void getValue() MIJIN_NOEXCEPT {}
};
#if MIJIN_ENABLE_EXCEPTIONS
template<typename TValue>
struct FutureStorage<TValue, true>
{
Optional<TValue> value;
std::exception_ptr exception;
[[nodiscard]]
bool hasValue() const MIJIN_NOEXCEPT
{
if (exception) {
return true;
}
return !value.empty();
}
void setException(std::exception_ptr exc) MIJIN_NOEXCEPT
{
exception = std::move(exc);
}
void setValue(TValue value_) MIJIN_NOEXCEPT { value = std::move(value_); }
[[nodiscard]] TValue& getValue()
{
if (exception) {
std::rethrow_exception(exception);
}
return value.get();
}
};
template<> template<>
struct FutureStorage<void> struct FutureStorage<void, true>
{ {
bool isSet = false;
std::exception_ptr exception;
[[nodiscard]]
bool hasValue() const MIJIN_NOEXCEPT
{
if (exception) {
return true;
}
return isSet;
}
void setException(std::exception_ptr exc) MIJIN_NOEXCEPT
{
exception = std::move(exc);
}
void setValue() MIJIN_NOEXCEPT
{
isSet = true;
}
void getValue()
{
if (exception) {
std::rethrow_exception(exception);
}
}
}; };
#endif
} // namespace impl } // namespace impl
template<typename TValue, template<typename> typename TAllocator> template<typename TValue, template<typename> typename TAllocator, bool exceptions>
class Future class Future
{ {
private: private:
[[no_unique_address]] impl::FutureStorage<TValue> value_; impl::FutureStorage<TValue, exceptions> value_;
bool isSet_ = false;
public: public:
Future() = default; Future() = default;
Future(const Future&) = delete; Future(const Future&) = delete;
@ -80,10 +151,12 @@ public:
constexpr bool operator!() const MIJIN_NOEXCEPT { return !ready(); } constexpr bool operator!() const MIJIN_NOEXCEPT { return !ready(); }
public: // access public: // access
[[nodiscard]] [[nodiscard]]
constexpr decltype(auto) get() MIJIN_NOEXCEPT constexpr decltype(auto) get() MIJIN_NOEXCEPT_IF(!exceptions)
{ {
MIJIN_ASSERT(isSet_, "Attempting to get from future that is not ready."); MIJIN_ASSERT(ready(), "Attempting to get from future that is not ready.");
if constexpr(std::is_same_v<TValue, void>) { if constexpr(std::is_same_v<TValue, void>)
{
value_.getValue(); // in case of exceptions
return; return;
} }
else { else {
@ -91,10 +164,12 @@ public: // access
} }
} }
[[nodiscard]] [[nodiscard]]
constexpr decltype(auto) get() const MIJIN_NOEXCEPT constexpr decltype(auto) get() const MIJIN_NOEXCEPT_IF(!exceptions)
{ {
MIJIN_ASSERT(isSet_, "Attempting to get from future that is not ready."); MIJIN_ASSERT(ready(), "Attempting to get from future that is not ready.");
if constexpr(std::is_same_v<TValue, void>) { if constexpr(std::is_same_v<TValue, void>)
{
value_.getValue(); // in case of exceptions
return; return;
} }
else { else {
@ -104,22 +179,22 @@ public: // access
[[nodiscard]] [[nodiscard]]
constexpr bool ready() const MIJIN_NOEXCEPT constexpr bool ready() const MIJIN_NOEXCEPT
{ {
return isSet_; return value_.hasValue();
} }
public: // modification public: // modification
template<typename TArg> requires (!std::is_same_v<TValue, void>) template<typename TArg> requires (!std::is_same_v<TValue, void>)
constexpr void set(TArg&& value) MIJIN_NOEXCEPT constexpr void set(TArg&& value) MIJIN_NOEXCEPT
{ {
MIJIN_ASSERT(!isSet_, "Trying to set a future twice!"); MIJIN_ASSERT(!ready(), "Trying to set a future twice!");
value_.setValue(std::move(value)); value_.setValue(std::move(value));
isSet_ = true;
sigSet.emit(); sigSet.emit();
} }
constexpr void set() MIJIN_NOEXCEPT requires (std::is_same_v<TValue, void>) constexpr void set() MIJIN_NOEXCEPT requires (std::is_same_v<TValue, void>)
{ {
MIJIN_ASSERT(!isSet_, "Trying to set a future twice!"); MIJIN_ASSERT(!ready(), "Trying to set a future twice!");
isSet_ = true; if constexpr (std::is_same_v<TValue, void>)
if constexpr (std::is_same_v<TValue, void>) { {
value_.setValue();
sigSet.emit(); sigSet.emit();
} }
else { else {
@ -127,12 +202,38 @@ public: // modification
MIJIN_ERROR("Attempting to call set(void) on future with value."); MIJIN_ERROR("Attempting to call set(void) on future with value.");
} }
} }
constexpr void setException(std::exception_ptr exception) requires (exceptions)
{
MIJIN_ASSERT(!ready(), "Trying to set a future twice!");
if constexpr (exceptions)
{
value_.setException(std::move(exception));
}
}
public: // signals public: // signals
BaseSignal<TAllocator> sigSet; BaseSignal<TAllocator> sigSet;
}; };
template<typename TValue = void, template<typename> typename TAllocator = MIJIN_DEFAULT_ALLOCATOR, bool exceptions = false>
using FuturePtr = std::shared_ptr<Future<TValue, TAllocator, exceptions>>;
template<typename TValue = void, template<typename> typename TAllocator = MIJIN_DEFAULT_ALLOCATOR> template<typename TValue = void, template<typename> typename TAllocator = MIJIN_DEFAULT_ALLOCATOR>
using FuturePtr = std::shared_ptr<Future<TValue, TAllocator>>; using ExceptFuture = Future<TValue, TAllocator, true>;
template<typename TValue = void, template<typename> typename TAllocator = MIJIN_DEFAULT_ALLOCATOR>
using ExceptFuturePtr = std::shared_ptr<Future<TValue, TAllocator, true>>;
template<typename T>
struct is_future : std::false_type {};
template<typename TValue, template<typename> typename TAllocator, bool exceptions>
struct is_future<Future<TValue, TAllocator, exceptions>> : std::true_type {};
template<typename T>
inline constexpr bool is_future_t = is_future<T>::value;
template<typename T>
concept FutureType = is_future<T>::value;
// //
// public functions // public functions