C++ with raw RPC#
pw_rpc: Efficient, low-code-size RPC system for embedded devices
Raw RPC allows C++ service methods and clients to send and receive raw,
unparsed byte buffers (pw::ConstByteSpan / pw::span<const std::byte>)
directly, without passing them through generated protobuf structs or classes.
When to use raw RPC#
Zero-copy serialization: Encode fields directly into wire buffers in-place using pw_protobuf stream encoders.
Large or dynamic payloads: Handle variable-length binary payloads, chunked firmware images, or pass-through proxy data without intermediate buffer copies.
Low-overhead benchmarking: Implement echo and throughput test services with minimal CPU cycles (see Benchmarking).
Mixing with Nanopb or pw_protobuf: Fall back to raw methods on individual performance-critical RPCs within a Nanopb or
pw_protobufservice.
Usage#
In your build file, depend on the raw_rpc variant of your proto library:
# Bazel: raw_rpc_proto_library
# GN: :my_protos.raw_rpc
Include the generated header "my_project/sensor_service.raw_rpc.pb.h":
#include "my_project/sensor_service.raw_rpc.pb.h"
#include "pw_bytes/span.h"
#include "pw_rpc/raw/server_reader_writer.h"
Server-side#
Implement services by inheriting from the generated pw_rpc::raw service base:
class RawSensorService final
: public ::pw::rpc::examples::pw_rpc::raw::SensorService::Service<
RawSensorService> {
public:
// 1. Unary RPC: accepts raw request bytes and completes with responder
void GetReading(pw::ConstByteSpan request_bytes,
pw::rpc::RawUnaryResponder& responder) {
PW_LOG_INFO("Received %u raw request bytes",
static_cast<unsigned>(request_bytes.size()));
std::byte response_buffer[64]{};
static_cast<void>(responder.Finish(response_buffer, pw::OkStatus()));
}
// 2. Server Streaming RPC
void StreamReadings(pw::ConstByteSpan request_bytes,
pw::rpc::RawServerWriter& writer) {
PW_LOG_INFO("Streaming readings for request of size %u",
static_cast<unsigned>(request_bytes.size()));
std::byte chunk[32]{};
static_cast<void>(writer.Write(chunk));
static_cast<void>(writer.Finish(pw::OkStatus()));
}
// 3. Bidirectional Streaming RPC
void Calibrate(pw::rpc::RawServerReaderWriter& stream) {
stream_ = std::move(stream);
stream_.set_on_next([this](pw::ConstByteSpan payload) {
// Echo back raw calibration payload
static_cast<void>(stream_.Write(payload));
});
}
private:
pw::rpc::RawServerReaderWriter stream_;
};
Mixing raw methods into services#
You do not need to declare an entire service as raw. You can implement individual methods as raw inside a Nanopb or pw_protobuf service:
class MixedSensorService final
: public ::pw::rpc::examples::pw_rpc::nanopb::SensorService::Service<
MixedSensorService> {
public:
// Standard Nanopb unary method:
pw::Status GetReading(const pw_rpc_examples_SensorRequest& request,
pw_rpc_examples_SensorResponse& response) {
PW_LOG_INFO("Reading sensor %u", static_cast<unsigned>(request.sensor_id));
response.temperature = 22.5f;
response.humidity = 45.0f;
return pw::OkStatus();
}
// Raw server streaming method fallback:
void StreamReadings([[maybe_unused]] pw::ConstByteSpan request_bytes,
pw::rpc::RawServerWriter& writer) {
std::byte payload[32]{};
static_cast<void>(writer.Write(payload));
static_cast<void>(writer.Finish(pw::OkStatus()));
}
};
Client-side#
Raw clients allow sending and receiving raw byte spans directly:
using RawSensorClient = ::pw::rpc::examples::pw_rpc::raw::SensorService::Client;
[[maybe_unused]] void InvokeRawRpc() {
RawSensorClient raw_client(client, 1);
std::byte request_payload[16]{};
auto call = raw_client.GetReading(
request_payload, [](pw::ConstByteSpan response, pw::Status status) {
if (status.ok()) {
PW_LOG_INFO("Received %u raw bytes",
static_cast<unsigned>(response.size()));
}
});
}