This section provides a comprehensive view of Canopy's internal architecture. Understanding these components is essential for advanced usage, debugging, and contributing to the project.
The architecture documentation is organized by component, each building on previous concepts:
- Zones - Execution context boundaries
- Services - Object lifecycle management within zones
- Memory Management - Smart pointers and reference counting
- Proxies and Stubs - RPC marshalling machinery
- Transports and Passthroughs - Communication plumbing between services
- Zone Hierarchies - Multi-level zone topologies
Every object in Canopy lives within a zone—an execution context with its own identity, object namespace, and service manager. Zones represent process any boundary, machine, logical, shared object, or secure enclave boundaries.
Each zone has a service that acts as the central authority for:
- Object registration and ID generation
- Transport connection management
- Reference count tracking
- Zone lifecycle coordination
Canopy's memory model is built entirely on smart pointers:
rpc::shared_ptr- RAII ownership semantics (object dies when references reach zero)rpc::weak_ptr- Non-owning references for breaking cyclesrpc::optimistic_ptr- Non-RAII references for independent lifetimes
Zone Death (Amnesia): A zone dies when all shared_ptr references are released—references to objects in the zone, from the zone (outbound proxies), and through the zone (passthroughs).
RPC marshalling operates at two levels:
- Object-level: Generic
object_proxyandobject_stubprovide RPC machinery - Interface-level: Generated
i_calculator_proxyandi_calculator_stubprovide type-safe method dispatch and parameter serialization/deserialization in configurable formats (YAS binary, JSON, Protocol Buffers)
Communication between services uses two mechanisms:
- Transports: Direct connections between adjacent zones
- Passthroughs: Multi-hop routing through intermediary zones when zones aren't adjacent
Transport lifetime is managed by multiple strong reference holders:
- Service proxies - Hold strong references (
member_ptr) to transports - Passthroughs - Hold strong references to both transports (forward and reverse) and to the intermediary service, keeping the entire routing path alive
- Child services - Hold strong reference to parent transport
- Active stubs - May cause transports to hold strong references to adjacent transports
- Services - Hold only weak references (registry only, doesn't keep alive)
This creates the reference chain that keeps zones and their communication plumbing alive. Zones can function purely as routing intermediaries, staying alive as long as passthroughs exist routing traffic through them. These cirular references are designed to form a self supporting structure that can be torn down when no longer needed. when the last reference is released, the zone dies. A bit like the last person walking off a suspension bridge results in the bridge neatly folding itself up and leaving no mess behind.
Client Zone (Zone 1) Server Zone (Zone 2)
│ │
├─ Client Code │
│ └─ calc_proxy->add(a, b) │
│ │
├─ Interface Proxy │
│ (i_calculator_proxy) │
│ └─ serialize params │
│ │
├─ Object Proxy │
│ └─ lookup service_proxy │
│ │
├─ Service Proxy │
│ └─ get transport │
│ │
├─ Transport │
│ └─ send() │
│ ▼ │
│ ════════════════════════► │
│ ├─ Transport
│ │ └─ inbound_send()
│ │
│ ├─ Object Stub
│ │ └─ find interface_stub
│ │
│ ├─ Interface Stub
│ │ (i_calculator_stub)
│ │ └─ deserialize params
│ │ └─ dispatch to impl
│ │
│ ├─ Implementation
│ │ (calculator_impl)
│ │ └─ result = a + b
│ │
│ ◄════════════════════════ │
│ ▲ │
└─ return result │
┌─────────────────────────────────────────────────────┐
│ Zone │
│ │ │
│ ┌────────────────────┼────────────────────┐ │
│ │ │ │ │
│ │ Service │ │
│ │ │ │ │
│ │ ┌────────────┼────────────┐ │ │
│ │ │ │ │ │ │
│ │ Objects Transports Service │ │
│ │ (stubs) (weak_ptr) Proxies │ │
│ │ │ │ │ │ │
│ └───────┼────────────┼─────────────┼───────┘ │
│ │ │ │ │
│ shared_ptr member_ptr shared_ptr │
│ │ │ │ │
└──────────┼────────────┼─────────────┼────────────────┘
│ │ │
Local Objects Transport Remote Objects
(in zone) (to other (from other
zones) zones)
Key Insight: Zone stays alive as long as there are references in, from, or through it. When all three counts reach zero, the zone enters "amnesia" and begins shutdown.
Zone 1: Client creates shared_ptr<i_calc>
↓
Service 1: Marshals reference to Zone 2
↓
Transport 1→2: Sends add_ref(object_id, zone_id)
↓
Transport 2: Receives add_ref, creates object_stub
↓
Object Stub: Increments shared_count_
↓
Zone 2: Object kept alive by remote reference
1. All references released:
- Local objects in zone destroyed
- Remote proxies to zone released
- Passthroughs through zone destroyed
2. Service detects it is no longer needed:
This occurs when all shared pointers to it are released.
3. Service notifies transport:
transport->disconnect()
no further inbound or outbound communication then occurs with the zone
4. Transport cleanup:
- Disconnect from remote
- Release parent reference (for child zones)
- Notify optimistic pointers
5. Zone destruction:
- Service destructor runs
- Transport destructor runs
- Zone memory released
Transports are kept alive by:
- Service proxies holding strong references while proxies exist
- Active stubs causing transports to maintain references to adjacent zones
- Passthroughs keeping routing paths alive
- Services hold only weak references to transports (registry, doesn't keep alive)
- child_service` holds a strong reference to its parent transport, ensuring the parent zone outlives the child. This creates an intentional circular dependency that's broken during shutdown.
When all service proxies and active stubs are destroyed, the transport can be cleaned up.
- TCP: Network communication between independent zones
- SPSC: Lock-free inter-process communication
- Local: In-process parent/child zones
- SGX: Host/enclave secure communication
- DLL: In-process child zones loaded from shared objects
- Coroutine DLL: Coroutine-aware in-process DLL child zones
- IPC + DLL: Child processes that host DLL-backed child zones over SPSC streams
When zones aren't adjacent, a passthrough routes communication through an intermediary:
Zone 1
↓
┌──┴──┐
Zone 2 Zone 3
Zone 2 → Zone 3 communication:
Zone 2 → Zone 1 (passthrough) → Zone 3
Passthroughs hold strong references to both transports AND the intermediary service, keeping the entire routing path (including the intermediary zone) alive.
IDL File (calculator.idl)
↓
IDL Parser (submodules/idlparser)
↓
AST (Abstract Syntax Tree)
↓
Code Generator (generator/src/synchronous_generator.cpp)
↓
Generated C++ Headers
├─ calculator.h (interface definition)
├─ calculator_proxy.h (client-side marshalling)
├─ calculator_stub.h (server-side marshalling)
└─ calculator_schema.h (JSON metadata)
Each interface gets:
- Pure virtual base class (
i_calculator) - Proxy class for clients (
i_calculator_proxy) - serializes parameters, deserializes results - Stub class for servers (
i_calculator_stub) - deserializes parameters, serializes results - Schema metadata for introspection
Serialization formats (configurable via generator options):
yas_binary- Binary serialization (default, high performance)yas_json- JSON serialization (human-readable, universal fallback)yas_compressed_binary- Compressed binaryprotocol_buffers- Protocol Buffers format
Never mix rpc::shared_ptr with std::shared_ptr. Use RPC smart pointers throughout RPC code, and keep standard pointers separate.
member_ptr are non callable shared rpc and std pointers to access the shared pointer you need to extract a local variable copy of it preventing race condiditions on the lifetime of that object
Each service only interacts with its own zone's objects. It is possible to use get_current_service() to access the current service in non-coroutine code only (this function is not suitable for coroutine code). Cross-zone access goes through transports and proxies.
When calls cross zone boundaries, stack-based rpc::shared_ptr and rpc::optimistic_ptr prevents transport destruction during active calls, rpc::weak_ptr should not be used normally as it is a weak pointer to the proxy and not the remote object:
Enable telemetry for visualization:
- Zone creation/destruction events
- Reference count changes (add_ref/release)
- Transport status changes
- Passthrough routing activity
See Telemetry Guide for details.
Read the architecture documents in order to build a complete mental model:
- Zones - Start with execution contexts
- Services - Understand lifecycle management
- Memory Management - Master smart pointers
- Proxies and Stubs - Learn marshalling machinery
- Transports and Passthroughs - Understand communication plumbing
- Zone Hierarchies - Build complex topologies
For practical usage, see the Developer Guide.