diff --git a/Source/UE5Coro/Private/Promise.cpp b/Source/UE5Coro/Private/Promise.cpp index 2722e0c..9482515 100644 --- a/Source/UE5Coro/Private/Promise.cpp +++ b/Source/UE5Coro/Private/Promise.cpp @@ -80,8 +80,6 @@ FPromise::~FPromise() void FPromise::Resume() { #if UE5CORO_DEBUG - checkf(ResumeStack.Num() == 0 || ResumeStack.Last() != this, - TEXT("Internal error")); checkf(!Extras->IsComplete(), TEXT("Attempting to resume completed coroutine")); ResumeStack.Push(this); diff --git a/Source/UE5Coro/Public/UE5Coro/AsyncAwaiters.h b/Source/UE5Coro/Public/UE5Coro/AsyncAwaiters.h index 7a03055..dbbf202 100644 --- a/Source/UE5Coro/Public/UE5Coro/AsyncAwaiters.h +++ b/Source/UE5Coro/Public/UE5Coro/AsyncAwaiters.h @@ -104,22 +104,26 @@ class [[nodiscard]] TFutureAwaiter final : public TAwaiter> bool await_ready() { - checkf(this->Future.IsValid(), + checkf(!Result, TEXT("Attempting to reuse spent TFutureAwaiter")); + checkf(Future.IsValid(), TEXT("Awaiting invalid/spent future will never resume")); return Future.IsReady(); } void Suspend(FPromise& Promise) { - checkf(!Result, TEXT("Attempting to reuse spent TFutureAwaiter")); + // Extremely rarely, Then will run synchronously because Future + // finished after IsReady but before Suspend. + // This is OK and will result in the caller coroutine resuming itself. Future.Then([this, &Promise](auto InFuture) { + checkf(!Future.IsValid(), TEXT("Internal error")); + // TFuture will pass T* for Value, TFuture an int if constexpr (std::is_lvalue_reference_v) { static_assert(std::is_pointer_v); - checkf(!Future.IsValid(), TEXT("Internal error")); Result = InFuture.Get(); Promise.Resume(); } @@ -127,7 +131,6 @@ class [[nodiscard]] TFutureAwaiter final : public TAwaiter> { // It's normally dangerous to expose a pointer to a local, but auto Value = InFuture.Get(); // This will be alive while... - checkf(!Future.IsValid(), TEXT("Internal error")); Result = &Value; Promise.Resume(); // ...await_resume moves from it here } @@ -136,10 +139,24 @@ class [[nodiscard]] TFutureAwaiter final : public TAwaiter> T await_resume() { - if constexpr (std::is_lvalue_reference_v) - return *Result; - else if constexpr (!std::is_void_v) - return std::move(*Result); + if (!Result) + { + // Result being nullptr indicates that await_ready returned true, + // Then has not and will not run, and Future is still valid + checkf(Future.IsValid(), TEXT("Internal error")); + static_assert(std::is_same_v); + Result = reinterpret_cast(-1); // Mark as spent + return Future.Get(); + } + else + { + // Otherwise, we're being called from Then, and Future is spent + checkf(!Future.IsValid(), TEXT("Internal error")); + if constexpr (std::is_lvalue_reference_v) + return *Result; + else if constexpr (!std::is_void_v) + return std::move(*Result); // This will move from Then's local + } } }; diff --git a/Source/UE5CoroTests/Private/FutureTest.cpp b/Source/UE5CoroTests/Private/FutureTest.cpp index 582f541..68798f4 100644 --- a/Source/UE5CoroTests/Private/FutureTest.cpp +++ b/Source/UE5CoroTests/Private/FutureTest.cpp @@ -64,6 +64,23 @@ void DoTest(FAutomationTestBase& Test) { FTestWorld World; + { + TPromise Promise; + Promise.SetValue(1); + auto Coro = World.Run(CORO_R(int) + { + co_return co_await Promise.GetFuture(); + }); + + IF_CORO_LATENT + { + Test.TestFalse(TEXT("Not polled yet"), Coro.IsDone()); + World.Tick(); + } + Test.TestTrue(TEXT("Already done"), Coro.IsDone()); + Test.TestEqual(TEXT("Value"), Coro.GetResult(), 1); + } + { int State = 0; TPromise Promise; diff --git a/UE5Coro.uplugin b/UE5Coro.uplugin index 361c130..26e2d38 100644 --- a/UE5Coro.uplugin +++ b/UE5Coro.uplugin @@ -1,7 +1,7 @@ { "FileVersion": 3, "Version": 1, - "VersionName": "1.6.2", + "VersionName": "1.7", "FriendlyName": "UE5Coro", "Description": "C++17/20 coroutine implementation for Unreal Engine", "Category": "Programming",