pw_rpc: nanopb codegen improvements
- Place generated services into a generated:: namespace, allowing
derived classes to reuse the service name.
- Update generated headers from <file>_rpc.pb.h to <file>.rpc.pb.h.
- Add documentation about the nanopb generated code API.
Change-Id: Ibd6399cc23c98a1c25ad4018cb62216478ddd183
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/14382
Reviewed-by: Wyatt Hepler <hepler@google.com>
Commit-Queue: Alexei Frolov <frolv@google.com>
diff --git a/pw_protobuf_compiler/proto.gni b/pw_protobuf_compiler/proto.gni
index 935f081..373f5ef 100644
--- a/pw_protobuf_compiler/proto.gni
+++ b/pw_protobuf_compiler/proto.gni
@@ -114,7 +114,7 @@
_outputs = []
foreach(_proto, _relative_proto_paths) {
- _output_h = string_replace(_proto, ".proto", "_rpc.pb.h")
+ _output_h = string_replace(_proto, ".proto", ".rpc.pb.h")
_outputs += [ "$_proto_gen_dir/$_output_h" ]
}
diff --git a/pw_rpc/nanopb/codegen_test.cc b/pw_rpc/nanopb/codegen_test.cc
index 5a717fa..dfb0f73 100644
--- a/pw_rpc/nanopb/codegen_test.cc
+++ b/pw_rpc/nanopb/codegen_test.cc
@@ -15,12 +15,12 @@
#include "gtest/gtest.h"
#include "pw_rpc/internal/hash.h"
#include "pw_rpc/test_method_context.h"
-#include "pw_rpc_test_protos/test_rpc.pb.h"
+#include "pw_rpc_test_protos/test.rpc.pb.h"
namespace pw::rpc {
namespace test {
-class TestServiceImpl : public TestService<TestServiceImpl> {
+class TestService final : public generated::TestService<TestService> {
public:
Status TestRpc(ServerContext&,
const pw_rpc_test_TestRequest& request,
@@ -46,13 +46,13 @@
namespace {
TEST(NanopbCodegen, CompilesProperly) {
- test::TestServiceImpl service;
+ test::TestService service;
EXPECT_EQ(service.id(), Hash("pw.rpc.test.TestService"));
EXPECT_STREQ(service.name(), "TestService");
}
TEST(NanopbCodegen, InvokeUnaryRpc) {
- PW_RPC_TEST_METHOD_CONTEXT(test::TestServiceImpl, TestRpc) context;
+ PW_RPC_TEST_METHOD_CONTEXT(test::TestService, TestRpc) context;
EXPECT_EQ(Status::OK,
context.call({.integer = 123, .status_code = Status::OK}));
@@ -66,7 +66,7 @@
}
TEST(NanopbCodegen, InvokeStreamingRpc) {
- PW_RPC_TEST_METHOD_CONTEXT(test::TestServiceImpl, TestStreamRpc) context;
+ PW_RPC_TEST_METHOD_CONTEXT(test::TestService, TestStreamRpc) context;
context.call({.integer = 0, .status_code = Status::ABORTED});
@@ -88,7 +88,7 @@
}
TEST(NanopbCodegen, InvokeStreamingRpc_ContextKeepsFixedNumberOfResponses) {
- PW_RPC_TEST_METHOD_CONTEXT(test::TestServiceImpl, TestStreamRpc, 3) context;
+ PW_RPC_TEST_METHOD_CONTEXT(test::TestService, TestStreamRpc, 3) context;
ASSERT_EQ(3u, context.responses().max_size());
diff --git a/pw_rpc/nanopb/docs.rst b/pw_rpc/nanopb/docs.rst
index 99f88ec..32f87fb 100644
--- a/pw_rpc/nanopb/docs.rst
+++ b/pw_rpc/nanopb/docs.rst
@@ -7,7 +7,147 @@
------
nanopb
------
+``pw_rpc`` can generate services which encode/decode RPC requests and responses
+as nanopb message structs.
-.. admonition:: TODO
+Usage
+=====
+To enable nanopb code generation, add ``nanopb_rpc`` as a generator to your
+Pigweed target's ``pw_protobuf_GENERATORS`` list. Refer to
+:ref:`chapter-pw-protobuf-compiler` for additional information.
- Document the nanopb generated code API
+.. code::
+
+ # my_target/target_toolchains.gni
+
+ defaults = {
+ pw_protobuf_GENERATORS = [
+ "pwpb",
+ "nanopb_rpc", # Enable RPC codegen
+ ]
+ }
+
+Define a ``pw_proto_library`` containing the .proto file defining your service
+(and optionally other related protos), then depend on the ``_nanopb_rpc``
+version of that library in the code implementing the service.
+
+.. code::
+
+ # 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 ChatService.
+ pw_source_set("chat_service") {
+ sources = [
+ "chat_service.cc",
+ "chat_service.h",
+ ]
+ public_deps = [ ":chat_protos_nanopb_rpc" ]
+ }
+
+A C++ header file is generated for each input .proto file, with the ``.proto``
+extension replaced by ``.rpc.pb.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.pb.h"``.
+
+Generated code API
+==================
+Take the following RPC service as an example.
+
+.. code:: protobuf
+
+ // chat/chat_protos/chat_service.proto
+
+ syntax = "proto3";
+
+ service ChatService {
+ // Returns information about a chatroom.
+ rpc GetRoomInformation(RoomInfoRequest) returns (RoomInfoResponse) {}
+
+ // Lists all of the users in a chatroom. The response is streamed as there
+ // may be a large amount of users.
+ 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 Chat(stream ChatMessage) returns (stream ChatMessage) {}
+ }
+
+Server-side
+-----------
+A C++ class is generated for each service in the .proto file. The class is
+located within a special ``generated`` 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.
+
+.. code:: c++
+
+ #include "chat_protos/chat_service.rpc.pb.h"
+
+ class ChatService final : public generated::ChatService<ChatService> {
+ public:
+ // Implementations of the service's RPC methods; see below.
+ };
+
+Unary RPC
+^^^^^^^^^
+A unary RPC is implemented as a function which takes in the RPC's request struct
+and populates a response struct to send back, with a status indicating whether
+the request succeeded.
+
+.. code:: c++
+
+ pw::Status GetRoomInformation(pw::rpc::ServerContext& ctx,
+ const RoomInfoRequest& request,
+ RoomInfoResponse& response);
+
+Server streaming RPC
+^^^^^^^^^^^^^^^^^^^^
+A server streaming RPC receives the client's request message alongside a
+``ServerWriter``, used to stream back responses.
+
+.. code:: c++
+
+ void ListUsersInRoom(pw::rpc::ServerContext& ctx,
+ const ListUsersRequest& request,
+ pw::rpc::ServerWriter<ListUsersResponse>& writer);
+
+The ``ServerWriter`` object is movable, and remains active until it is manually
+closed or goes out of scope. The writer has a simple API to return responses:
+
+.. cpp:function:: Status ServerWriter::Write(const T& response)
+
+ Writes a single response message to the stream. The returned status indicates
+ whether the write was successful.
+
+.. cpp:function:: void ServerWriter::Finish(Status status = Status::OK)
+
+ Closes the stream and sends back the RPC's overall status to the client.
+
+Once a ``ServerWriter`` has been closed, all future ``Write`` calls will fail.
+
+.. attention::
+
+ Make sure to use ``std::move`` when passing the ``ServerWriter`` around to
+ avoid accidentally closing it and ending the RPC.
+
+Client streaming RPC
+^^^^^^^^^^^^^^^^^^^^
+.. attention::
+
+ ``pw_rpc`` does not yet support client streaming RPCs.
+
+Bidirectional streaming RPC
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. attention::
+
+ ``pw_rpc`` does not yet support bidirectional streaming RPCs.
diff --git a/pw_rpc/nanopb/echo_service_test.cc b/pw_rpc/nanopb/echo_service_test.cc
index 54b4dda..490ebff 100644
--- a/pw_rpc/nanopb/echo_service_test.cc
+++ b/pw_rpc/nanopb/echo_service_test.cc
@@ -20,13 +20,13 @@
namespace {
TEST(EchoService, Echo_EchoesRequestMessage) {
- PW_RPC_TEST_METHOD_CONTEXT(EchoServiceImpl, Echo) context;
+ PW_RPC_TEST_METHOD_CONTEXT(EchoService, Echo) context;
ASSERT_EQ(context.call({.msg = "Hello, world"}), Status::OK);
EXPECT_STREQ(context.response().msg, "Hello, world");
}
TEST(EchoService, Echo_EmptyRequest) {
- PW_RPC_TEST_METHOD_CONTEXT(EchoServiceImpl, Echo) context;
+ PW_RPC_TEST_METHOD_CONTEXT(EchoService, Echo) context;
ASSERT_EQ(context.call({.msg = ""}), Status::OK);
EXPECT_STREQ(context.response().msg, "");
}
diff --git a/pw_rpc/nanopb/public/pw_rpc/echo_service_nanopb.h b/pw_rpc/nanopb/public/pw_rpc/echo_service_nanopb.h
index 938a9d9..0ce388c 100644
--- a/pw_rpc/nanopb/public/pw_rpc/echo_service_nanopb.h
+++ b/pw_rpc/nanopb/public/pw_rpc/echo_service_nanopb.h
@@ -15,11 +15,11 @@
#include <cstring>
-#include "pw_rpc_protos/echo_rpc.pb.h"
+#include "pw_rpc_protos/echo.rpc.pb.h"
namespace pw::rpc {
-class EchoServiceImpl : public EchoService<EchoServiceImpl> {
+class EchoService final : public generated::EchoService<EchoService> {
public:
Status Echo(ServerContext&,
const pw_rpc_EchoMessage& request,
diff --git a/pw_rpc/py/pw_rpc/codegen_nanopb.py b/pw_rpc/py/pw_rpc/codegen_nanopb.py
index 6e8b997..c8db83e 100644
--- a/pw_rpc/py/pw_rpc/codegen_nanopb.py
+++ b/pw_rpc/py/pw_rpc/codegen_nanopb.py
@@ -40,7 +40,7 @@
def _proto_filename_to_generated_header(proto_file: str) -> str:
"""Returns the generated C++ RPC header name for a .proto file."""
filename = os.path.splitext(proto_file)[0]
- return f'{filename}_rpc{PROTO_H_EXTENSION}'
+ return f'{filename}.rpc{PROTO_H_EXTENSION}'
def _generate_method_descriptor(method: ProtoServiceMethod,
@@ -201,21 +201,25 @@
output.write_line('namespace pw::rpc::test_internal {\n')
output.write_line('template <typename, uint32_t>')
output.write_line('class ServiceTestUtilities;')
- output.write_line('\n} // namespace pw::rpc::test_internal')
+ output.write_line('\n} // namespace pw::rpc::test_internal\n')
if package.cpp_namespace():
file_namespace = package.cpp_namespace()
if file_namespace.startswith('::'):
file_namespace = file_namespace[2:]
- output.write_line(f'\nnamespace {file_namespace} {{')
+ output.write_line(f'namespace {file_namespace} {{')
+
+ output.write_line('namespace generated {')
for node in package:
if node.type() == ProtoNode.Type.SERVICE:
_generate_code_for_service(node, package, output)
+ output.write_line('\n} // namespace generated')
+
if package.cpp_namespace():
- output.write_line(f'\n}} // namespace {file_namespace}')
+ output.write_line(f'}} // namespace {file_namespace}')
def process_proto_file(proto_file) -> Iterable[OutputFile]: