Design & roadmap#

pw_rpc: Efficient, low-code-size RPC system for embedded devices

Overview#

The semantics of pw_rpc are similar to gRPC.

RPC call lifecycle#

In pw_rpc, an RPC begins when the client sends an initial packet. The server receives the packet, looks up the relevant service method, then calls into the RPC function. The RPC is considered active until the server sends a status to finish the RPC. The client may terminate an ongoing RPC by cancelling it. Multiple concurrent RPC requests to the same method may be made simultaneously (Note: Concurrent requests are not yet possible using the Java client. See Issue 237418397).

Depending the type of RPC, the client and server exchange zero or more protobuf request or response payloads. There are four RPC types:

  • Unary. The client sends one request and the server sends one response with a status.

  • Server streaming. The client sends one request and the server sends zero or more responses followed by a status.

  • Client streaming. The client sends zero or more requests and the server sends one response with a status.

  • Bidirectional streaming. The client sends zero or more requests and the server sends zero or more responses followed by a status.

Events#

The key events in the RPC lifecycle are:

  • Start. The client initiates the RPC. The server’s RPC body executes.

  • Finish. The server sends a status and completes the RPC. The client calls a callback.

  • Request. The client sends a request protobuf. The server calls a callback when it receives it. In unary and server streaming RPCs, there is only one request and it is handled when the RPC starts.

  • Response. The server sends a response protobuf. The client calls a callback when it receives it. In unary and client streaming RPCs, there is only one response and it is handled when the RPC completes.

  • Error. The server or client terminates the RPC abnormally with a status. The receiving endpoint calls a callback.

  • Request Completion. The client sends a message that it would like to request call completion. The server calls a callback when it receives it. Some servers may ignore the request completion message. In client and bidirectional streaming RPCs, this also indicates that client has finished sending requests.

Services#

A service is a logical grouping of RPCs defined within a .proto file. pw_rpc uses these .proto definitions to generate code for a base service, from which user-defined RPCs are implemented.

pw_rpc supports multiple protobuf libraries, and the generated code API depends on which is used.

Services must be registered with a server in order to call their methods. Services may later be unregistered, which cancels calls for methods in that service and prevents future calls to them, until the service is re-registered.

Background:

Status codes#

pw_rpc call objects (ClientReaderWriter, ServerReaderWriter, etc.) use certain status codes to indicate what occurred. These codes are returned from functions like Write() or Finish().

  • OK – The operation succeeded.

  • UNAVAILABLE – The channel is not currently registered with the server or client.

  • UNKNOWN – Sending a packet failed due to an unrecoverable pw::rpc::ChannelOutput::Send() error.

Unrequested responses#

pw_rpc supports sending responses to RPCs that have not yet been invoked by a client. This is useful in testing and in situations like an RPC that triggers reboot. After the reboot, the device opens the writer object and sends its response to the client.

The C++ API for opening a server reader/writer takes the generated RPC function as a template parameter. The server to use, channel ID, and service instance are passed as arguments. The API is the same for all RPC types, except the appropriate reader/writer class must be used.

// Open a ServerWriter for a server streaming RPC.
auto writer = RawServerWriter::Open<pw_rpc::raw::ServiceName::MethodName>(
    server, channel_id, service_instance);

// Send some responses, even though the client has not yet called this RPC.
CHECK_OK(writer.Write(encoded_response_1));
CHECK_OK(writer.Write(encoded_response_2));

// Finish the RPC.
CHECK_OK(writer.Finish(OkStatus()));

Errata#

Prior to support for concurrent requests to a single method, no identifier was present to distinguish different calls to the same method. When a “call ID” feature was first introduced to solve this issue, existing clients and servers (1) set this value to zero and (2) ignored this value.

When initial support for concurrent methods was added, a separate “open call ID” was introduced to distinguish unrequested responses. However, legacy servers built prior to this change continue to send unrequested responses with call ID zero. Prior to this fix, clients which used “open call ID” would not accept unrequested responses from legacy servers. Clients built after that change will accept unrequested responses which use both “open call ID” and call ID zero.

See Issue 237418397 for more details and discussion.

Naming#

For upstream Pigweed services, this naming style is a requirement. Note that some services created before this was established may use non-compliant names. For Pigweed users, this naming style is a suggestion.

Reserved names#

pw_rpc reserves a few service method names so they can be used for generated classes. The following names cannot be used for service methods:

  • Client

  • Service

  • Any reserved words in the languages pw_rpc supports (e.g. class).

pw_rpc does not reserve any service names, but the restriction of avoiding reserved words in supported languages applies.

Service naming style#

pw_rpc service names should use capitalized camel case and should not use the term “Service”. Appending “Service” to a service name is redundant, similar to appending “Class” or “Function” to a class or function name. The C++ implementation class may use “Service” in its name, however.

For example, a service for accessing a file system should simply be named service FileSystem, rather than service FileSystemService, in the .proto file.

// file.proto
package pw.file;

service FileSystem {
    rpc List(ListRequest) returns (stream ListResponse);
}

The C++ service implementation class may append “Service” to the name.

// file_system_service.h
#include "pw_file/file.raw_rpc.pb.h"

namespace pw::file {

class FileSystemService : public pw_rpc::raw::FileSystem::Service<FileSystemService> {
  void List(ConstByteSpan request, RawServerWriter& writer);
};

}  // namespace pw::file

Roadmap#

Concurrent requests were not initially supported in pw_rpc (added in C++, Python, and TypeScript). As a result, some user-written service implementations may not expect or correctly support concurrent requests.