Skip to content

Commit a50c9af

Browse files
authored
✨ (patch) Add stack memory to basic_context (#66)
`basic_context` now has stack memory for the context embedded within itself with the use of a template parameter. It will no longer be a parent class that requires stack memory to be provided. Resolves #63
1 parent ff49f20 commit a50c9af

3 files changed

Lines changed: 189 additions & 5 deletions

File tree

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ if(NOT CMAKE_CROSSCOMPILING AND BUILD_UNIT_TESTS)
8989
PRIVATE
9090
tests/main.test.cpp
9191
tests/basics.test.cpp
92+
tests/basic_context.test.cpp
9293
tests/blocked_by.test.cpp
9394
tests/cancel.test.cpp
9495
tests/exclusive_access.test.cpp

modules/async_context.cppm

Lines changed: 61 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -868,23 +868,22 @@ private:
868868
* @note basic_context is designed for simple use cases and testing, not
869869
* production embedded systems where strict memory management is required.
870870
*/
871-
export class basic_context : public context
871+
class basic_context_impl : public context
872872
{
873873
public:
874-
// TODO(63): Add stack memory template argument
875874
/**
876-
* @brief Default constructor for basic_context
875+
* @brief Default constructor for basic_context_impl
877876
*
878877
* Creates a new basic context with default initialization.
879878
*/
880-
basic_context() = default;
879+
basic_context_impl() = default;
881880

882881
/**
883882
* @brief Virtual destructor for proper cleanup
884883
*
885884
* Ensures that the basic context is properly cleaned up when deleted.
886885
*/
887-
~basic_context() override = default;
886+
~basic_context_impl() override = default;
888887

889888
/**
890889
* @brief Get the pending delay time for time-blocking operations
@@ -969,6 +968,63 @@ private:
969968
sleep_duration m_pending_delay{ 0 };
970969
};
971970

971+
/**
972+
* @brief A basic context implementation that supports synchronous waiting
973+
*
974+
* The basic_context class provides a concrete implementation of the context
975+
* interface that supports synchronous waiting operations. It extends the base
976+
* context with functionality to wait for coroutines to complete using a simple
977+
* synchronous loop.
978+
*
979+
* This class provides stack memory via its `StackSizeInWords` template
980+
* variable.
981+
*
982+
* @tparam StackSizeInWords - the number of words to allocate for the context's
983+
* stack memory. Word size is 4 bytes for 32-bit systems and 8 bytes on 64-bit
984+
* systems.
985+
*/
986+
export template<size_t StackSizeInWords>
987+
class basic_context
988+
{
989+
public:
990+
static_assert(StackSizeInWords > 0UL,
991+
"Stack memory must be greater than 0 words.");
992+
993+
basic_context()
994+
{
995+
m_context.initialize_stack_memory(m_stack);
996+
}
997+
998+
~basic_context()
999+
{
1000+
m_context.cancel();
1001+
}
1002+
1003+
context& context()
1004+
{
1005+
return m_context;
1006+
}
1007+
1008+
operator class context&()
1009+
{
1010+
return m_context;
1011+
}
1012+
1013+
auto* operator->()
1014+
{
1015+
return &m_context;
1016+
}
1017+
1018+
auto& operator*()
1019+
{
1020+
return m_context;
1021+
}
1022+
1023+
private:
1024+
basic_context_impl m_context;
1025+
std::array<uptr, StackSizeInWords> m_stack{};
1026+
};
1027+
9721028
/**
9731029
* @brief A RAII-style guard for exclusive access to a context
9741030
*

tests/basic_context.test.cpp

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
#include <coroutine>
2+
3+
#include <boost/ut.hpp>
4+
5+
import async_context;
6+
import test_utils;
7+
8+
boost::ut::suite<"basic_context"> basic_context = []() {
9+
using namespace boost::ut;
10+
11+
static constexpr auto stack_size = 1024;
12+
13+
"sync return type void"_test = []() {
14+
// Setup
15+
async::basic_context<stack_size> ctx;
16+
17+
unsigned step = 0;
18+
auto sync_coroutine = [&step](async::context&) -> async::future<void> {
19+
step = 1;
20+
return {};
21+
};
22+
23+
// Exercise
24+
auto future = sync_coroutine(ctx);
25+
26+
// Verify
27+
expect(that % 0 == ctx->memory_used());
28+
expect(that % future.done());
29+
expect(that % future.has_value());
30+
expect(that % 1 == step);
31+
32+
expect(that % stack_size == ctx->capacity());
33+
};
34+
35+
"co_await coroutine"_test = []() {
36+
// Setup
37+
async::basic_context<stack_size> ctx;
38+
39+
static constexpr int expected_return_value = 1413;
40+
unsigned step = 0;
41+
auto co2 = [&step](async::context&) -> async::future<int> {
42+
step = 2;
43+
co_await std::suspend_always{};
44+
// skipped as the co1 will immediately resume
45+
step = 3;
46+
co_return expected_return_value;
47+
};
48+
auto co = [&step, &co2](async::context& p_ctx) -> async::future<int> {
49+
step = 1; // skipped as the co2 will immediately start
50+
co_await co2(p_ctx);
51+
step = 4;
52+
co_return expected_return_value;
53+
};
54+
55+
// Exercise 1
56+
auto future = co(ctx);
57+
58+
// Verify 1
59+
expect(that % 0 < ctx->memory_used());
60+
expect(that % not future.done());
61+
expect(that % not future.has_value());
62+
expect(that % 0 == step);
63+
64+
// Exercise 2: start, enter co_2, start immediately and suspend
65+
future.resume();
66+
67+
// Verify 2
68+
expect(that % 0 < ctx->memory_used());
69+
expect(that % not future.done());
70+
expect(that % not future.has_value());
71+
expect(that % 2 == step);
72+
73+
// Exercise 3: resume, co_2 co_returns, immediately resumes parent, return
74+
future.resume();
75+
76+
// Verify 3
77+
expect(that % 0 == ctx->memory_used());
78+
expect(that % future.done());
79+
expect(that % future.has_value());
80+
expect(that % expected_return_value == future.value());
81+
expect(that % 4 == step);
82+
83+
expect(that % stack_size == ctx->capacity());
84+
};
85+
86+
"co_await coroutine"_test = []() {
87+
// Setup
88+
async::basic_context<stack_size> ctx;
89+
90+
static constexpr int return_value1 = 1413;
91+
static constexpr int return_value2 = 4324;
92+
static constexpr int expected_total = return_value1 + return_value2;
93+
94+
unsigned step = 0;
95+
auto co2 = [](async::context&) -> async::future<int> {
96+
return return_value1;
97+
};
98+
99+
auto co = [&step, &co2](async::context& p_ctx) -> async::future<int> {
100+
step = 1; // skipped as the co2 will immediately start
101+
auto val = co_await co2(p_ctx);
102+
step = 2;
103+
co_return val + return_value2;
104+
};
105+
106+
// Exercise 1
107+
auto future = co(ctx);
108+
109+
// Verify 1
110+
expect(that % 0 < ctx->memory_used());
111+
expect(that % not future.done());
112+
expect(that % not future.has_value());
113+
expect(that % 0 == step);
114+
115+
// Exercise 2: start, call co_2, returns value immediately and co_returns
116+
future.resume();
117+
118+
// Verify 3
119+
expect(that % 0 == ctx->memory_used());
120+
expect(that % future.done());
121+
expect(that % future.has_value());
122+
expect(that % expected_total == future.value());
123+
expect(that % 2 == step);
124+
125+
expect(that % stack_size == ctx->capacity());
126+
};
127+
};

0 commit comments

Comments
 (0)