pw_rpc: Generate RPC definition stubs separately
- Define an interface for generating stubs.
- Generate the method implementation stubs outside the class.
- Share more code between raw and Nanopb RPC stub generation.
- Skip empty names in the C++ namespace to avoid outputting "::".
Change-Id: Ic00b81ae5af07e89408962454432fa227033b904
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/39201
Commit-Queue: Wyatt Hepler <hepler@google.com>
Pigweed-Auto-Submit: Wyatt Hepler <hepler@google.com>
Reviewed-by: Alexei Frolov <frolv@google.com>
diff --git a/pw_protobuf/py/pw_protobuf/proto_tree.py b/pw_protobuf/py/pw_protobuf/proto_tree.py
index de5a4e5..6dedf82 100644
--- a/pw_protobuf/py/pw_protobuf/proto_tree.py
+++ b/pw_protobuf/py/pw_protobuf/proto_tree.py
@@ -67,8 +67,8 @@
def cpp_namespace(self, root: Optional['ProtoNode'] = None) -> str:
"""C++ namespace of the node, up to the specified root."""
- return '::'.join(
- self._attr_hierarchy(lambda node: node.cpp_name(), root))
+ return '::'.join(name for name in self._attr_hierarchy(
+ lambda node: node.cpp_name(), root) if name)
def proto_path(self) -> str:
"""Fully-qualified package path of the node."""
diff --git a/pw_rpc/docs.rst b/pw_rpc/docs.rst
index 8bd39b3..eca6870 100644
--- a/pw_rpc/docs.rst
+++ b/pw_rpc/docs.rst
@@ -136,6 +136,21 @@
reference or copy and paste these to get started with implementing a service.
These stub classes are generated at the bottom of the pw_rpc proto header.
+ To use the stubs, do the following:
+
+ #. Locate the generated RPC header in the build directory. For example:
+
+ .. code-block:: sh
+
+ find out/ -name <proto_name>.rpc.pb.h
+
+ #. Scroll to the bottom of the generated RPC header.
+ #. Copy the stub class declaration to a header file.
+ #. Copy the member function definitions to a source file.
+ #. Rename the class or change the namespace, if desired.
+ #. List these files in a build target with a dependency on the
+ ``pw_proto_library``.
+
A Nanopb implementation of this service would be as follows:
.. code-block:: cpp
diff --git a/pw_rpc/py/pw_rpc/codegen.py b/pw_rpc/py/pw_rpc/codegen.py
index 531a955..957a893 100644
--- a/pw_rpc/py/pw_rpc/codegen.py
+++ b/pw_rpc/py/pw_rpc/codegen.py
@@ -13,6 +13,7 @@
# the License.
"""Common RPC codegen utilities."""
+import abc
from datetime import datetime
import os
from typing import cast, Any, Callable, Iterable
@@ -170,7 +171,42 @@
output.write_line('};\n')
-StubFunction = Callable[[ProtoServiceMethod, OutputFile], None]
+class StubGenerator(abc.ABC):
+ @abc.abstractmethod
+ def unary_signature(self, method: ProtoServiceMethod, prefix: str) -> str:
+ """Returns the signature of this unary method."""
+
+ @abc.abstractmethod
+ def unary_stub(self, method: ProtoServiceMethod,
+ output: OutputFile) -> None:
+ """Returns the stub for this unary method."""
+
+ @abc.abstractmethod
+ def server_streaming_signature(self, method: ProtoServiceMethod,
+ prefix: str) -> str:
+ """Returns the signature of this server streaming method."""
+
+ def server_streaming_stub( # pylint: disable=no-self-use
+ self, unused_method: ProtoServiceMethod,
+ output: OutputFile) -> None:
+ """Returns the stub for this server streaming method."""
+ output.write_line(STUB_REQUEST_TODO)
+ output.write_line('static_cast<void>(request);')
+ output.write_line(STUB_WRITER_TODO)
+ output.write_line('static_cast<void>(writer);')
+
+
+def _select_stub_methods(generator: StubGenerator, method: ProtoServiceMethod):
+ if method.type() is ProtoServiceMethod.Type.UNARY:
+ return generator.unary_signature, generator.unary_stub
+
+ if method.type() is ProtoServiceMethod.Type.SERVER_STREAMING:
+ return (generator.server_streaming_signature,
+ generator.server_streaming_stub)
+
+ raise NotImplementedError(
+ 'Client and bidirectional streaming not yet implemented')
+
_STUBS_COMMENT = r'''
/*
@@ -194,36 +230,51 @@
def package_stubs(proto_package: ProtoNode, output: OutputFile,
- unary_stub: StubFunction,
- server_streaming_stub: StubFunction) -> None:
+ stub_generator: StubGenerator) -> None:
+ """Generates the RPC stubs for a package."""
+ if proto_package.cpp_namespace():
+ file_ns = proto_package.cpp_namespace()
+ if file_ns.startswith('::'):
+ file_ns = file_ns[2:]
+
+ start_ns = lambda: output.write_line(f'namespace {file_ns} {{\n')
+ finish_ns = lambda: output.write_line(f'}} // namespace {file_ns}\n')
+ else:
+ start_ns = finish_ns = lambda: None
+
+ services = [
+ cast(ProtoService, node) for node in proto_package
+ if node.type() == ProtoNode.Type.SERVICE
+ ]
output.write_line('#ifdef _PW_RPC_COMPILE_GENERATED_SERVICE_STUBS')
output.write_line(_STUBS_COMMENT)
output.write_line(f'#include "{output.name()}"\n')
- if proto_package.cpp_namespace():
- file_namespace = proto_package.cpp_namespace()
- if file_namespace.startswith('::'):
- file_namespace = file_namespace[2:]
+ start_ns()
- output.write_line(f'namespace {file_namespace} {{')
+ for node in services:
+ _generate_service_class(node, output, stub_generator)
- for node in proto_package:
- if node.type() == ProtoNode.Type.SERVICE:
- _generate_service_stub(cast(ProtoService, node), output,
- unary_stub, server_streaming_stub)
-
- if proto_package.cpp_namespace():
- output.write_line(f'}} // namespace {file_namespace}')
-
- output.write_line('\n#endif // _PW_RPC_COMPILE_GENERATED_SERVICE_STUBS')
-
-
-def _generate_service_stub(service: ProtoService, output: OutputFile,
- unary_stub: StubFunction,
- server_streaming_stub: StubFunction) -> None:
output.write_line()
+
+ finish_ns()
+
+ start_ns()
+
+ for node in services:
+ _generate_service_stubs(node, output, stub_generator)
+ output.write_line()
+
+ finish_ns()
+
+ output.write_line('#endif // _PW_RPC_COMPILE_GENERATED_SERVICE_STUBS')
+
+
+def _generate_service_class(service: ProtoService, output: OutputFile,
+ stub_generator: StubGenerator) -> None:
+ output.write_line(f'// Implementation class for {service.proto_path()}.')
output.write_line(
f'class {service.name()} '
f': public generated::{service.name()}<{service.name()}> {{')
@@ -239,12 +290,28 @@
else:
blank_line = True
- if method.type() is ProtoServiceMethod.Type.UNARY:
- unary_stub(method, output)
- elif method.type() is ProtoServiceMethod.Type.SERVER_STREAMING:
- server_streaming_stub(method, output)
- else:
- raise NotImplementedError(
- 'Client and bidirectional streaming not yet implemented')
+ signature, _ = _select_stub_methods(stub_generator, method)
+
+ output.write_line(signature(method, '') + ';')
output.write_line('};\n')
+
+
+def _generate_service_stubs(service: ProtoService, output: OutputFile,
+ stub_generator: StubGenerator) -> None:
+ output.write_line(f'// Method definitions for {service.proto_path()}.')
+
+ blank_line = False
+
+ for method in service.methods():
+ if blank_line:
+ output.write_line()
+ else:
+ blank_line = True
+
+ signature, stub = _select_stub_methods(stub_generator, method)
+
+ output.write_line(signature(method, f'{service.name()}::') + ' {')
+ with output.indent():
+ stub(method, output)
+ output.write_line('}')
diff --git a/pw_rpc/py/pw_rpc/codegen_nanopb.py b/pw_rpc/py/pw_rpc/codegen_nanopb.py
index 4127062..183856c 100644
--- a/pw_rpc/py/pw_rpc/codegen_nanopb.py
+++ b/pw_rpc/py/pw_rpc/codegen_nanopb.py
@@ -163,35 +163,26 @@
_generate_code_for_service, _generate_code_for_client)
-def _unary_stub(method: ProtoServiceMethod, output: OutputFile) -> None:
- output.write_line(f'pw::Status {method.name()}(ServerContext&, '
- f'const {method.request_type().nanopb_name()}& request, '
- f'{method.response_type().nanopb_name()}& response) {{')
+class StubGenerator(codegen.StubGenerator):
+ def unary_signature(self, method: ProtoServiceMethod, prefix: str) -> str:
+ return (f'pw::Status {prefix}{method.name()}(ServerContext&, '
+ f'const {method.request_type().nanopb_name()}& request, '
+ f'{method.response_type().nanopb_name()}& response)')
- with output.indent():
+ def unary_stub(self, method: ProtoServiceMethod,
+ output: OutputFile) -> None:
output.write_line(codegen.STUB_REQUEST_TODO)
output.write_line('static_cast<void>(request);')
output.write_line(codegen.STUB_RESPONSE_TODO)
output.write_line('static_cast<void>(response);')
output.write_line('return pw::Status::Unimplemented();')
- output.write_line('}')
-
-
-def _server_streaming_stub(method: ProtoServiceMethod,
- output: OutputFile) -> None:
- output.write_line(
- f'void {method.name()}(ServerContext&, '
- f'const {method.request_type().nanopb_name()}& request, '
- f'ServerWriter<{method.response_type().nanopb_name()}>& writer) {{')
-
- with output.indent():
- output.write_line(codegen.STUB_REQUEST_TODO)
- output.write_line('static_cast<void>(request);')
- output.write_line(codegen.STUB_WRITER_TODO)
- output.write_line('static_cast<void>(writer);')
-
- output.write_line('}')
+ def server_streaming_signature(self, method: ProtoServiceMethod,
+ prefix: str) -> str:
+ return (
+ f'void {prefix}{method.name()}(ServerContext&, '
+ f'const {method.request_type().nanopb_name()}& request, '
+ f'ServerWriter<{method.response_type().nanopb_name()}>& writer)')
def process_proto_file(proto_file) -> Iterable[OutputFile]:
@@ -203,7 +194,6 @@
_generate_code_for_package(proto_file, package_root, output_file)
output_file.write_line()
- codegen.package_stubs(package_root, output_file, _unary_stub,
- _server_streaming_stub)
+ codegen.package_stubs(package_root, output_file, StubGenerator())
return [output_file]
diff --git a/pw_rpc/py/pw_rpc/codegen_raw.py b/pw_rpc/py/pw_rpc/codegen_raw.py
index 8155b41..51fee92 100644
--- a/pw_rpc/py/pw_rpc/codegen_raw.py
+++ b/pw_rpc/py/pw_rpc/codegen_raw.py
@@ -77,32 +77,24 @@
_generate_code_for_service, _generate_code_for_client)
-def _unary_stub(method: ProtoServiceMethod, output: OutputFile) -> None:
- output.write_line(f'pw::StatusWithSize {method.name()}(ServerContext&, '
- 'pw::ConstByteSpan request, pw::ByteSpan response) {')
+class StubGenerator(codegen.StubGenerator):
+ def unary_signature(self, method: ProtoServiceMethod, prefix: str) -> str:
+ return (f'pw::StatusWithSize {prefix}{method.name()}(ServerContext&, '
+ 'pw::ConstByteSpan request, pw::ByteSpan response)')
- with output.indent():
+ def unary_stub(self, method: ProtoServiceMethod,
+ output: OutputFile) -> None:
output.write_line(codegen.STUB_REQUEST_TODO)
output.write_line('static_cast<void>(request);')
output.write_line(codegen.STUB_RESPONSE_TODO)
output.write_line('static_cast<void>(response);')
output.write_line('return pw::StatusWithSize::Unimplemented();')
- output.write_line('}')
+ def server_streaming_signature(self, method: ProtoServiceMethod,
+ prefix: str) -> str:
-
-def _server_streaming_stub(method: ProtoServiceMethod,
- output: OutputFile) -> None:
- output.write_line(f'void {method.name()}(ServerContext&, '
- 'pw::ConstByteSpan request, RawServerWriter& writer) {')
-
- with output.indent():
- output.write_line(codegen.STUB_REQUEST_TODO)
- output.write_line('static_cast<void>(request);')
- output.write_line(codegen.STUB_WRITER_TODO)
- output.write_line('static_cast<void>(writer);')
-
- output.write_line('}')
+ return (f'void {prefix}{method.name()}(ServerContext&, '
+ 'pw::ConstByteSpan request, RawServerWriter& writer)')
def process_proto_file(proto_file) -> Iterable[OutputFile]:
@@ -114,7 +106,6 @@
_generate_code_for_package(proto_file, package_root, output_file)
output_file.write_line()
- codegen.package_stubs(package_root, output_file, _unary_stub,
- _server_streaming_stub)
+ codegen.package_stubs(package_root, output_file, StubGenerator())
return [output_file]