- Notifications
You must be signed in to change notification settings - Fork1
stackless coroutine, but zero-allocation
License
jamboree/coz
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
Stackless coroutine for C++, but zero-allocation. Rebirth ofCO2.
- Boost.Config
- Boost.Preprocessor
C++20 introduced coroutine into the language, however, in many cases (especially in async scenario), it incurs memory allocation, as HALO is not guaranteed. This creates resistance to its usage, as the convenience it offers may not worth the overhead it brings. People will tend to write monolithic coroutines instead of splitting them into small, reusable coroutines in fear of introducing too many allocations, this is contrary to the discipline of programming.
COZ is a single-header library that utilizes preprocessor & compiler magic to emulate the C++ coroutine, while requires zero allocation, it also doesn't require type erasure. With COZ, the entire coroutine is under your control, unlike standard coroutine, which can only be accessed indirectly via thecoroutine_handle
.
NOTE
COZ uses stateful metaprogramming technique, which may not be blessed by the standard committee.
This library is modeled after the standard coroutine. It offers several macros to replace the language counterparts.
To use it,#include <coz/coroutine.hpp>
A coroutine written in this library looks like below:
autofunction(Args... args) COZ_BEG(promise-initializer, (captured-args...), local-vars...;) {// for generator-like coroutineCOZ_YIELD(...);// for task-like coroutineCOZ_AWAIT(...);COZ_RETURN(...);} COZ_END
The coroutine body has to be surrounded with 2 macros:COZ_BEG
andCOZ_END
.
The macroCOZ_BEG
takes some parameters:
- promise-initializer - expression to initialize the promise, e.g.
async<int>(exe)
- captured-args (optional) - comma separated args to be captured, e.g.
(a, b)
- local-vars (optional) - local-variable definitions, e.g.
int a = 42;
If there's nocaptured-args andlocals, it looks like:
COZ_BEG(init, ())
Thepromise-initializer is an expression, whose type must define apromise_type
, which will be constructed with the expression.It can take args from the function params. For example, you can take an executor to be used for the promise.
template<classExe>autof(Exe exe) COZ_BEG(async<int>(exe), ())
- the args (e.g.
exe
in above example) don't have to be in thecaptured-args. - if the expression contains comma that is not in parentheses, you must surround the it with parentheses (e.g.
(task<T, E>)
).
You can intialize the local variables as below:
autof(int i) COZ_BEG(init, (i), int i2 = i * 2;// can refer to the arg std::string msg{"hello"};) ...
()
initializer cannot be used.auto
deduced variable cannot be used.
Inside the coroutine body, there are some restrictions:
- local variables with automatic storage cannot cross suspension points - you should specify them in local variables section of
COZ_BEG
as described above switch
body cannot contain suspension points.- identifiers starting with
_coz_
are reserved for this library - Some language constructs should use their marcro replacements (see below).
After defining the coroutine body, remember to close it withCOZ_END
.
It has 4 variants:COZ_AWAIT
,COZ_AWAIT_SET
,COZ_AWAIT_APPLY
andCOZ_AWAIT_LET
.
MACRO | Core Language |
---|---|
COZ_AWAIT(expr) | co_await expr |
COZ_AWAIT_SET(var, expr) | var = co_await expr |
COZ_AWAIT_APPLY(f, expr, args...) | f(co_await expr, args...) |
COZ_AWAIT_LET(var-decl, expr) {...} | {var-decl = co_await expr; ...} |
- The
expr
is either used directly or transformed.operator co_await
is not used. - If your compiler supportsStatement Expression extension (e.g. GCC & Clang), you can use
COZ_AWAIT
as an expression.However, don't use more than oneCOZ_AWAIT
in a single statement, and don't use it as an argument of a function in company with other arguments. f
inCOZ_AWAIT_APPLY
can also be a marco (e.g.COZ_RETURN
)COZ_AWAIT_LET
allows you to declare a local variable that binds to theco_await
result, then you can process it in the brace scope.
MACRO | expr Lifetime |
---|---|
COZ_YIELD(expr) | transient |
COZ_YIELD_KEEP(expr) | cross suspension point |
promise.yield_value(expr);<suspend>
- It differs from the standard semantic, which is equivalent to
co_await promise.yield_value(expr)
. Instead, we ignore the result ofyield_value
and just suspend afterward. - While
COZ_YIELD_KEEP
is more general,COZ_YIELD
is more optimization-friendly.
MACRO | Core Language |
---|---|
COZ_RETURN() | co_return |
COZ_RETURN(expr) | co_return expr |
Needed only if the try-block contains suspension points.
COZ_TRY { ...} COZ_CATCH (const std::runtime_error& e) { ...}catch (const std::exception& e) { ...}
Only the firstcatch
clause needs to be written asCOZ_CATCH
, the subsequent ones should use the plaincatch
.
coz::coroutine
has interface defined as below:
template<classPromise,classParams,classState>structcoroutine {template<classInit>explicitcoroutine(Init&& init);// No copy.coroutine(const coroutine&) =delete; coroutine&operator=(const coroutine&) =delete; coroutine_handle<Promise>handle()noexcept; Promise&promise()noexcept;const Promise&promise()constnoexcept;booldone()constnoexcept;voidstart(Params&& params);voidresume();voiddestroy();};
- The
init
constructor param is thepromise-initializer. - The lifetime of
Promise
is tied to the coroutine. - Non-started coroutine is considered to be
done
. - Don't call
destroy
if it's alreadydone
.
coz::coroutine_handle
has the same interface as the standard one.
This defines what is returned from the coroutine.The prototype is:
template<classInit,classParams,classState>structco_result;
The first template param (i.e.Init
) is the type ofpromise-initializer.Params
andState
are the template params that you should pass tocoz::coroutine<Promise, Params, State>
, thePromise
should be the same asInit::promise_type
.
Users could customize it like below:
template<classParams,classState>struct [[nodiscard]] coz::co_result<MyCoroInit, Params, State> { MyCoroInit m_init; Params m_params;// optionalautoget_return_object(); ...};
co_result
will be constructed the with thepromise-initializer and thecaptured-args.- if
get_return_object
is defined, its result is returned; otherwise, theco_result
itself is returned.
The interface forPromise looks like below:
structPromise {voidfinalize();// eithervoidreturn_void();// orvoidreturn_value();voidunhandled_exception();// optionalautoawait_transform(auto expr);};
- There's no
initial_suspend
andfinal_suspend
.The user should callcoroutine::start
to start the coroutine. - Once the coroutine stops (either normally or via
destroy
) thePromise::finalize
will be called. await_transform
is not greedy (i.e. could be filtered by SFINAE).
The interface forAwaiter looks like below:
structAwaiter {boolawait_ready();// eithervoidawait_suspend(coroutine_handle<Promise> coro);// orboolawait_suspend(coroutine_handle<Promise> coro); Tawait_resume();};
- Unlike standard coroutine,
await_suspend
cannot returncoroutine_handle
.
Copyright (c) 2024 JamboreeDistributed under the Boost Software License, Version 1.0. (See accompanyingfile LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)