pw_rpc2 adapter#
pw_rpc: Efficient, low-code-size RPC system for embedded devices
Projects using pw_rpc2 can serve existing pw_rpc C++ services
(Nanopb, pw_protobuf, or raw) from an rpc2::Server
without modifying the service implementations or .proto files. pw_rpc2
clients generated from the same .proto file call the adapted services just
like native pw_rpc2 services.
The adapter is a transitional tool with code size, memory, and performance
costs; see Tradeoffs & migration path and migrate services to
native pw_rpc2 whenever possible.
Build system setup#
The adapter is defined in pw_rpc/rpc2_adapter.h. Add the adapter dependency
to your build target:
Bazel:
//pw_rpc:rpc2_adapterGN:
$dir_pw_rpc:rpc2_adapterCMake:
pw_rpc.rpc2_adapter
Because the adapter interacts with the pw::rpc::Server across multiple
threads, PW_RPC_USE_GLOBAL_MUTEX must be enabled (1, the default).
Choosing a server adapter#
pw_rpc provides two server adapter classes depending on whether your system
also runs a pw_rpc server.
Use pw_rpc services in pw_rpc2 only (Rpc2Adapter)#
Rpc2Adapter lets you run pw_rpc services in
pw_rpc2 without an existing pw_rpc server. Register services with it
using Rpc2ServiceFor.
pw::rpc2::Server rpc2_server(allocator, dispatcher);
// Create an adapter for running pw_rpc services without an existing
// pw::rpc::Server.
pw::rpc::Rpc2Adapter adapter(adapter_allocator, thread_options);
// Construct, own, and register SensorServiceImpl with the adapter.
pw::rpc::Rpc2ServiceFor<SensorServiceImpl> sensor_service(adapter);
PW_CHECK_OK(rpc2_server.RegisterService(sensor_service));
Integrate with an existing pw_rpc server (Rpc2ServerAdapter)#
Use Rpc2ServerAdapter when your firmware
already runs a pw::rpc::Server and you want to expose services on that
server to pw_rpc2 clients as well.
Unless PW_RPC_DYNAMIC_ALLOCATION is enabled, the pw::rpc::Server
must have unassigned channels for the adapter to use. Each pw_rpc2
connection opens a new channel. The server’s other channels continue operating
normally, so a single service instance can serve both pw_rpc and pw_rpc2
clients concurrently.
// Set up an existing pw_rpc server and service. Any channels already
// configured on the server keep working, and unassigned channels are used for
// pw_rpc2 connections.
std::array<pw::rpc::Channel, 4> channels{};
pw::rpc::Server rpc_server(channels);
SensorServiceImpl sensor_service;
rpc_server.RegisterService(sensor_service);
pw::rpc2::Server rpc2_server(allocator, dispatcher);
// Bridge the pw_rpc2 server and the existing pw_rpc server.
pw::rpc::Rpc2ServerAdapter adapter(
rpc_server, adapter_allocator, thread_options);
// Wrap the existing pw_rpc service for registration with the pw_rpc2 server.
pw::rpc::Rpc2ServiceAdapter adapted_sensor(adapter, sensor_service);
PW_CHECK_OK(rpc2_server.RegisterService(adapted_sensor));
Adapting services#
Wrap each pw_rpc service as an rpc2::Service using
either Rpc2ServiceFor or
Rpc2ServiceAdapter, then pass it to
rpc2::Server::RegisterService.
Construct and own a service (Rpc2ServiceFor)#
Rpc2ServiceFor constructs and owns a pw_rpc
service. Constructor arguments after the adapter are forwarded to the service’s
constructor. Access the underlying service via service().
pw::rpc::Rpc2ServiceFor<SensorServiceImpl> sensor_service(
adapter, /*temperature_offset=*/0.5f);
PW_CHECK_OK(rpc2_server.RegisterService(sensor_service));
Wrap an existing service instance (Rpc2ServiceAdapter)#
Rpc2ServiceAdapter wraps an existing
pw_rpc service that is registered with a shared rpc::Server.
SensorServiceImpl sensor_service;
rpc_server.RegisterService(sensor_service);
pw::rpc::Rpc2ServiceAdapter adapted_sensor(adapter, sensor_service);
PW_CHECK_OK(rpc2_server.RegisterService(adapted_sensor));
Non-OK status codes and response payloads#
In pw_rpc, unary and client streaming RPCs return both a pw::Status
and a response protobuf, whereas pw_rpc2 has no application status codes and
returns either a response message (with kOk) or an error status with no
payload.
By default, finishing a unary or client streaming pw_rpc call with any
non-OK status ends the pw_rpc2 call with kCancelled and discards any
response payload (for server and bidirectional streaming calls, all queued
SERVER_STREAM messages are delivered first before ending with
kCancelled).
If a unary or client streaming method encodes failure inside its response
message alongside a non-OK status, pass DeliverNonOkResponses() to Rpc2ServiceFor or Rpc2ServiceAdapter so the adapter delivers the response payload to
pw_rpc2 clients instead of cancelling the call. Pass specific generated
method functions to opt in individual methods, or call
DeliverNonOkResponses() with no template arguments to opt in all unary and
client streaming methods of a service (for Rpc2ServiceFor, any arguments
after DeliverNonOkResponses() are forwarded to the service’s constructor):
// Deliver non-OK responses for specific methods. Arguments after the policy
// tag are forwarded to SensorServiceImpl's constructor.
pw::rpc::Rpc2ServiceFor<SensorServiceImpl> sensor_service(
adapter,
pw::rpc::DeliverNonOkResponses<
SensorServiceImpl::ServiceInfo::GetReading>(),
/*temperature_offset=*/0.5f);
PW_CHECK_OK(rpc2_server.RegisterService(sensor_service));
// Deliver non-OK responses for all unary and client streaming methods.
pw::rpc::Rpc2ServiceAdapter adapted_sensor(
adapter, pw::rpc::DeliverNonOkResponses(), sensor_service);
PW_CHECK_OK(rpc2_server.RegisterService(adapted_sensor));
Warning
pw_rpc2 clients receive kOk alongside the serialized response for
every non-OK status returned by an opted-in method, including early
error returns that leave the response protobuf default-initialized. Opted-in
methods must populate error state in the response message on every non-OK
return path. Only use the service-wide DeliverNonOkResponses() form when
every unary and client streaming method in the service follows this contract.
Behavior differences from pw_rpc#
Channels: Each
pw_rpc2connection gets its ownpw_rpcchannel ID (starting at first_channel_id, 100 by default, up to max_channels). When a connection closes, its channel remains open and is reused by the nextpw_rpc2connection, so per-channel state (such as apw_log_rpcdrain) carries over. Do not close the adapter’s channels or useSetDefaultChannelOutput()on a shared server.Threading: Handlers and stream callbacks for
pw_rpc2calls run on the adapter’s worker thread. A service shared with otherpw_rpcchannels may be invoked from both threads concurrently.Allocator: The adapter allocates per-connection state and queued packets from its pw::Allocator across threads (under an internal lock). Do not share a non-thread-safe allocator with the rpc2::Server.
Status codes:
pw_rpc2does not have application status codes. By default, finishing apw_rpccall with any non-OKstatus ends thepw_rpc2call withkCancelledand discards any final response payload; see Non-OK status codes and response payloads.Outbound stream buffering: The adapter queues up to max_queued_messages_per_call (8 by default) outgoing stream messages per call. Writes fail with
UNKNOWNif the queue is full or the adapter’s allocator is exhausted.Client stream completion: Client and bidirectional streaming handlers must enable PW_RPC_COMPLETION_REQUEST_CALLBACK and use
set_on_completion_requestedto detect when the client closes its stream.Resource limits: New calls fail with
kUnavailableif the adapter reaches max_channels, has no free channel slot on thepw_rpcserver, or cannot allocate memory. An active call that runs out of memory ends with an error.Teardown: Close the rpc2::Server, then call Rpc2ServerAdapter::Stop before destroying adapted services or the adapter.
Stop()requiresPW_THREAD_JOINING_ENABLED = 1; without thread joining, the adapter must never be destroyed (for example, declare it with pw::RuntimeInitGlobal).
Tradeoffs & migration path#
The adapter is intended as a transitional tool to enable incremental migration
to pw_rpc2, or to reuse pre-existing pw_rpc services before
native pw_rpc2 versions are available. Migrate services fully to
pw_rpc2 whenever possible.
Running pw_rpc services through the adapter carries significant costs
compared to native pw_rpc2 services:
Larger code size (flash): The binary must link both the
pw_rpcandpw_rpc2server implementations, plus the adapter layer that bridges them.Extra thread and RAM overhead: Because
pw_rpcmethod handlers are synchronous and may block, the adapter runs a dedicated worker thread (which requires its own stack) so handlers cannot stall thepw_rpc2pw_async2 dispatcher. The adapter also dynamically allocates per-connection state and copies of in-flight packets from a pw::Allocator, in addition topw_rpc’s static call objects and encoding buffer.Lower performance and higher latency: Native
pw_rpc2services run cooperatively on the dispatcher thread with zero-copy pw_buf payloads and minimal locking. Every call to an adaptedpw_rpcservice allocates and copies packet payloads into a work queue, acquires both the adapter’s mutex andpw_rpc’s global mutex, and context-switches between thepw_rpc2dispatcher thread and the adapter’s worker thread.Limited flow control and backpressure:
pw_rpcwriters cannot suspend a task while waiting for transport capacity. Instead of end-to-end cooperative backpressure, the adapter buffers up to a fixed number of outbound stream messages per call (max_queued_messages_per_call) and fails writes once that queue is full.Reduced status fidelity:
pw_rpc2does not have application status codes in final responses. By default non-OKstatuses are collapsed intokCancelled. If a unary or client streaming method is configured with DeliverNonOkResponses(), non-OKstatuses are ignored completely.
Once a service no longer needs to serve pw_rpc clients, rewrite it as a
native pw_rpc2 service to eliminate these overheads and take full advantage
of pw_rpc2.