Coroutine Machine Process — a C++23 coroutine runtime project.
mcpp · Architecture · Issues
Important
CMP provides a lazy, single-consumer Task<T> / Task<void> and a caller-thread RunLoop
with explicit scheduling. Cancellation, timers, asynchronous I/O, and detached execution are
not implemented.
CMP is being built as a modern coroutine runtime and library on standard stackless C++
coroutines. The intended direction is an explicit co_await model that can grow, in small
verified steps, toward scheduling, timers, asynchronous I/O, cancellation, and safe handling of
blocking work.
The name stands for Coroutine Machine Process. CMP uses C as the intended name for a lightweight coroutine execution unit. This is analogous to Go runtime's G as a naming and mental-model inspiration only; it is not a claim that a future CMP task is already equivalent to a goroutine.
The project is guided by a few principles:
- use C++23 standard stackless coroutines and C++ Modules;
- keep suspension explicit through
co_awaitand purpose-built awaiters; - develop runtime pieces incrementally, with tests and small reviewable changes;
- support more than server workloads;
- keep mcpp as the single source of build and package truth.
C++ standard coroutines are a language mechanism, not a complete runtime. CMP therefore does not promise that:
- a task is automatically equivalent to a Go goroutine;
- an arbitrary blocking call becomes non-blocking;
- coroutine switching is safe directly inside a signal handler;
- M:N scheduling, work stealing, timers, cancellation, or async I/O already exist.
Those capabilities must be designed and verified individually. The expected direction is explicit async I/O awaiters, a dedicated blocking pool, and cooperative safe points.
Install xlings, then install the mcpp version pinned by
.xlings.json:
xlings install
mcpp --version
mcpp build
mcpp testRun the standalone consumer:
cd examples/basic
mcpp runThe example prints Coroutine result: 42 from inside a scheduled Task<void> coroutine and exits
successfully. It proves that an independent mcpp package can resolve the path dependency, import
mcpplibs.cmp, compose Tasks, and drive them through the public RunLoop.
import std;
import mcpplibs.cmp;
using mcpplibs::cmp::Task;
using mcpplibs::cmp::RunLoop;
Task<int> answer() {
co_return 42;
}
Task<void> print_answer(RunLoop::Scheduler scheduler) {
co_await scheduler.schedule();
auto value = co_await answer();
std::println("Coroutine result: {}", value);
co_return;
}
int main() {
RunLoop loop {};
loop.run(print_answer(loop.get_scheduler()));
}Task is lazy: calling answer() creates a suspended coroutine. It starts when consumed by
co_await. A Task is move-only, has one consumer, and can only be awaited as an rvalue. It stores
either a value or an exception, transfers directly between child and continuation, and destroys
an unconsumed frame through RAII. Task<T&>, copying, move assignment, and detached execution are
deliberately unsupported.
A translation unit that defines a coroutine must import std so the compiler can see the standard
coroutine protocol types. CMP imports std privately and does not re-export the whole standard
library.
RunLoop::run() consumes one root Task, executes ready coroutines on the calling thread, returns
its value, and rethrows its exception. Scheduler::schedule() always suspends and queues the
continuation. Scheduler handles are copyable, but remain tied to their originating RunLoop.
Sequential run() calls are supported; nested or concurrent calls are rejected. A moved-from Task
must not be awaited.
RunLoop is not a background thread and does not make blocking code asynchronous. A Task that
suspends without arranging a future resume can leave run() waiting indefinitely. CMP does not
provide automatic thread affinity: after an external awaiter resumes on another thread, explicitly
await the desired Scheduler to return to its RunLoop.
.
├── .xlings.json # pinned project tool environment
├── mcpp.toml # package identity and test dependency
├── src/cmp.cppm # root module interface
├── src/task.cppm # Task module partition
├── src/run_loop.cppm # RunLoop and Scheduler partition
├── tests/cmp_test.cpp # Task contract and lifetime tests
├── tests/run_loop_test.cpp # scheduler, boundary, and threading tests
├── examples/basic/ # standalone path-dependency consumer
├── docs/architecture.md # current structure, boundaries, and evolution
└── .github/workflows/ # Linux, macOS, and Windows CI
The repository does not ship mcpp new templates yet. Purpose-built templates can be added
after CMP has a stable runtime API worth demonstrating.
The local verification path is:
mcpp build --cache=off
mcpp test --cache=off
cd examples/basic && mcpp runCI runs the equivalent build, test, and standalone example flow on Linux, macOS, and Windows.
The mcpp version is pinned by .xlings.json; contributors should not rely on an unrelated
global mcpp installation.
CMP does not track mcpp.lock; .gitignore enforces that repository policy. Runtime dependencies
belong in [dependencies]; gtest is declared explicitly under [dev-dependencies.compat].
Runtime work is split into independently reviewable phases:
- package identity and importable-module bootstrap — implemented;
- coroutine task and lifetime semantics — initial
Taskimplemented; - a root runner and minimal single-thread scheduler — initially implemented;
- timers, cancellation, and structured wake-up paths;
- multi-worker scheduling and work stealing;
- asynchronous I/O integration and a blocking pool.
The remaining order is directional, not a promise that a listed feature is already implemented.
Read the architecture notes before changing module boundaries. Keep
changes small, use C++23 module conventions, and treat mcpp build, mcpp test, the standalone
example, and CI as the implementation facts.