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]: