C++ with pw_protobuf#
Caution
If you’re starting a new project, Pigweed recommends Nanopb over
pw_protobuf. See Using pw_protobuf.
pw_rpc can generate services which encode/decode RPC requests and responses
as pw_protobuf message structs.
Usage#
Define a pw_proto_library containing the .proto file defining your service
(and optionally other related protos), then depend on the pwpb_rpc
version of that library in the code implementing the service.
# chat/BUILD.gn
import("$dir_pw_build/target_types.gni")
import("$dir_pw_protobuf_compiler/proto.gni")
pw_proto_library("chat_protos") {
sources = [ "chat_protos/chat_service.proto" ]
}
# Library that implements the Chat service.
pw_source_set("chat_service") {
sources = [
"chat_service.cc",
"chat_service.h",
]
public_deps = [ ":chat_protos.pwpb_rpc" ]
}
A C++ header file is generated for each input .proto file, with the .proto
extension replaced by .rpc.pwpb.h. For example, given the input file
chat_protos/chat_service.proto, the generated header file will be placed
at the include path "chat_protos/chat_service.rpc.pwpb.h".
Generated code API#
All examples in this document use the following RPC service definition.
syntax = "proto3";
package chat;
service Chat {
// Returns information about a chatroom.
rpc GetRoomInformation(RoomInfoRequest) returns (RoomInfoResponse);
// Lists all of the users in a chatroom.
rpc ListUsersInRoom(ListUsersRequest) returns (stream ListUsersResponse);
// Uploads a file, in chunks, to a chatroom.
rpc UploadFile(stream UploadFileRequest) returns (UploadFileResponse);
// Sends messages to a chatroom while receiving messages from other users.
rpc SendMessage(stream ChatMessage) returns (stream ChatMessage);
}
message RoomInfoRequest {
string room = 1;
}
message RoomInfoResponse {
string room = 1;
uint32 users = 2;
}
message ListUsersRequest {
string room = 1;
}
message ListUsersResponse {
string user = 1;
}
message UploadFileRequest {
bytes chunk = 1;
}
message UploadFileResponse {
uint32 bytes_received = 1;
}
message ChatMessage {
string msg = 1;
uint32 timestamp = 2;
}
Server-side#
A C++ class is generated for each service in the .proto file. The class is
located within a special pw_rpc::pwpb sub-namespace of the file’s package.
The generated class is a base class which must be derived to implement the service’s methods. The base class is templated on the derived class.
class PwpbChatService final
: public ::chat::pw_rpc::pwpb::Chat::Service<PwpbChatService> {
public:
// 1. Unary RPC
pw::Status GetRoomInformation(
const ::chat::pwpb::RoomInfoRequest::Message& request,
::chat::pwpb::RoomInfoResponse::Message& response) {
PW_LOG_INFO("Room requested: %s", request.room.data());
response.room = request.room;
response.users = 42;
return pw::OkStatus();
}
// 2. Server Streaming RPC
void ListUsersInRoom(
const ::chat::pwpb::ListUsersRequest::Message& request,
pw::rpc::PwpbServerWriter<::chat::pwpb::ListUsersResponse::Message>&
writer) {
PW_LOG_INFO("Listing users in room: %s", request.room.data());
::chat::pwpb::ListUsersResponse::Message user1{.user = "Alice"};
static_cast<void>(writer.Write(user1));
::chat::pwpb::ListUsersResponse::Message user2{.user = "Bob"};
static_cast<void>(writer.Write(user2));
static_cast<void>(writer.Finish(pw::OkStatus()));
}
// 3. Client Streaming RPC
void UploadFile(
pw::rpc::PwpbServerReader<::chat::pwpb::UploadFileRequest::Message,
::chat::pwpb::UploadFileResponse::Message>&
reader) {
upload_reader_ = std::move(reader);
upload_reader_.set_on_next(
[this](const ::chat::pwpb::UploadFileRequest::Message& request) {
total_bytes_ += request.chunk.size();
if (request.chunk.empty()) {
// Upload finished: complete the call with total bytes transferred
::chat::pwpb::UploadFileResponse::Message response{
.bytes_received = static_cast<uint32_t>(total_bytes_)};
static_cast<void>(upload_reader_.Finish(response, pw::OkStatus()));
}
});
}
// 4. Bidirectional Streaming RPC
void SendMessage(
pw::rpc::PwpbServerReaderWriter<::chat::pwpb::ChatMessage::Message,
::chat::pwpb::ChatMessage::Message>&
stream) {
chat_stream_ = std::move(stream);
chat_stream_.set_on_next(
[this](const ::chat::pwpb::ChatMessage::Message& message) {
::chat::pwpb::ChatMessage::Message reply{
.msg = "Echo", .timestamp = message.timestamp};
static_cast<void>(chat_stream_.Write(reply));
});
}
private:
pw::rpc::PwpbServerReader<::chat::pwpb::UploadFileRequest::Message,
::chat::pwpb::UploadFileResponse::Message>
upload_reader_;
pw::rpc::PwpbServerReaderWriter<::chat::pwpb::ChatMessage::Message,
::chat::pwpb::ChatMessage::Message>
chat_stream_;
size_t total_bytes_ = 0;
};
The writer and reader helper APIs provide methods to stream and finish calls:
-
Status PwpbServerWriter::Write(const Response::Message &response)#
Writes a single response message to the stream. The returned status indicates whether the write was successful.
-
Status PwpbServerWriter::Finish(Status status = OkStatus())#
Closes the stream and sends back the RPC’s overall status to the client.
-
Status PwpbServerWriter::TryFinish(Status status = OkStatus())#
Closes the stream and sends back the RPC’s overall status to the client only if the final packet is successfully sent.
Attention
Make sure to use std::move when passing the PwpbServerWriter around to
avoid accidentally closing it and ending the RPC.
Client-side#
A corresponding client class is generated for every service defined in the proto
file. To allow multiple types of clients to exist, it is placed under the
pw_rpc::pwpb namespace. The Client class is nested under
pw_rpc::pwpb::ServiceName. For example, the Chat service would create
chat::pw_rpc::pwpb::Chat::Client.
Service clients are instantiated with a reference to the RPC client through which they will send requests, and the channel ID they will use.
Callback invocation
RPC callbacks are invoked synchronously from Client::ProcessPacket.
Unary RPC#
A unary RPC call takes the request struct and a callback to invoke when a response is received. The callback receives the RPC’s status and response struct.
Server streaming RPC#
A server streaming RPC call takes the initial request struct and two callbacks. The first is invoked on every stream response received, and the second is invoked once the stream is complete with its overall status.
Client streaming RPC#
A client streaming RPC call returns a PwpbClientWriter object used to send
a stream of requests, and takes a callback invoked when the server’s final
response arrives:
[[maybe_unused]] void StartUpload() {
PwpbChatClient chat_client(client, 1);
auto writer = chat_client.UploadFile(
[](const ::chat::pwpb::UploadFileResponse::Message& response,
pw::Status status) {
if (status.ok()) {
PW_LOG_INFO("Uploaded %u bytes",
static_cast<unsigned>(response.bytes_received));
}
});
::chat::pwpb::UploadFileRequest::Message chunk{};
static_cast<void>(writer.Write(chunk));
static_cast<void>(writer.RequestCompletion());
}
Bidirectional streaming RPC#
A bidirectional streaming RPC call returns a PwpbClientReaderWriter object
used to send requests, and takes callbacks for incoming stream responses and stream
completion:
[[maybe_unused]] void StartChat() {
PwpbChatClient chat_client(client, 1);
auto stream = chat_client.SendMessage(
[](const ::chat::pwpb::ChatMessage::Message& msg) {
PW_LOG_INFO("Message from room: %s", msg.msg.data());
},
[](pw::Status status) { PW_LOG_INFO("Chat closed: %s", status.str()); });
::chat::pwpb::ChatMessage::Message msg{.msg = "Hello!", .timestamp = 0};
static_cast<void>(stream.Write(msg));
static_cast<void>(stream.RequestCompletion());
}
Example usage#
The following example demonstrates how to call an RPC method using a pw_protobuf service client and receive the response.
using PwpbChatClient = ::chat::pw_rpc::pwpb::Chat::Client;
void LogRoomInformation(const ::chat::pwpb::RoomInfoResponse::Message& response,
pw::Status status) {
if (status.ok()) {
PW_LOG_INFO("Room %s has %u users",
response.room.data(),
static_cast<unsigned>(response.users));
}
}
[[maybe_unused]] void InvokeSomeRpcs() {
PwpbChatClient chat_client(client, 1);
// Unary call
auto call =
chat_client.GetRoomInformation({.room = "pigweed"}, LogRoomInformation);
if (!call.active()) {
return;
}
}