Scope note:
- the examples in this document are written for the primary C++ implementation
- they demonstrate C++ runtime patterns and do not imply full parity in Rust or JavaScript
- for implementation scope, see C++ Status, Rust Status, and JavaScript Status
Who this is for:
- readers who understand the basics and want pattern-oriented examples
- developers looking for a starting point rather than copy-paste-complete code
- readers who want to see how the concepts fit together in C++
Working examples demonstrating Canopy features and patterns in the C++ implementation.
For basic calculator examples covering IDL definition, implementation, and simple usage, see Getting Started. The examples below focus on advanced patterns and cross-zone communication scenarios.
The local child transport creates a child zone from a parent service. For peer processes or hosts, use one of the streaming transports instead.
#include "calculator_impl.h"
class parent_app
{
std::shared_ptr<rpc::root_service> service_;
rpc::shared_ptr<calculator::v1::i_calculator> child_calculator_;
public:
CORO_TASK(error_code) start()
{
service_ = rpc::root_service::create(
"parent", rpc::DEFAULT_PREFIX);
auto child_transport = std::make_shared<rpc::local::child_transport>(
"calculator_child", service_);
child_transport->set_child_entry_point<
calculator::v1::i_calculator,
calculator::v1::i_calculator>(
[](rpc::shared_ptr<calculator::v1::i_calculator> parent_calculator,
std::shared_ptr<rpc::child_service> child_service)
-> CORO_TASK(rpc::service_connect_result<calculator::v1::i_calculator>)
{
(void)parent_calculator;
(void)child_service;
CO_RETURN rpc::service_connect_result<calculator::v1::i_calculator>{
rpc::error::OK(), calculator::create_calculator()};
});
rpc::shared_ptr<calculator::v1::i_calculator> input_calculator;
auto connect_result
= CO_AWAIT service_->connect_to_zone<
calculator::v1::i_calculator,
calculator::v1::i_calculator>(
"calculator_child",
child_transport,
input_calculator);
if (connect_result.error_code != rpc::error::OK())
CO_RETURN connect_result.error_code;
child_calculator_ = connect_result.output_interface;
std::cout << "Connected to child zone\n";
CO_RETURN rpc::error::OK();
}
CORO_TASK(error_code) use_calculator()
{
int result;
auto error = CO_AWAIT child_calculator_->add(100, 200, result);
if (error != rpc::error::OK())
CO_RETURN error;
std::cout << "100 + 200 = " << result << "\n";
CO_RETURN rpc::error::OK();
}
};#include "calculator_impl.h"
#include <coro/scheduler.hpp>
#include <thread>
class coro_server
{
std::shared_ptr<rpc::root_service> service_;
rpc::shared_ptr<calculator::v1::i_calculator> calculator_;
std::shared_ptr<coro::scheduler> scheduler_;
public:
void start()
{
scheduler_ = coro::scheduler::make_unique(
coro::scheduler::options{
.thread_strategy = coro::scheduler::thread_strategy_t::spawn,
.pool = coro::thread_pool::options{
.thread_count = std::thread::hardware_concurrency(),
},
.execution_strategy = coro::scheduler::execution_strategy_t::process_tasks_on_thread_pool
});
service_ = rpc::root_service::create(
"coro_server", rpc::DEFAULT_PREFIX, scheduler_);
calculator_ = calculator::create_calculator();
// Spawn background task
scheduler_->spawn(listen_for_requests());
}
auto listen_for_requests() -> CORO_TASK(void)
{
// Background task to handle requests
CO_RETURN;
}
void run()
{
bool running = true;
while (running)
{
scheduler_->process_events(std::chrono::milliseconds(1));
}
}
};namespace service
{
interface i_factory
{
error_code create_object([out] rpc::shared_ptr<i_data>& obj);
error_code process_object([in] rpc::shared_ptr<i_data> obj);
};
interface i_data
{
error_code get_value([out] int& value);
error_code set_value(int value);
};
}class data_impl : public rpc::base<data_impl, i_data>
{
int value_ = 0;
public:
CORO_TASK(error_code) get_value(int& value) override
{
value = value_;
CO_RETURN rpc::error::OK();
}
CORO_TASK(error_code) set_value(int value) override
{
value_ = value;
CO_RETURN rpc::error::OK();
}
};class factory_impl : public rpc::base<factory_impl, i_factory>
{
public:
CORO_TASK(error_code) create_object(rpc::shared_ptr<i_data>& obj) override
{
obj = rpc::make_shared<data_impl>();
CO_RETURN rpc::error::OK();
}
CORO_TASK(error_code) process_object(rpc::shared_ptr<i_data> obj) override
{
if (!obj)
CO_RETURN rpc::error::INVALID_DATA();
int value;
auto error = CO_AWAIT obj->get_value(value);
std::cout << "Received object with value: " << value << "\n";
CO_RETURN error;
}
};#include <iostream>
void handle_error(rpc::error_code error)
{
if (error == rpc::error::OK())
{
std::cout << "Success\n";
}
else if (error == rpc::error::INVALID_DATA())
{
std::cout << "Invalid data\n";
}
else if (error == rpc::error::OBJECT_GONE())
{
std::cout << "Object was destroyed\n";
}
else if (error == rpc::error::TRANSPORT_ERROR())
{
std::cout << "Transport error\n";
}
else
{
std::cout << "Error: " << static_cast<int>(error) << "\n";
}
}// Assume foo_ptr is rpc::shared_ptr<xxx::i_foo>
rpc::shared_ptr<xxx::i_foo> foo_ptr = get_foo();
// Try to cast to i_bar
auto bar_ptr = CO_AWAIT rpc::dynamic_pointer_cast<xxx::i_bar>(foo_ptr);
if (bar_ptr)
{
// Successfully cast to i_bar
bar_ptr->bar_method();
}
else
{
// foo_ptr does not implement i_bar
}Optimistic pointers are for references to objects with independent lifetimes (e.g., database connections, services managed externally). They don't keep objects alive but also don't return serious errors if the object is gone.
// Long-lived service object with an independent lifetime
rpc::optimistic_ptr<i_database> db = get_database_callback();
auto error = CO_AWAIT db->query("SELECT * FROM users");
if (error == rpc::error::OBJECT_GONE())
{
// Expected expiry for an optimistic pointer target
// Refresh the reference or skip this operation
}// A client gives a long-running service an optimistic callback reference.
rpc::optimistic_ptr<i_token_listener> client_callback = ...;
auto error = CO_AWAIT client_callback->on_token("next token");
if (error == rpc::error::OBJECT_GONE())
{
// The client has disconnected or released the callback object.
// Stop streaming or clean up associated state.
}Key Difference: OBJECT_GONE (optimistic) means the remote weak target
was checked at call time and is no longer available, while OBJECT_NOT_FOUND
(shared) means an object disappeared despite a strong distributed reference.
The WebSocket demo shows a complete real-world application:
demos/websocket/
├── server/
│ ├── server.cpp # Main entry point
│ ├── transport.h/cpp # WebSocket transport
│ ├── demo_zone.h # RPC service
│ └── demo.cpp # Calculator implementation
├── client/
│ ├── test_calculator.js # Node.js RPC client
│ └── websocket_proto.js # Generated protobuf
└── idl/
└── websocket_demo.idl # Interface definitions
Key patterns demonstrated:
- Custom transport implementation
- Coroutine-based async I/O
- Protobuf serialization
- Browser and Node.js clients
- JSON schema generation
template<class T>
class type_test : public testing::Test
{
protected:
T lib_;
public:
T& get_lib() { return lib_; }
void SetUp() override { lib_.set_up(); }
void TearDown() override { lib_.tear_down(); }
};
TYPED_TEST(my_test_case, test_name)
{
auto& lib = this->get_lib();
// Test implementation
}template<typename TestFixture, typename CoroFunc>
void run_coro_test(TestFixture& fixture, CoroFunc&& coro_func)
{
auto& lib = fixture.get_lib();
#ifdef CANOPY_BUILD_COROUTINE
bool completed = false;
auto wrapper = [&]() -> CORO_TASK(bool)
{
auto result = CO_AWAIT coro_func(lib);
completed = true;
CO_RETURN result;
};
lib.get_scheduler()->spawn(wrapper());
while (!completed)
lib.get_scheduler()->process_events(std::chrono::milliseconds(1));
#else
coro_func(lib);
#endif
}- Best Practices - Guidelines and troubleshooting
- API Reference - Complete API documentation
- Building Canopy - Build configuration