IDL Walkthrough
An inference service, from interface to deployment.
A streaming inference backend whose contract and calling model can stay stable locally, over TLS, or inside an SGX enclave, while deployment and security composition change around it.
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 supports blocking and
co_awaitbuilds through Canopy's bi-modal API.
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
namespace v1— the recommended explicit, stable versioned name in every generated typerpc::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 selected by build and connection configuration
- No client binding baked into the contract — C++ and supported generated JavaScript/Protocol Buffers paths come from the same IDL; broader runtimes are planned
- No execution model — the same source compiles blocking or
co_await - No separate version field — the namespace name is the version;
v1andv2can coexist - No hand-written wire protocol — framing, encoding, dispatch, and generated contract identities are handled by Canopy
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
An explicit namespace v1 is the recommended 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, and a later v2 can
coexist without silently redirecting existing callers.
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 chosen through the build and the service or transport configuration used for newly created proxies. A C++-to-C++ path might use YAS binary for throughput; the generated browser client uses Protocol Buffers for cross-language compatibility, and future runtime bindings can use the same schema. 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 no application response is awaited
for each token. That removes a per-token request/response round trip; throughput still depends on
serialisation, transport capacity, scheduling, and backpressure.
Step 4 — choose a deployment
The interface and enclave-compatible business logic can stay the same between these deployments. Transport construction, platform packaging, and security policy are supplied separately.
Local (same process)
In-process, plugin, or DLL boundary with low-overhead local and binary paths. Useful during development and for component 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. An SGX transport crosses the enclave boundary; when route security and DCAP policy are configured, evidence is verified and bound to the protected session before sensitive RPC is admitted.
The client-side connection factory materialises the matching profile and returns an
rpc::shared_ptr<i_inference_service>. From that point the typed application calls use
the same contract across 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.
Canopy's built-in get_schema RPC retrieves generated interface descriptors across
local, streaming, SGX, and protected routes. The C++ runtime also provides schema-driven dynamic
call and post helpers, which are used by the MCP integration. Generated browser clients remain
the supported typed JavaScript path; a completely generic no-stub browser facade is future work.
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.