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;
  }
}