From 7d6fcc60fc867d0f79a3f6f2b3bc1c957d235529 Mon Sep 17 00:00:00 2001
From: Patrick Wuttke
Date: Mon, 22 Sep 2025 21:39:03 +0200
Subject: [PATCH] Added support for forwarding exceptions via Future.
---
source/mijin/async/coroutine.hpp | 80 +++++++++++-----
source/mijin/async/future.hpp | 155 +++++++++++++++++++++++++------
2 files changed, 187 insertions(+), 48 deletions(-)
diff --git a/source/mijin/async/coroutine.hpp b/source/mijin/async/coroutine.hpp
index a846ce3..4302444 100644
--- a/source/mijin/async/coroutine.hpp
+++ b/source/mijin/async/coroutine.hpp
@@ -5,10 +5,6 @@
#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
#include
#include
@@ -27,6 +23,15 @@
#include "../util/misc.hpp"
#include "../util/scope_guard.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
#include "../debug/stacktrace.hpp"
#endif
@@ -186,10 +191,16 @@ struct TaskReturn
};
}
-template
+template typename TAllocator = MIJIN_DEFAULT_ALLOCATOR>
+using TaskFuture = Future;
+
+template typename TAllocator = MIJIN_DEFAULT_ALLOCATOR>
+using TaskFuturePtr = FuturePtr;
+
+template typename TAllocator>
struct TaskAwaitableFuture
{
- FuturePtr future;
+ TaskFuturePtr future;
[[nodiscard]] constexpr bool await_ready() const MIJIN_NOEXCEPT { return future->ready(); }
constexpr void await_suspend(std::coroutine_handle<>) const MIJIN_NOEXCEPT {}
@@ -329,12 +340,12 @@ struct TaskPromise : impl::TaskReturn
- auto await_transform(FuturePtr future, std::source_location sourceLoc = std::source_location::current()) MIJIN_NOEXCEPT
+ template typename TAllocator>
+ auto await_transform(TaskFuturePtr future, std::source_location sourceLoc = std::source_location::current()) MIJIN_NOEXCEPT
{
MIJIN_ASSERT(loop_ != nullptr, "Cannot await future outside of a loop!");
sharedState_->sourceLoc = std::move(sourceLoc);
- TaskAwaitableFuture awaitable{future};
+ TaskAwaitableFuture awaitable{future};
if (!awaitable.await_ready())
{
state_.status = TaskStatus::WAITING;
@@ -635,10 +646,10 @@ public:
void setUncaughtExceptionHandler(exception_handler_t handler) MIJIN_NOEXCEPT { uncaughtExceptionHandler_ = std::move(handler); }
template
- FuturePtr addTaskImpl(TaskBase task, TaskHandle* outHandle) MIJIN_NOEXCEPT;
+ TaskFuturePtr addTaskImpl(TaskBase task, TaskHandle* outHandle) MIJIN_NOEXCEPT;
template
- FuturePtr addTask(TaskBase task, TaskHandle* outHandle = nullptr) MIJIN_NOEXCEPT
+ TaskFuturePtr addTask(TaskBase task, TaskHandle* outHandle = nullptr) MIJIN_NOEXCEPT
{
static_assert(TaskAllocatorTraits::default_is_valid_v, "Allocator is not valid when default constructed, use makeTask() instead.");
return addTaskImpl(std::move(task), outHandle);
@@ -664,7 +675,7 @@ protected:
protected:
static inline TaskLoop*& currentLoopStorage() MIJIN_NOEXCEPT;
template
- static inline void setFutureHelper(StoredTask& storedTask) MIJIN_NOEXCEPT;
+ static inline void setFutureHelper(StoredTask& storedTask) MIJIN_NOEXCEPT_IF(!MIJIN_COROUTINE_ENABLE_EXCEPTIONS);
};
template
@@ -811,12 +822,12 @@ TaskBase::~TaskBase() MIJIN_NOEXCEPT
template typename TAllocator>
template
-FuturePtr TaskLoop::addTaskImpl(TaskBase task, TaskHandle* outHandle) MIJIN_NOEXCEPT
+TaskFuturePtr TaskLoop::addTaskImpl(TaskBase task, TaskHandle* outHandle) MIJIN_NOEXCEPT
{
MIJIN_ASSERT(!task.getLoop(), "Attempting to add task that already has a loop!");
task.setLoop(this);
- FuturePtr future = std::allocate_shared>(TAllocator>(allocator_), allocator_);
+ TaskFuturePtr future = std::allocate_shared>(TAllocator>(allocator_), allocator_);
auto setFuture = &setFutureHelper;
if (outHandle != nullptr)
@@ -855,7 +866,7 @@ TaskStatus TaskLoop::tickTask(StoredTask& task)
while (status == TaskStatus::RUNNING);
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())
{
try
@@ -883,7 +894,22 @@ TaskStatus TaskLoop::tickTask(StoredTask& task)
#endif // MIJIN_COROUTINE_ENABLE_EXCEPTION_HANDLING
if (status == TaskStatus::YIELDED || status == TaskStatus::FINISHED)
{
- task.setFuture(task);
+ try
+ {
+ task.setFuture(task);
+ }
+ catch(TaskCancelled&) {}
+ catch(...)
+ {
+ if (uncaughtExceptionHandler_)
+ {
+ uncaughtExceptionHandler_(std::current_exception());
+ }
+ else
+ {
+ throw;
+ }
+ }
}
return status;
}
@@ -910,10 +936,22 @@ template typename TAllocator>
template typename TAllocator>
template
-/* static */ inline void TaskLoop::setFutureHelper(StoredTask& storedTask) MIJIN_NOEXCEPT
+/* static */ inline void TaskLoop::setFutureHelper(StoredTask& storedTask) MIJIN_NOEXCEPT_IF(!MIJIN_COROUTINE_ENABLE_EXCEPTIONS)
{
TaskBase& task = *static_cast*>(storedTask.task->raw());
- auto future = std::any_cast>(storedTask.resultData);
+ const auto& future = std::any_cast&>(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)
{
@@ -1249,14 +1287,14 @@ inline TaskAwaitableSuspend c_suspend() {
return TaskAwaitableSuspend();
}
-template typename TCollection, typename TType, typename... TTemplateArgs>
-Task<> c_allDone(const TCollection, TTemplateArgs...>& futures)
+template typename TCollection, FutureType TFuture, typename... TTemplateArgs>
+Task<> c_allDone(const TCollection& futures)
{
bool allDone = true;
do
{
allDone = true;
- for (const FuturePtr& future : futures)
+ for (const TFuture& future : futures)
{
if (future && !future->ready()) {
allDone = false;
diff --git a/source/mijin/async/future.hpp b/source/mijin/async/future.hpp
index ac156f9..a267a23 100644
--- a/source/mijin/async/future.hpp
+++ b/source/mijin/async/future.hpp
@@ -4,6 +4,7 @@
#if !defined(MIJIN_ASYNC_FUTURE_HPP_INCLUDED)
#define MIJIN_ASYNC_FUTURE_HPP_INCLUDED 1
+#include
#include
#include
#include
@@ -27,43 +28,113 @@ namespace mijin
//
// public types
//
-template typename TAllocator = MIJIN_DEFAULT_ALLOCATOR>
+template typename TAllocator = MIJIN_DEFAULT_ALLOCATOR, bool exceptions = false>
class Future;
// TODO: add support for mutexes and waiting for futures
namespace impl
{
-template
+template
struct FutureStorage
{
Optional value;
+ [[nodiscard]]
+ bool hasValue() const MIJIN_NOEXCEPT
+ {
+ return !value.empty();
+ }
+
void setValue(TValue value_) MIJIN_NOEXCEPT { value = std::move(value_); }
[[nodiscard]] TValue& getValue() MIJIN_NOEXCEPT { return value.get(); }
};
-// template
-// struct FutureStorage
-// {
-// Optional value;
-//
-// void setValue(TValue& value_) MIJIN_NOEXCEPT { value = &value_; }
-// [[nodiscard]] TValue& getValue() const MIJIN_NOEXCEPT { return *value.get(); }
-// };
+template<>
+struct FutureStorage
+{
+ bool isSet = false;
+
+ [[nodiscard]]
+ bool hasValue() const MIJIN_NOEXCEPT
+ {
+ return isSet;
+ }
+ void setValue() MIJIN_NOEXCEPT
+ {
+ isSet = true;
+ }
+ void getValue() MIJIN_NOEXCEPT {}
+};
+
+#if MIJIN_ENABLE_EXCEPTIONS
+template
+struct FutureStorage
+{
+ Optional 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<>
-struct FutureStorage
+struct FutureStorage
{
+ 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
-template typename TAllocator>
+template typename TAllocator, bool exceptions>
class Future
{
private:
- [[no_unique_address]] impl::FutureStorage value_;
- bool isSet_ = false;
+ impl::FutureStorage value_;
public:
Future() = default;
Future(const Future&) = delete;
@@ -80,10 +151,12 @@ public:
constexpr bool operator!() const MIJIN_NOEXCEPT { return !ready(); }
public: // access
[[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.");
- if constexpr(std::is_same_v) {
+ MIJIN_ASSERT(ready(), "Attempting to get from future that is not ready.");
+ if constexpr(std::is_same_v)
+ {
+ value_.getValue(); // in case of exceptions
return;
}
else {
@@ -91,10 +164,12 @@ public: // access
}
}
[[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.");
- if constexpr(std::is_same_v) {
+ MIJIN_ASSERT(ready(), "Attempting to get from future that is not ready.");
+ if constexpr(std::is_same_v)
+ {
+ value_.getValue(); // in case of exceptions
return;
}
else {
@@ -104,22 +179,22 @@ public: // access
[[nodiscard]]
constexpr bool ready() const MIJIN_NOEXCEPT
{
- return isSet_;
+ return value_.hasValue();
}
public: // modification
template requires (!std::is_same_v)
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));
- isSet_ = true;
sigSet.emit();
}
constexpr void set() MIJIN_NOEXCEPT requires (std::is_same_v)
{
- MIJIN_ASSERT(!isSet_, "Trying to set a future twice!");
- isSet_ = true;
- if constexpr (std::is_same_v) {
+ MIJIN_ASSERT(!ready(), "Trying to set a future twice!");
+ if constexpr (std::is_same_v)
+ {
+ value_.setValue();
sigSet.emit();
}
else {
@@ -127,12 +202,38 @@ public: // modification
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
BaseSignal sigSet;
};
+template typename TAllocator = MIJIN_DEFAULT_ALLOCATOR, bool exceptions = false>
+using FuturePtr = std::shared_ptr>;
+
template typename TAllocator = MIJIN_DEFAULT_ALLOCATOR>
-using FuturePtr = std::shared_ptr>;
+using ExceptFuture = Future;
+
+template typename TAllocator = MIJIN_DEFAULT_ALLOCATOR>
+using ExceptFuturePtr = std::shared_ptr>;
+
+template
+struct is_future : std::false_type {};
+
+template typename TAllocator, bool exceptions>
+struct is_future> : std::true_type {};
+
+template
+inline constexpr bool is_future_t = is_future::value;
+
+template
+concept FutureType = is_future::value;
//
// public functions