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.

Coroutine A libcoro scheduler — an epoll loop with cooperative coroutines. A single thread can drive many concurrent connections with low memory per in-flight call. On Linux the io_uring path is available for high-throughput I/O. A scheduler is always required, because the marshaller's resume semantics depend on one.
Blocking A thread pool (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.