IDL Walkthrough
An inference service, from interface to deployment.
A streaming inference backend that works locally, over TLS, or inside an SGX enclave — without changing the interface, the implementation, or the calling code.
The scenario
You are building a C++ inference backend and you need callers in other processes, other machines, or a browser UI to be able to start an inference run, receive tokens as they are generated, and cancel if needed.
Without a framework you write serialisation on both sides, manage a callback channel, design a wire protocol for streaming output, handle object lifetimes across the connection, and repeat that work for every transport you need. Canopy replaces that with a single IDL file and generated glue on both sides.
- Token streaming arrives via a callback interface — the server calls back into the client over the same connection, fire-and-forget.
- A session object is created on the server and returned as an
rpc::shared_ptr— the session stays alive exactly as long as the caller holds it. - The service factory is the top-level entry point — callers connect to it and ask it to create sessions.
- The same interface compiles in both blocking and
co_awaitmodes; nothing changes.
Step 1 — write the IDL
The IDL describes the callable surface: interfaces, methods, argument direction, and one-way calls. It says nothing about transport, serialisation, or execution model.
IDL features used here
[inline] namespace v1— stable versioned name without nesting callers in two namespacesrpc::optional<T>— omissible field with an explicit default[post]— one-way fire-and-forget; no reply channel opened per tokenrpc::optimistic_ptr<T>— callable non-owning reference (breaks the callback cycle)rpc::shared_ptr<T>— returned session whose lifetime is controlled by the caller[out]— output parameter direction annotation[description=…]— description embedded in the generated JSON schema
What you are not deciding here
- No transport — TCP, IPC, local, DLL, or SGX is chosen at construction time
- No serialisation format — YAS binary, JSON, or Protocol Buffers is a build or per-connection choice
- No target language — C++, Rust (via Protocol Buffers), and JavaScript all consume the same IDL
- No execution model — the same source compiles blocking or
co_await - No version number — the namespace name is the version;
v1andv2coexist without flags or negotiation - No wire protocol — framing, encoding, and version negotiation are handled by the runtime
The IDL is the type system
The IDL file is not just documentation — it is the single canonical definition from which every downstream artefact is generated. Change a type in the IDL and the C++ proxies, protobuf descriptors, JSON schemas, and JavaScript stubs all update in the next build. Nothing can drift out of sync because nothing is written twice.
Version management
[inline] namespace v1 is the versioning mechanism. The qualified name
inference::v1::i_session is the version — there is no separate
version field to keep in sync with the interface shape.
At build time, Canopy writes a fingerprint for every interface marked
[status=production] into a check_sums/production/ file.
If the interface definition changes, the fingerprint changes, and a CI check
can reject the build — enforcing that a production contract is never silently modified.
To evolve a production interface, you rename it: bump
v1 to v2. The old name stays callable by any code
compiled against it; the new name starts a fresh contract. Both can coexist
in the same IDL file and the same running service.
Serialisation format as a deployment detail
Each CanopyGenerate target (yas_binary, yas_json,
protocol_buffers) is a different transformation of the same IDL types.
The caller holds an rpc::shared_ptr<i_session> — it does
not know or specify which wire format is in use.
Format is selected at transport construction time and can be negotiated per connection. A C++-to-C++ path might use YAS binary for throughput; a browser client uses JSON for readability; a Rust service uses Protocol Buffers for cross-language compatibility. The interface code does not change.
The same IDL attributes that annotate types for C++ also annotate the
generated JSON schemas: [description=…] becomes hover
documentation in VS Code and method descriptions in MCP tool definitions.
Annotations written once appear everywhere they are needed.
Step 2 — what Canopy generates
One CanopyGenerate CMake call produces C++ for each requested serialisation format.
The generated headers expose the same virtual interface the IDL described —
no hand-written serialisation, dispatch, or transport code on either side.
After generation the caller-side header exposes the pure virtual interface exactly as written in the IDL. The caller holds an rpc::shared_ptr<i_inference_service> and calls it like a local object:
Step 3 — write the implementation
The server-side implementation inherits from rpc::base and overrides the interface methods.
There is no serialisation, no transport plumbing, and no callback channel to set up —
that is all in generated code.
[post] annotation on on_token means the server calls it and moves
on immediately — there is no round-trip delay waiting for the client to acknowledge each token.
Throughput is bounded by serialisation and network bandwidth, not latency.
Step 4 — choose a deployment
The implementation above does not change between these three deployments. The only difference is which transport object you construct around it.
Local (same process)
Plugin or DLL boundary. Zero-copy serialisation path for the binary format. Useful during development and for in-process isolation.
TCP + TLS
Network deployment between processes or machines. The stream transformer wraps each accepted TCP connection in TLS before handing it to the transport — application code unchanged.
SGX enclave
Inference inside a trusted-execution environment. Remote attestation confirms the enclave identity before the session begins. Same IDL, same implementation — different transport constructor.
The client-side connection follows the same pattern: build the corresponding transport,
call connect_to_zone, and receive an rpc::shared_ptr<i_inference_service>.
From that point the calling code above works identically for all three deployments.
Step 5 — schemas and agent discovery
The same IDL that drives generated C++ also drives generated JSON schemas. Two schema profiles serve different consumers from a single source:
Config profile
Full authoring schema: descriptions from [description=…],
default values, string|integer enums, additionalProperties: false,
and cross-file $ref with $id.
Used by VS Code and other JSON-schema-aware editors to provide completion and validation when writing Canopy configuration files.
MCP profile
Minimal tool schema: everything inlined, no $id, string-only enums,
no defaults. Sized and shaped for LLM tool-use layers.
An AI agent receives this schema and can build a valid JSON call for any method in the interface — with no hand-written schema maintenance.
At runtime, a caller can ask a Canopy service to describe itself. The service returns method names, parameter schemas, and interface metadata — all derived from the generated code, with no hand-written MCP configuration needed.
Marking an interface [introspectable] Planned
allows runtime discovery via i_marshaller::get_schema. A browser client or AI agent
can call get_schema on a connected object, receive the live MCP tool list,
and then call methods by name using JSON-encoded parameters — with no generated client stubs
and no hard-coded method identifiers.
What was written vs what was generated
CanopyGenerate call specifying the IDL file and desired serialisation formats.generate, cancel, and the session factory — no transport or serialisation code.Try the calculator demo first
The WebSocket calculator uses the same IDL pattern — simpler interface, same transport and generation story. Run it to see the generated call path end to end before exploring a fuller service contract.