Execution
One source tree. Blocking or coroutine, chosen at build time.
The same C++ implementation compiles as plain blocking calls or as C++20
co_await coroutines. Which one you get is a build preset — your interface code
and your service code are byte-for-byte identical between the two.
How the same code becomes both
Canopy writes async-capable interface methods with RPC_TASK,
CO_AWAIT, and CO_RETURN (with CORO_TASK for supporting
coroutine-aware code). In a coroutine build they expand to task and coroutine forms; in a blocking
build they become ordinary synchronous C++.
// You write this once; the IDL [out] annotation becomes a C++ reference:
RPC_TASK(rpc::result) add(int a, int b, int& r) {
CO_RETURN CO_AWAIT calc_->add(a, b, r);
}
// Blocking build → rpc::result add(...) { return calc_->add(a, b, r); }
// Coroutine build → rpc::coro::task<rpc::result> add(...) { co_return co_await ...; }
Switching modes is a CMake preset — Debug for blocking,
Debug_Coroutine for coroutines. Because there is no hidden async machinery to
infer, the interfaces are also easy for AI tools to generate and reason about.
What drives the work: the executor
Long-running work — streaming loops, dispatch — runs on an rpc::executor. It
takes a different concrete form in each mode but exposes the same call sites, so the same
source drives both.
rpc::blocking_executor) with per-worker queues and
work-stealing. It is opt-in: non-streaming code runs entirely on the
caller's thread and pays nothing for a pool. Streaming features require one. One
worker drives each streaming loop — ideal for tens of connections, not
thousands.
io_uring is reserved for the coroutine path —
its async-completion model only pays off there. Blocking-mode TCP uses plain POSIX
recv/send with poll().
When to use which
Blocking for clarity
Plain stack frames, standard debuggers, any C++17 toolchain. The simplest path for development, testing, and low-connection deployments — and the only path where non-streaming code carries no thread-pool cost at all.
Coroutines for scale
Many concurrent connections per thread, lower memory per call, io_uring on Linux. The right choice for networked and streamed services under load. Costs: C++20, a coroutine toolchain, and suspension-point bugs that surface as stalls rather than crashes.
One-way [post] calls
A method marked [post] sends and returns immediately — no reply is
awaited beyond local errors. The basis for high-rate feeds: price data, telemetry,
media frames, and streamed LLM tokens.
Coroutines inside SGX
Standard library <coroutine> support is absent inside an SGX
enclave; Canopy supplies its own, so the coroutine model — and io_uring-style async
I/O to the host — works across the enclave boundary too.