Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ if(NOT CMAKE_CROSSCOMPILING AND BUILD_UNIT_TESTS)
PRIVATE
tests/main.test.cpp
tests/basics.test.cpp
tests/basic_context.test.cpp
tests/blocked_by.test.cpp
tests/cancel.test.cpp
tests/exclusive_access.test.cpp
Expand Down
66 changes: 61 additions & 5 deletions modules/async_context.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -868,23 +868,22 @@ private:
* @note basic_context is designed for simple use cases and testing, not
* production embedded systems where strict memory management is required.
*/
export class basic_context : public context
class basic_context_impl : public context
{
public:
// TODO(63): Add stack memory template argument
/**
* @brief Default constructor for basic_context
* @brief Default constructor for basic_context_impl
*
* Creates a new basic context with default initialization.
*/
basic_context() = default;
basic_context_impl() = default;

/**
* @brief Virtual destructor for proper cleanup
*
* Ensures that the basic context is properly cleaned up when deleted.
*/
~basic_context() override = default;
~basic_context_impl() override = default;

/**
* @brief Get the pending delay time for time-blocking operations
Expand Down Expand Up @@ -969,6 +968,63 @@ private:
sleep_duration m_pending_delay{ 0 };
};

/**
* @brief A basic context implementation that supports synchronous waiting
*
* The basic_context class provides a concrete implementation of the context
* interface that supports synchronous waiting operations. It extends the base
* context with functionality to wait for coroutines to complete using a simple
* synchronous loop.
*
* This class provides stack memory via its `StackSizeInWords` template
* variable.
*
* @tparam StackSizeInWords - the number of words to allocate for the context's
* stack memory. Word size is 4 bytes for 32-bit systems and 8 bytes on 64-bit
* systems.
*/
export template<size_t StackSizeInWords>
class basic_context
{
public:
static_assert(StackSizeInWords > 0UL,
"Stack memory must be greater than 0 words.");

basic_context()
{
m_context.initialize_stack_memory(m_stack);
}

~basic_context()
{
m_context.cancel();
}

context& context()
{
return m_context;
}

operator class context&()
{
return m_context;
}

auto* operator->()
{
return &m_context;
}

auto& operator*()
{
return m_context;
}

private:
basic_context_impl m_context;
std::array<uptr, StackSizeInWords> m_stack{};
};

/**
* @brief A RAII-style guard for exclusive access to a context
*
Expand Down
127 changes: 127 additions & 0 deletions tests/basic_context.test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
#include <coroutine>

#include <boost/ut.hpp>

import async_context;
import test_utils;

boost::ut::suite<"basic_context"> basic_context = []() {
using namespace boost::ut;

static constexpr auto stack_size = 1024;

"sync return type void"_test = []() {
// Setup
async::basic_context<stack_size> ctx;

unsigned step = 0;
auto sync_coroutine = [&step](async::context&) -> async::future<void> {
step = 1;
return {};
};

// Exercise
auto future = sync_coroutine(ctx);

// Verify
expect(that % 0 == ctx->memory_used());
expect(that % future.done());
expect(that % future.has_value());
expect(that % 1 == step);

expect(that % stack_size == ctx->capacity());
};

"co_await coroutine"_test = []() {
// Setup
async::basic_context<stack_size> ctx;

static constexpr int expected_return_value = 1413;
unsigned step = 0;
auto co2 = [&step](async::context&) -> async::future<int> {
step = 2;
co_await std::suspend_always{};
// skipped as the co1 will immediately resume
step = 3;
co_return expected_return_value;
};
auto co = [&step, &co2](async::context& p_ctx) -> async::future<int> {
step = 1; // skipped as the co2 will immediately start
co_await co2(p_ctx);
step = 4;
co_return expected_return_value;
};

// Exercise 1
auto future = co(ctx);

// Verify 1
expect(that % 0 < ctx->memory_used());
expect(that % not future.done());
expect(that % not future.has_value());
expect(that % 0 == step);

// Exercise 2: start, enter co_2, start immediately and suspend
future.resume();

// Verify 2
expect(that % 0 < ctx->memory_used());
expect(that % not future.done());
expect(that % not future.has_value());
expect(that % 2 == step);

// Exercise 3: resume, co_2 co_returns, immediately resumes parent, return
future.resume();

// Verify 3
expect(that % 0 == ctx->memory_used());
expect(that % future.done());
expect(that % future.has_value());
expect(that % expected_return_value == future.value());
expect(that % 4 == step);

expect(that % stack_size == ctx->capacity());
};

"co_await coroutine"_test = []() {
// Setup
async::basic_context<stack_size> ctx;

static constexpr int return_value1 = 1413;
static constexpr int return_value2 = 4324;
static constexpr int expected_total = return_value1 + return_value2;

unsigned step = 0;
auto co2 = [](async::context&) -> async::future<int> {
return return_value1;
};

auto co = [&step, &co2](async::context& p_ctx) -> async::future<int> {
step = 1; // skipped as the co2 will immediately start
auto val = co_await co2(p_ctx);
step = 2;
co_return val + return_value2;
};

// Exercise 1
auto future = co(ctx);

// Verify 1
expect(that % 0 < ctx->memory_used());
expect(that % not future.done());
expect(that % not future.has_value());
expect(that % 0 == step);

// Exercise 2: start, call co_2, returns value immediately and co_returns
future.resume();

// Verify 3
expect(that % 0 == ctx->memory_used());
expect(that % future.done());
expect(that % future.has_value());
expect(that % expected_total == future.value());
expect(that % 2 == step);

expect(that % stack_size == ctx->capacity());
};
};