1065 lines
39 KiB
C++

#pragma once
/// @defgroup Tasks Tasks
/// @brief Coroutine-based task handles for controlling execution and lifetime management
/// @copydoc Task
/// @defgroup Time Time Streams
/// @brief Time-stream functionality for time-sensitive awaiters
/// @defgroup Awaiters Awaiters
/// @brief Versatile task awaiters that offer utility to most projects
#include <functional>
#include <future>
#include <memory>
#include <string>
//--- User configuration header ---//
#include "TasksConfig.h"
//--- Debug Macros ---//
#if SQUID_ENABLE_TASK_DEBUG
/// @ingroup Tasks
/// @brief Macro that instruments a task with a debug name string. Usually at the top of every task coroutine as @c TASK_NAME(__FUNCTION__)
#define TASK_NAME(...) co_await SetDebugName(__VA_ARGS__);
#define DEBUG_STR , std::string in_debugStr
#define PASS_DEBUG_STR , in_debugStr
#define MANUAL_DEBUG_STR(debugStr) , debugStr
#define WaitUntilImpl(...) _WaitUntil(__VA_ARGS__, #__VA_ARGS__)
#define WaitWhileImpl(...) _WaitWhile(__VA_ARGS__, #__VA_ARGS__)
#ifndef IN_DOXYGEN
#define WaitUntil(...) WaitUntilImpl(__VA_ARGS__)
#define WaitWhile(...) WaitWhileImpl(__VA_ARGS__)
#endif //IN_DOXYGEN
#else
#define TASK_NAME(...)
#define DEBUG_STR
#define PASS_DEBUG_STR
#define MANUAL_DEBUG_STR(...)
#ifndef IN_DOXYGEN
#define WaitUntil(...) _WaitUntil(__VA_ARGS__)
#define WaitWhile(...) _WaitWhile(__VA_ARGS__)
#endif //IN_DOXYGEN
#endif //SQUID_ENABLE_TASK_DEBUG
NAMESPACE_SQUID_BEGIN
/// @addtogroup Tasks
/// @{
//--- Task Reference Type ---//
enum class eTaskRef /// Whether a handle references a task using a strong or weak reference
{
Strong, ///< Handle will keep the task alive (so long as there exists a valid Resumable handle)
Weak, ///< Handle will not the task alive
};
//--- Task Resumable Type ---//
enum class eTaskResumable /// Whether a handle can be resumed (all live tasks have exactly one resumable handle and 0+ non-resumable handles)
{
Yes, ///< Handle is resumable
No, ///< Handle is not resumable
};
//--- Task Status ---//
enum class eTaskStatus /// Status of a task (whether it is currently suspended or done)
{
Suspended, ///< Task is currently suspended
Done, ///< Task has terminated and coroutine frame has been destroyed
};
//--- tTaskCancelFn ---//
using tTaskCancelFn = std::function<bool()>; ///< CancelIf/StopIf condition function type
// Forward declarations
template <typename tRet, eTaskRef RefType, eTaskResumable Resumable>
class Task; /// Templated handle type (defaults to <void, Strong, Resumable>)
template <typename tRet = void>
using TaskHandle = Task<tRet, eTaskRef::Strong, eTaskResumable::No>; ///< Non-resumable handle that holds a strong reference to a task
using WeakTask = Task<void, eTaskRef::Weak, eTaskResumable::Yes>; ///< Resumable handle that holds a weak reference to a task (always void return type)
using WeakTaskHandle = Task<void, eTaskRef::Weak, eTaskResumable::No>; ///< Non-resumable handle that holds a weak reference to a task (always void return type)
/// @} end of addtogroup Tasks
/// @addtogroup Awaiters
/// @{
//--- Suspend Awaiter ---//
/// Awaiter class that suspends unconditionally
struct Suspend : public std::suspend_always
{
};
//--- Stop Context ---//
/// Context for a task's stop requests (undefined behavior if used after the underlying task is destroyed)
struct StopContext
{
bool IsStopRequested() const
{
return *m_isStoppedPtr;
}
protected:
friend class TaskInternalBase;
StopContext(const bool* in_isStoppedPtr)
: m_isStoppedPtr(in_isStoppedPtr)
{
}
private:
const bool* m_isStoppedPtr = nullptr;
};
//--- GetStopContext Awaiter ---//
/// Awaiter class that immediately (without suspending) yields a stop context
struct GetStopContext
{
};
/// @} end of addtogroup Awaiters
//--- Internal Implementation Header ---//
#include "Private/TaskPrivate.h" // Internal use only! Do not move or include elsewhere!
/// @addtogroup Tasks
/// @{
//--- Task ---//
/// Task is a high-level task handle used to manage the lifetime and execution of an underlying coroutine
/// @details
///
/// Handle Types
/// ------------
/// The Task class is actually a template class that implements 4 user-level handle types:
///
/// - \ref Task<tRet>
/// - A resumable task handle that holds a strong reference to the underlying coroutine.
/// - Should be the return type of every coroutine you write.
/// - Can be used to resume tasks, kill tasks, check if they are done, and access a return value.
/// - Is permitted to have void return type (e.g. Task<>), which disables return value access.
/// - \ref WeakTask
/// - A resumable task handle that holds a weak reference to the underlying coroutine.
/// - Can only be used to resume tasks, kill tasks, and check if they are done.
/// - \ref TaskHandle<tRet>
/// - A non-resumable task handle that holds a strong reference to the underlying coroutine.
/// - Can be used to kill tasks, check if they are done, and access a return value.
/// - Is permitted to have void return type (e.g. TaskHandle<>), which disables return value access.
/// - \ref WeakTaskHandle
/// - A non-resumable task handle that holds a weak reference to the underlying coroutine.
/// - Can only be used to kill tasks and check if they are done.
///
/// Handle Type | Return Type | Resumable? | Ref Strength
/// ------------------- | ----------- | ---------- | ------------------
/// \ref Task | <any type> | Yes | Strong
/// \ref WeakTask | void | Yes | Weak
/// \ref TaskHandle | <any type> | No | Strong
/// \ref WeakTaskHandle | void | No | Weak
///
/// Conversion Rules
/// ----------------
///
/// It is possible to convert between these 4 types, but not all conversions are permitted. The rules for conversion are:
/// - A conversion may remove resumability (e.g. \ref Task<tRet> -> \ref TaskHandle<tRet>), but cannot restore it
/// - A conversion may remove return type (e.g. \ref Task<tRet> -> \ref Task<>), but cannot restore it
/// - A conversion may remove reference strength (e.g. \ref Task<tRet> -> \ref WeakTask), but cannot restore it
///
/// In simpler terms, this means: __a handle can always convert to a handle type with fewer capabilities, but not vice-versa__.
///
/// Generally-speaking, it would be unsafe to convert in such a way that would add handle properties, hence the motivation for these
/// conversion rules. Care has been taken, however, to provide clear human-readable compile-time error messages if and when an
/// invalid conversion is attempted in code.
///
/// Resumability
/// ------------
///
/// For a given coroutine instance, it is impossible to have more than a single resumable handle that references it at runtime.
/// We refer to this as the "single-resumer rule". Because both Task and WeakTask are move-only types that cannot be copy-constructed
/// or copy-assigned from other handles, this guarantees at compile-time that there will never be two handles that are able to resume
/// the same underlying coroutine. This compile-time guarantee was implemented after many insidious bugs emerged in gameplay code
/// written using early versions of the Squid::Tasks library.
///
/// When a task's single-resumer handle is destroyed, the task is immediately killed. If a coroutine were able to remain suspended
/// without the possibility of ever being resumed again, then any task waiting for it to terminate would deadlock. For this reason,
/// Squid::Tasks enforces that all coroutines must have a valid resumable handle at all times, otherwise they are immediately killed.
///
/// (If you are unfamiliar with what it meant by "move-only type", we recommend you research "C++ move semantics" to familiarize
/// yourself.)
///
/// Lifetime Management
/// -------------------
///
/// The default lifetime of a Task's underlying coroutine is determined by the handles the refer to it:
/// - The underlying coroutine will be killed immediately if it is no longer referenced by a resumable handle ( \ref Task/ \ref WeakTask).
/// - The underlying coroutine will be killed immediately if it ever has zero strong references remaining to it ( \ref Task/ \ref TaskHandle).
///
/// This lifetime management model is essentially the same as a strong-pointer/weak-pointer model, with the added constraint that
/// tasks are killed as soon as they can no longer logically be resumed.
///
/// @tparam tRet Return type of the underlying coroutine (can be void if the coroutine does not co_return a value)
/// @tparam RefType Whether this handle holds a strong or weak reference to the underlying coroutine
/// @tparam Resumable Whether this handle can be used to resume the underlying coroutine
template <typename tRet = void, eTaskRef RefType = eTaskRef::Strong, eTaskResumable Resumable = eTaskResumable::Yes>
class Task
{
public:
/// @cond
using tTaskInternal = TaskInternal<tRet>;
using promise_type = TaskPromise<tRet>;
/// @endcond
#define NONVOID_ONLY template <typename U = tRet, typename std::enable_if_t<!std::is_void<U>::value>* = nullptr>
// Prohibit illegal task types
static_assert(RefType == eTaskRef::Strong || std::is_void<tRet>::value, "Illegal task type (cannot combine weak reference type with non-void return type");
Task() /// Default constructor (constructs an invalid handle)
{
}
Task(nullptr_t) /// Null-pointer constructor (constructs an invalid handle)
{
}
Task(std::shared_ptr<tTaskInternal> in_taskInternal) /// @private
: m_taskInternal(in_taskInternal)
{
AddRef();
}
Task(std::coroutine_handle<promise_type> in_coroHandle) /// @private
: m_taskInternal(std::make_shared<tTaskInternal>(in_coroHandle))
{
AddRef();
}
Task(const Task& in_otherTask) /// Copy constructor (TaskHandle/WeakTaskHandle only)
: Task(in_otherTask.GetInternalTask())
{
static_assert(IsCopyable(), "Cannot copy-construct Task/WeakTask (only TaskHandle/WeakTaskHandle)");
AddRef();
}
Task(Task&& in_otherTask) noexcept /// Move constructor
: m_taskInternal(std::move(in_otherTask.m_taskInternal))
{
// NOTE: No need to alter logical reference here (this is a move)
}
Task& operator=(nullptr_t) noexcept /// Null-pointer assignment operator (makes the handle invalid)
{
RemoveRef(); // Remove logical reference from old internal task
m_taskInternal = nullptr;
return *this;
}
Task& operator=(const Task& in_otherTask) /// Copy assignment operator (TaskHandle/WeakTaskHandle only)
{
static_assert(IsCopyable(), "Cannot copy-assign Task/WeakTask (only TaskHandle/WeakTaskHandle)");
RemoveRef(); // Remove logical reference from our current internal task
m_taskInternal = in_otherTask.m_taskInternal;
AddRef();
return *this;
}
Task& operator=(Task&& in_otherTask) noexcept /// Move assignment operator
{
// If the internal task that we're about to move over can never be resumed again, kill it immediately
KillIfResumable();
RemoveRef(); // Remove logical reference from old internal task
// NOTE: No need to add logical reference here (this is a move)
m_taskInternal = std::move(in_otherTask.m_taskInternal);
return *this;
}
~Task() /// Destructor
{
RemoveRef(); // Remove logical reference task
// If the internal task can never be resumed again, kill it immediately
KillIfResumable();
}
bool IsValid() const /// Returns whether the underlying coroutine is valid
{
return m_taskInternal.get();
}
operator bool() const /// Conversion-to-bool that yields whether an underlying coroutine is set for the task
{
return IsValid();
}
bool IsDone() const /// Returns whether the task has terminated
{
return IsValid() ? m_taskInternal->IsDone() : true;
}
bool IsStopRequested() const /// Returns whether a stop request has been issued for the task
{
return IsValid() ? m_taskInternal->IsStopRequested() : true;
}
void RequestStop() /// Issues a request for the task to terminate gracefully as soon as possible
{
if(IsValid())
{
m_taskInternal->RequestStop(); // Tell sub-tasks to stop, as well
}
}
void Kill() /// Immediately terminates the task
{
// NOTE: Killing a task immediately destroys the coroutine and all of the coroutine's local variables
if(IsValid())
{
m_taskInternal->Kill();
}
}
NONVOID_ONLY std::optional<tRet> TakeReturnValue() /// Attempts to take the task's return value (throws error if return value is either orphaned or was already taken)
{
SQUID_RUNTIME_CHECK(IsValid(), "Tried to retrieve return value from an invalid handle");
return GetInternalTask()->TakeReturnValue();
}
eTaskStatus Resume() /// Resumes the task (Task/WeakTask only)
{
static_assert(IsResumable(), "Cannot call Resume() on a TaskHandle/WeakTaskHandle");
return IsValid() ? m_taskInternal->Resume() : eTaskStatus::Done;
}
#if SQUID_ENABLE_TASK_DEBUG
std::string GetDebugName(std::optional<TaskDebugStackFormatter> in_formatter = {}) const /// Gets this task's debug name (use TASK_NAME to set the debug name)
{
const char* defaultRetVal = Resumable == eTaskResumable::Yes ? "[empty task]" : "[empty task handle]";
auto debugName = IsValid() ? m_taskInternal->GetDebugName() : defaultRetVal;
return in_formatter ? in_formatter.value().Format(debugName) : debugName;
}
std::string GetDebugStack(std::optional<TaskDebugStackFormatter> in_formatter = {}) const /// Gets this task's debug stack (use TASK_NAME to set a task's debug name)
{
if(IsValid())
{
return in_formatter ? in_formatter.value().Format(m_taskInternal->GetDebugStack()) : m_taskInternal->GetDebugStack();
}
return GetDebugName(in_formatter);
}
#else
std::string GetDebugName(std::optional<TaskDebugStackFormatter> in_formatter = {}) const /// @private
{
return ""; // Returns an empty string when task debug is disabled
}
std::string GetDebugStack(std::optional<TaskDebugStackFormatter> in_formatter = {}) const /// @private
{
return ""; // Returns an empty string when task debug is disabled
}
#endif //SQUID_ENABLE_TASK_DEBUG
#if SQUID_USE_EXCEPTIONS
std::exception_ptr GetUnhandledException() const /// Gets any unhandled exceptions thrown by the task
{
SQUID_RUNTIME_CHECK(IsValid(), "Tried to retrieve unhandled exception from an invalid handle");
return m_taskInternal->GetUnhandledException();
}
void RethrowUnhandledException() const /// Rethrows any unhandled exceptions thrown by the task
{
if(auto e = m_taskInternal->GetUnhandledException())
{
std::rethrow_exception(e);
}
}
#else
void RethrowUnhandledException() const /// @private
{
}
#endif //SQUID_USE_EXCEPTIONS
// Task conversion methods
template <typename tOtherRet>
operator Task<tOtherRet>() const & /// @private
{
constexpr bool isLegalReturnTypeConversion = std::is_void<tOtherRet>::value || std::is_same<tRet, tOtherRet>::value;
constexpr bool isLegalTypeConversion = IsStrong() && IsResumable();
static_assert(isLegalTypeConversion, "Cannot promote WeakTask/TaskHandle/WeakTaskHandle to Task");
static_assert(!isLegalTypeConversion || isLegalReturnTypeConversion, "Mismatched return type (invalid return type conversion)");
static_assert(!isLegalTypeConversion || !isLegalReturnTypeConversion, "Cannot copy Task -> Task because it is non-copyable (try std::move(task))");
return {};
}
template <typename tOtherRet>
operator Task<tOtherRet>() && /// @private
{
constexpr bool isLegalReturnTypeConversion = std::is_void<tOtherRet>::value || std::is_same<tRet, tOtherRet>::value;
constexpr bool isLegalTypeConversion = IsStrong() && IsResumable();
static_assert(isLegalTypeConversion, "Cannot promote WeakTask/TaskHandle/WeakTaskHandle to Task");
static_assert(!isLegalTypeConversion || isLegalReturnTypeConversion, "Cannot convert tasks to non-void return type (invalid return type conversion)");
// Move-to-void conversion (applies to all types)
return MoveToTask<tOtherRet, RefType, Resumable>();
}
operator WeakTask() const & /// @private Copy-convert to WeakTask (always illegal)
{
static_assert(IsResumable(), "Cannot convert TaskHandle -> WeakTask (invalid resumability conversion");
static_assert(!IsResumable(), "Cannot copy Task -> WeakTask because it is non-copyable (try std::move(task))");
return {};
}
operator WeakTask() && /// @private Move-convert to WeakTask (sometimes legal)
{
static_assert(IsResumable(), "Cannot convert TaskHandle -> WeakTask (invalid resumability conversion)");
return MoveToTask<void, eTaskRef::Weak, eTaskResumable::Yes>();
}
operator TaskHandle<tRet>() const /// @private
{
static_assert(IsStrong(), "Cannot convert WeakTask/WeakTaskHandle -> TaskHandle (invalid reference-strength conversion)");
return CopyToTask<tRet, eTaskRef::Strong, eTaskResumable::No>();
}
template <typename tOtherRet>
operator TaskHandle<tOtherRet>() const /// @private
{
constexpr bool isLegalReturnTypeConversion = std::is_void<tOtherRet>::value || std::is_same<tRet, tOtherRet>::value;
static_assert(IsStrong(), "Cannot convert WeakTask/WeakTaskHandle -> TaskHandle (invalid reference-strength conversion)");
static_assert(!IsStrong() || isLegalReturnTypeConversion, "Mismatched return type (invalid return type conversion)");
return CopyToTask<tOtherRet, eTaskRef::Strong, eTaskResumable::No>();
}
operator WeakTaskHandle() const /// @private
{
// Convert anything to a weak task handle
return CopyToTask<void, eTaskRef::Weak, eTaskResumable::No>();
}
// Cancel-If Methods
/// Returns wrapper task that kills this task when the given function returns true. Returns whether wrapped task was canceled.
/// Task return value will be bool if wrapped task had void return type, otherwise std::optional<tRet>.
auto CancelIf(tTaskCancelFn in_cancelFn) &&
{
return CancelTaskIf(std::move(*this), in_cancelFn);
}
/// Returns wrapper task that kills this task when a stop request is issued on it. Returns whether wrapped task was canceled.
/// Task return value will be bool if wrapped task had void return type, otherwise std::optional<tRet>.
auto CancelIfStopRequested() && ///
{
return std::move(*this).CancelIf([this] { return IsStopRequested(); });
}
auto CancelIf(tTaskCancelFn in_cancelFn) & /// @private Illegal lvalue implementation
{
static_assert(static_false<tRet>::value, "Cannot call CancelIf() on an lvalue (try std::move(task).CancelIf())");
return CancelTaskIf(std::move(*this), in_cancelFn);
}
auto CancelIfStopRequested() & /// @private Illegal lvalue implementation
{
static_assert(static_false<tRet>::value, "Cannot call CancelIfStopRequested() on an lvalue (try std::move(task).CancelIfStopRequested())");
return std::move(*this).CancelIf([this] { return IsStopRequested(); });
}
// Stop-If Methods
/// @brief Returns wrapper task that requests a stop on this task when the given function returns true, then waits for the task to terminate (without timeout).
/// @details Task returns whether wrapped task was canceled. Task return value will be bool if wrapped task had void return type, otherwise std::optional<tRet>.
auto StopIf(tTaskCancelFn in_cancelFn) && /// Returns wrapper task that requests a stop on this task when the given function returns true
{
return StopTaskIf(std::move(*this), in_cancelFn);
}
auto StopIf(tTaskCancelFn in_cancelFn) & /// @private Illegal lvalue implementation
{
static_assert(static_false<tRet>::value, "Cannot call StopIf() on an lvalue (try std::move(task).StopIf())");
return StopTaskIf(std::move(*this), in_cancelFn);
}
#if SQUID_ENABLE_GLOBAL_TIME
/// @brief Returns wrapper task that requests a stop on this task when the given function returns true, then waits for the task to terminate (with timeout in the global time-stream).
/// @details Task returns whether wrapped task was canceled. Task return value will be bool if wrapped task had void return type, otherwise std::optional<tRet>.
auto StopIf(tTaskCancelFn in_cancelFn, tTaskTime in_timeout) &&
{
// Cannot be called unless SQUID_ENABLE_GLOBAL_TIME has been set in TasksConfig.h.
return StopTaskIf(std::move(*this), in_cancelFn, in_timeout);
}
auto StopIf(tTaskCancelFn in_cancelFn, tTaskTime in_timeout) & /// @private Illegal lvalue implementation
{
static_assert(static_false<tRet>::value, "Cannot call StopIf() on an lvalue (try std::move(task).StopIf())");
return StopTaskIf(std::move(*this), in_cancelFn, in_timeout);
}
#else
auto StopIf(tTaskCancelFn in_cancelFn, tTaskTime in_timeout) && /// @private Illegal global-time implementation
{
static_assert(static_false<tRet>::value, "Global task time not enabled (see SQUID_ENABLE_GLOBAL_TIME in TasksConfig.h)");
return StopTaskIf(std::move(*this), in_cancelFn);
}
auto StopIf(tTaskCancelFn in_cancelFn, tTaskTime in_timeout) & /// @private Illegal lvalue implementation
{
static_assert(static_false<tRet>::value, "Global task time not enabled (see TasksConfig.h)");
return StopTaskIf(std::move(*this), in_cancelFn);
}
#endif //SQUID_ENABLE_GLOBAL_TIME
/// @brief Returns wrapper task that requests a stop on this task when the given function returns true, then waits for the task to terminate (with timeout in a given time-stream).
/// @details Task returns whether wrapped task was canceled. Task return value will be bool if wrapped task had void return type, otherwise std::optional<tRet>.
template <typename tTimeFn>
auto StopIf(tTaskCancelFn in_cancelFn, tTaskTime in_timeout, tTimeFn in_timeFn) &&
{
return StopTaskIf(std::move(*this), in_cancelFn, in_timeout, in_timeFn);
}
template <typename tTimeFn>
auto StopIf(tTaskCancelFn in_cancelFn, tTaskTime in_timeout, tTimeFn in_timeFn) & /// @private Illegal lvalue implementation
{
static_assert(static_false<tRet>::value, "Cannot call StopIf() on an lvalue (try std::move(task).StopIf())");
return StopTaskIf(std::move(*this), in_cancelFn, in_timeout, in_timeFn);
}
private:
/// @cond
template <typename, eTaskRef, eTaskResumable, typename> friend struct TaskAwaiterBase;
template <typename, eTaskRef, eTaskResumable> friend class Task;
friend class TaskInternalBase;
/// @endcond
// Task Internal Storage
std::shared_ptr<TaskInternalBase> m_taskInternal;
// Casts the internal task storage pointer to a concrete (non-TaskInternalBase) pointer
std::shared_ptr<tTaskInternal> GetInternalTask() const
{
// We can safely downcast from TaskInternalBase to TaskInternal<void>
return std::static_pointer_cast<tTaskInternal>(m_taskInternal);
}
// Copy/Move Implementations
template <typename tNewRet, eTaskRef NewRefType, eTaskResumable NewResumable>
Task<tNewRet, NewRefType, NewResumable> CopyToTask() const
{
Task<tNewRet, NewRefType, NewResumable> ret;
ret.m_taskInternal = m_taskInternal;
ret.AddRef();
return ret;
}
template <typename tNewRet, eTaskRef NewRefType, eTaskResumable NewResumable>
Task<tNewRet, NewRefType, NewResumable> MoveToTask()
{
Task<tNewRet, NewRefType, NewResumable> ret;
ret.m_taskInternal = m_taskInternal;
ret.AddRef();
RemoveRef();
m_taskInternal = nullptr;
return ret;
}
// Logical reference management
void AddRef()
{
if(m_taskInternal)
{
if(RefType == eTaskRef::Strong)
{
m_taskInternal->AddLogicalRef();
}
}
}
void RemoveRef()
{
if(m_taskInternal)
{
if(RefType == eTaskRef::Strong)
{
m_taskInternal->RemoveLogicalRef();
}
}
}
void KillIfResumable()
{
if (IsResumable() && IsValid())
{
Kill();
}
}
// Constexpr Helpers
static constexpr bool IsResumable()
{
return Resumable == eTaskResumable::Yes;
}
static constexpr bool IsStrong()
{
return RefType == eTaskRef::Strong;
}
static constexpr bool IsCopyable()
{
return !IsResumable();
}
#undef NONVOID_ONLY
};
/// @} end of addtogroup Tasks
/// @addtogroup Time
/// @details
///
/// Time-Streams
/// ------------
///
/// Every game project has its own method of updating and measuring game time. Most games feature multiple different "time-streams", such
/// as "game time", "real time", "editor time", "paused time", "audio time", etc... Because of this, the Squid::Tasks library requires each
/// time-sensitive awaiter (e.g. ```WaitSeconds()```, ```Timeout()```, etc) to be presented with a time-stream function that returns the current
/// time in the desired time-stream. By convention, these time-streams are passed as functions into the final argument of time-sensitive
/// awaiters.
///
/// Enabling Global Time Support
/// ----------------------------
///
/// For less-complex projects it can be desirable to default to a "global time-stream" that removes the requirement to explicitly pass a
/// time-stream function into time-sensitive awaiters. To enable this functionality, the user must set ```SQUID_ENABLE_GLOBAL_TIME``` in
/// TasksConfig.h and implement a special function called Squid::GetTime(). Failure to define this function will result in a linker error.
///
/// The Squid::GetTime() function should return a floating-point value representing the number of seconds since the program started running.
/// Here is an example Squid::GetTime() function implementation from within the ```main.cpp``` file of a sample project:
///
/// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~.cpp
/// NAMESPACE_SQUID_BEGIN
/// tTaskTime GetTime()
/// {
/// return (tTaskTime)TimeSystem::GetTime();
/// }
/// NAMESPACE_SQUID_END
/// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
///
/// It is recommended to save off the current time value at the start of each game frame, returning that saved value from within ```Squid::GetTime()```.
/// The reason for this is that, within a single frame, you likely want all of the tasks to behave as if they are updating at the same time.
/// By providing the same exact time value to all Tasks that are resumed within a given update, the software is more likely to behave in a stable
/// and predictable manner.
/// @{
/// Helper function to elapsed time in a given time-stream
template <typename tTimeFn>
tTaskTime GetTimeSince(tTaskTime in_t, tTimeFn in_timeFn)
{
return in_timeFn() - in_t;
}
#if SQUID_ENABLE_GLOBAL_TIME
/// @brief User-defined global time-stream function (must be implemented if SQUID_ENABLE_GLOBAL_TIME is set, otherwise
/// there will be a linker error)
tTaskTime GetGlobalTime();
/// Global time-stream function used internally by Squid::Tasks (requires SQUID_ENABLE_GLOBAL_TIME)
inline auto GlobalTime()
{
return &GetGlobalTime;
}
/// Helper function to elapsed time in the global time-stream (requires SQUID_ENABLE_GLOBAL_TIME)
inline tTaskTime GetTimeSince(tTaskTime in_t)
{
return GetTimeSince(in_t, GlobalTime());
}
#else
template <typename T = void>
tTaskTime GetTimeSince(tTaskTime in_t)
{
static_assert(static_false<T>::value, "Global task time not enabled (see SQUID_ENABLE_GLOBAL_TIME in TasksConfig.h)");
return tTaskTime(0);
}
#endif //SQUID_ENABLE_GLOBAL_TIME
/// @} end of addtogroup Time
/// @addtogroup Awaiters
/// @{
//--- All/Any/Select Tasks ---//
/// @private
struct TaskWrapper
{
public:
TaskWrapper(Task<> in_task) : task(std::move(in_task)) {}
~TaskWrapper() {}
Task<> task;
template <typename tRet>
static std::shared_ptr<TaskWrapper> Wrap(Task<tRet> in_task)
{
return std::make_shared<TaskWrapper>(std::move(in_task));
}
template <typename tReadyFn>
static std::shared_ptr<TaskWrapper> Wrap(tReadyFn in_readyFn)
{
auto task = [](tReadyFn in_readyFn) -> Task<> { co_await in_readyFn; }(in_readyFn);
return std::make_shared<TaskWrapper>(std::move(task));
}
};
/// @private
struct TaskSingleEntry
{
template <typename tRet>
TaskSingleEntry(Task<tRet> in_task)
: taskWrapper(TaskWrapper::Wrap(std::move(in_task)))
{
}
template <typename tReadyFn>
TaskSingleEntry(tReadyFn in_readyFn)
: taskWrapper(TaskWrapper::Wrap(in_readyFn))
{
}
auto Resume()
{
return taskWrapper->task.Resume();
}
std::shared_ptr<TaskWrapper> taskWrapper;
};
/// @private
template <typename tValue>
struct TaskSelectEntry
{
template <typename tRet>
TaskSelectEntry(tValue in_value, Task<tRet> in_task)
: value(in_value)
, taskWrapper(TaskWrapper::Wrap(std::move(in_task)))
{
}
template <typename tReadyFn>
TaskSelectEntry(tValue in_value, tReadyFn in_readyFn)
: value(in_value)
, taskWrapper(TaskWrapper::Wrap(in_readyFn))
{
}
auto Resume()
{
return taskWrapper->task.Resume();
}
auto GetValue()
{
return value;
}
tValue value;
std::shared_ptr<TaskWrapper> taskWrapper;
};
/// @cond
#define TASK_NAME_ENTRIES(name, entries) \
TASK_NAME(name, [entries]() { \
std::string debugStr; \
for(auto entry : entries) \
{ \
debugStr += debugStr.size() ? "\n" : "\n`"; \
debugStr += entry.taskWrapper->task.GetDebugStack(); \
} \
debugStr += "`\n"; \
return debugStr; \
});
#define TASK_NAME_ENTRIES_ALL(name, entries) \
TASK_NAME(name, [entries]() { \
std::string debugStr; \
for(auto entry : entries) \
{ \
debugStr += debugStr.size() ? "\n" : "\n`"; \
debugStr += entry.taskWrapper->task.GetDebugStack() + (entry.taskWrapper->task.IsDone() ? " [DONE]" : " [RUNNING]"); \
} \
debugStr += "`\n"; \
return debugStr; \
});
/// @endcond
/// Awaiter task that manages a set of other awaiters and waits until at least one of them is done
inline Task<> WaitForAny(std::vector<TaskSingleEntry> in_entries)
{
TASK_NAME_ENTRIES(__FUNCTION__, in_entries);
for(auto& entry : in_entries)
{
co_await AddStopTask(entry.taskWrapper->task); // Setup stop-request propagation
}
while(true)
{
for(auto& entry : in_entries)
{
if(entry.Resume() == eTaskStatus::Done)
{
co_return;
}
}
co_await Suspend();
}
}
/// Awaiter task that manages a set of other awaiters and waits until all of them are done
COROUTINE_OPTIMIZE_OFF // NOTE: There is a compiler optimization bug in versions of Clang used on some platforms that cause it to crash when compiling this function
inline Task<> WaitForAll(std::vector<TaskSingleEntry> in_entries)
{
TASK_NAME_ENTRIES_ALL(__FUNCTION__, in_entries);
for(auto& entry : in_entries)
{
co_await AddStopTask(entry.taskWrapper->task); // Setup stop-request propagation
}
while(true)
{
bool allDone = true;
for(auto& entry : in_entries)
{
if(entry.Resume() != eTaskStatus::Done)
{
allDone = false;
}
}
if(allDone)
{
co_return; // Done!
}
co_await Suspend();
}
}
COROUTINE_OPTIMIZE_ON
/// Awaiter task that behaves like WaitForAny(), but returns a value associated with whichever awaiter finishes first
template<class tValue>
Task<tValue> Select(std::vector<TaskSelectEntry<tValue>> in_entries)
{
TASK_NAME_ENTRIES(__FUNCTION__, in_entries);
for(auto& entry : in_entries)
{
co_await AddStopTask(entry.taskWrapper->task); // Setup stop-request propagation
}
while(true)
{
for(size_t i = 0; i < in_entries.size(); ++i)
{
if(in_entries[i].Resume() == eTaskStatus::Done)
{
co_return in_entries[i].GetValue();
}
}
co_await Suspend();
}
co_return tValue{};
}
#ifdef IN_DOXYGEN
/// Awaiter function that waits until a given functor returns true
inline Task<> WaitUntil(tTaskReadyFn in_readyFn) {}
/// Awaiter function that waits until a given functor returns false
inline Task<> WaitWhile(tTaskReadyFn in_readyFn) {}
#endif // IN_DOXYGEN
inline Task<> _WaitUntil(tTaskReadyFn in_readyFn DEBUG_STR) /// @private
{
TASK_NAME("WaitUntil", [debugStr = FormatDebugString(in_debugStr)]{ return debugStr; });
co_await in_readyFn; // Wait until the ready functor returns true
}
inline Task<> _WaitWhile(tTaskReadyFn in_readyFn DEBUG_STR) /// @private
{
TASK_NAME("WaitWhile", [debugStr = FormatDebugString(in_debugStr)]{ return debugStr; });
co_await[&in_readyFn]{ return !in_readyFn(); }; // Wait until the ready function returns false
}
/// Awaiter function that waits forever (only for use in tasks that will be killed externally)
inline Task<> WaitForever()
{
return _WaitUntil([]() { return false; } MANUAL_DEBUG_STR("WaitForever"));
}
/// Awaiter function that waits N seconds in a given time-stream
template <typename tTimeFn>
Task<tTaskTime> WaitSeconds(tTaskTime in_seconds, tTimeFn in_timeFn)
{
auto startTime = in_timeFn();
TASK_NAME(__FUNCTION__, [in_timeFn, startTime, in_seconds] { return std::to_string(GetTimeSince(startTime, in_timeFn)) + "/" + std::to_string(in_seconds); });
auto IsTimerUp = [in_timeFn, startTime, in_seconds] {
return GetTimeSince(startTime, in_timeFn) >= in_seconds;
};
co_await IsTimerUp; // Wait until the timer is up
co_return in_timeFn() - startTime - in_seconds;
}
/// Awaiter function that wraps a given task, canceling it after N seconds in a given time-stream. Returns whether it timed-out or not.
template <typename tRet, typename tTimeFn>
auto Timeout(Task<tRet>&& in_task, tTaskTime in_seconds, tTimeFn in_timeFn)
{
auto IsTimerUp = [in_timeFn, startTime = in_timeFn(), in_seconds]{
return GetTimeSince(startTime, in_timeFn) >= in_seconds;
};
return CancelTaskIf(std::move(in_task), IsTimerUp);
}
/// Awaiter function that calls a given function after N seconds in a given time-stream
template <typename tFn, typename tTimeFn>
Task<> DelayCall(tTaskTime in_delaySeconds, tFn in_fn, tTimeFn in_timeFn)
{
TASK_NAME(__FUNCTION__);
// Call function after N seconds
co_await WaitSeconds(in_delaySeconds, in_timeFn);
in_fn();
}
#if SQUID_ENABLE_GLOBAL_TIME
/// Awaiter function that waits N seconds in the global time-stream (requires SQUID_ENABLE_GLOBAL_TIME)
inline Task<tTaskTime> WaitSeconds(tTaskTime in_seconds)
{
return WaitSeconds(in_seconds, GlobalTime());
}
/// Awaiter function that wraps a given task, canceling it after N seconds in the global time-stream. Returns whether it timed-out or not. (requires SQUID_ENABLE_GLOBAL_TIME)
template <typename tRet>
auto Timeout(Task<tRet>&& in_task, tTaskTime in_seconds)
{
return Timeout(std::move(in_task), in_seconds, GlobalTime());
}
/// Awaiter function that calls a given function after N seconds in the global time-stream (requires SQUID_ENABLE_GLOBAL_TIME)
template <typename tFn>
Task<> DelayCall(tTaskTime in_delaySeconds, tFn in_fn)
{
return DelayCall(in_delaySeconds, in_fn, GlobalTime());
}
#else
template <typename T = void>
Task<tTaskTime> WaitSeconds(tTaskTime in_seconds) /// @private Illegal global-time implementation
{
static_assert(static_false<T>::value, "Global task time not enabled (see SQUID_ENABLE_GLOBAL_TIME in TasksConfig.h)");
return Task<tTaskTime>{};
}
template <typename tRet, typename T = void>
auto Timeout(Task<tRet>&& in_task, tTaskTime in_seconds) /// @private Illegal global-time implementation
{
static_assert(static_false<T>::value, "Global task time not enabled (see SQUID_ENABLE_GLOBAL_TIME in TasksConfig.h)");
return Task<>{};
}
template <typename tFn, typename T = void>
static Task<> DelayCall(tTaskTime in_delaySeconds, tFn in_fn) /// @private Illegal global-time implementation
{
static_assert(static_false<T>::value, "Global task time not enabled (see SQUID_ENABLE_GLOBAL_TIME in TasksConfig.h)");
return Task<>{};
}
#endif //SQUID_ENABLE_GLOBAL_TIME
//--- Cancel-If Implementation ---//
template <typename tRet>
Task<std::optional<tRet>> CancelIfImpl(Task<tRet> in_task, tTaskCancelFn in_cancelFn) /// @private
{
TASK_NAME("CancelIf", [taskHandle = TaskHandle<tRet>(in_task)]{ return taskHandle.GetDebugStack(); });
co_await AddStopTask(in_task); // Setup stop-request propagation
while(true)
{
if(in_cancelFn && in_cancelFn())
{
co_return{};
}
auto taskStatus = in_task.Resume();
if(taskStatus == eTaskStatus::Done)
{
co_return in_task.TakeReturnValue();
}
co_await Suspend();
}
co_return{};
}
inline Task<bool> CancelIfImpl(Task<> in_task, tTaskCancelFn in_cancelFn) /// @private
{
TASK_NAME("CancelIf", [taskHandle = TaskHandle<>(in_task)]{ return taskHandle.GetDebugStack(); });
co_await AddStopTask(in_task); // Setup stop-request propagation
while(true)
{
if(in_cancelFn && in_cancelFn())
{
co_return false;
}
auto taskStatus = in_task.Resume();
if(taskStatus == eTaskStatus::Done)
{
co_return true;
}
co_await Suspend();
}
co_return false;
}
template <typename tRet, eTaskRef RefType, eTaskResumable Resumable>
auto CancelTaskIf(Task<tRet, RefType, Resumable>&& in_task, tTaskCancelFn in_cancelFn) /// @private
{
static_assert(RefType == eTaskRef::Strong && Resumable == eTaskResumable::Yes, "Cannot call CancelIf() on WeakTask, TaskHandle or WeakTaskHandle");
return CancelIfImpl(std::move(in_task), in_cancelFn);
}
//--- Stop-If Implementation ---//
template <typename tRet, typename tTimeFn>
Task<std::optional<tRet>> StopIfImpl(Task<tRet> in_task, tTaskCancelFn in_cancelFn, std::optional<tTaskTime> in_timeout, tTimeFn in_timeFn) /// @private
{
TASK_NAME("StopIf", [taskHandle = TaskHandle<tRet>(in_task), in_timeout]{
return std::string("timeout = ") + (in_timeout ? std::to_string(in_timeout.value()) : "none") + ", task = " + taskHandle.GetDebugStack();
});
co_await AddStopTask(in_task); // Setup stop-request propagation
while(true)
{
if(!in_task.IsStopRequested() && in_cancelFn && in_cancelFn())
{
in_task.RequestStop();
if(in_timeout.has_value())
{
co_return co_await Timeout(std::move(in_task), in_timeout.value(), in_timeFn);
}
}
auto taskStatus = in_task.Resume();
if(taskStatus == eTaskStatus::Done)
{
co_return in_task.TakeReturnValue();
}
co_await Suspend();
}
}
template <typename tTimeFn>
Task<bool> StopIfImpl(Task<> in_task, tTaskCancelFn in_cancelFn, std::optional<tTaskTime> in_timeout, tTimeFn in_timeFn) /// @private
{
TASK_NAME("StopIf", [taskHandle = TaskHandle<>(in_task), in_timeout]{
return std::string("timeout = ") + (in_timeout ? std::to_string(in_timeout.value()) : "none") + ", task = " + taskHandle.GetDebugStack();
});
co_await AddStopTask(in_task); // Setup stop-request propagation
while(true)
{
if(!in_task.IsStopRequested() && in_cancelFn && in_cancelFn())
{
in_task.RequestStop();
if(in_timeout)
{
co_return co_await Timeout(std::move(in_task), in_timeout.value(), in_timeFn);
}
}
auto taskStatus = in_task.Resume();
if(taskStatus == eTaskStatus::Done)
{
co_return true;
}
co_await Suspend();
}
co_return false;
}
template <typename tRet, eTaskRef RefType, eTaskResumable Resumable>
auto StopTaskIf(Task<tRet, RefType, Resumable>&& in_task, tTaskCancelFn in_cancelFn) /// @private
{
return StopIfImpl(std::move(in_task), in_cancelFn, {}, (float(*)())nullptr);
}
#if SQUID_ENABLE_GLOBAL_TIME
template <typename tRet, eTaskRef RefType, eTaskResumable Resumable>
auto StopTaskIf(Task<tRet, RefType, Resumable>&& in_task, tTaskCancelFn in_cancelFn, tTaskTime in_timeout) /// @private
{
return StopIfImpl(std::move(in_task), in_cancelFn, in_timeout, GlobalTime()); // Default time function to global-time
}
#else
template <typename tRet, eTaskRef RefType, eTaskResumable Resumable>
auto StopTaskIf(Task<tRet, RefType, Resumable>&& in_task, tTaskCancelFn in_cancelFn, tTaskTime in_timeout) /// @private Illegal global-time implementation
{
static_assert(static_false<tRet>::value, "Global task time not enabled (see SQUID_ENABLE_GLOBAL_TIME in TasksConfig.h)");
return StopIfImpl(std::move(in_task), in_cancelFn, in_timeout, nullptr); // Default time function to global-time
}
#endif //SQUID_ENABLE_GLOBAL_TIME
template <typename tRet, eTaskRef RefType, eTaskResumable Resumable, typename tTimeFn>
auto StopTaskIf(Task<tRet, RefType, Resumable>&& in_task, tTaskCancelFn in_cancelFn, tTaskTime in_timeout, tTimeFn in_timeFn) /// @private
{
// See forward-declaration for default arguments
static_assert(RefType == eTaskRef::Strong && Resumable == eTaskResumable::Yes, "Cannot call StopIf() on WeakTask, TaskHandle or WeakTaskHandle");
return StopIfImpl(std::move(in_task), in_cancelFn, in_timeout, in_timeFn);
}
/// @} end of addtogroup Tasks
NAMESPACE_SQUID_END