pw_rpc: Generate service implementation stubs
- Generate a service implementation stub in the RPC proto header. This
can be used as a reference or copied when implementing a service.
- Expand the documentation for code generation.
- Reduce code duplication in codegen.
- Remove codegen_test.py, which was not running because of a missing
unittest.main() line. The test was intended as an end-to-end test for
RPC codegen, but it was out of date and is no longer necessary because
the C++ codegen tests cover it.
Change-Id: Ie63f75da523b8746a849909d00f6a66c37767f40
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/26161
Commit-Queue: Wyatt Hepler <hepler@google.com>
Reviewed-by: Keir Mierle <keir@google.com>
diff --git a/pw_rpc/BUILD b/pw_rpc/BUILD
index 244ce00..bccb767 100644
--- a/pw_rpc/BUILD
+++ b/pw_rpc/BUILD
@@ -119,6 +119,7 @@
"nanopb/public/pw_rpc/nanopb_client_call.h",
"nanopb/public/pw_rpc/nanopb_test_method_context.h",
"nanopb/pw_rpc_nanopb_private/internal_test_utils.h",
+ "nanopb/stub_generation_test.cc",
],
)
diff --git a/pw_rpc/docs.rst b/pw_rpc/docs.rst
index 0d5d029..47eb235 100644
--- a/pw_rpc/docs.rst
+++ b/pw_rpc/docs.rst
@@ -56,21 +56,51 @@
sources = [ "foo_bar/the_service.proto" ]
}
-2. RPC service definition
--------------------------
-``pw_rpc`` generates a C++ base class for each RPC service declared in a .proto
-file. The serivce class is implemented by inheriting from this generated base
-and defining a method for each RPC.
+2. RPC code generation
+----------------------
+``pw_rpc`` generates a C++ header file for each ``.proto`` file. This header is
+generated in the build output directory. Its exact location varies by build
+system and toolchain, but the C++ include path always matches the sources
+declaration in the ``pw_proto_library``. The ``.proto`` extension is replaced
+with an extension corresponding to the protobuf library in use.
-A service named ``TheService`` in package ``foo.bar`` will generate the
-following class:
+================== =============== =============== =============
+Protobuf libraries Build subtarget Protobuf header pw_rpc header
+================== =============== =============== =============
+Raw only .raw_rpc (none) .raw_rpc.pb.h
+Nanopb or raw .nanopb_rpc .pb.h .rpc.pb.h
+pw_protobuf or raw .pwpb_rpc .pwpb.h .rpc.pwpb.h
+================== =============== =============== =============
+
+For example, the generated RPC header for ``"foo_bar/the_service.proto"`` is
+``"foo_bar/the_service.rpc.pb.h"`` for Nanopb or
+``"foo_bar/the_service.raw_rpc.pb.h"`` for raw RPCs.
+
+The generated header defines a base class for each RPC service declared in the
+``.proto`` file. A service named ``TheService`` in package ``foo.bar`` would
+generate the following base class:
.. cpp:class:: template <typename Implementation> foo::bar::generated::TheService
+3. RPC service definition
+-------------------------
+The serivce class is implemented by inheriting from the generated RPC service
+base class and defining a method for each RPC. The methods must match the name
+and function signature for one of the supported protobuf implementations.
+Services may mix and match protobuf implementations within one service.
+
+.. tip::
+
+ The generated code includes RPC service implementation stubs. You can
+ 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.
+
A Nanopb implementation of this service would be as follows:
.. code-block:: cpp
+ #include "foo_bar/the_service.rpc.pb.h"
+
namespace foo::bar {
class TheService : public generated::TheService<TheService> {
@@ -79,7 +109,7 @@
const foo_bar_Request& request,
foo_bar_Response& response) {
// implementation
- return pw::Status::OK;
+ return pw::Status::Ok();
}
void MethodTwo(ServerContext& ctx,
@@ -103,7 +133,7 @@
pw_source_set("the_service") {
public_configs = [ ":public" ]
public = [ "public/foo_bar/service.h" ]
- public_deps = [ ":the_service_proto_nanopb_rpc" ]
+ public_deps = [ ":the_service_proto.nanopb_rpc" ]
}
.. attention::
@@ -111,7 +141,7 @@
pw_rpc's generated classes will support using ``pw_protobuf`` or raw buffers
(no protobuf library) in the future.
-3. Register the service with a server
+4. Register the service with a server
-------------------------------------
This example code sets up an RPC server with an :ref:`HDLC<module-pw_hdlc_lite>`
channel output and the example service.
diff --git a/pw_rpc/nanopb/BUILD.gn b/pw_rpc/nanopb/BUILD.gn
index 97b97a1..3b76625 100644
--- a/pw_rpc/nanopb/BUILD.gn
+++ b/pw_rpc/nanopb/BUILD.gn
@@ -102,6 +102,7 @@
":method_lookup_test",
":nanopb_method_test",
":nanopb_method_union_test",
+ ":stub_generation_test",
]
}
@@ -173,3 +174,9 @@
sources = [ "echo_service_test.cc" ]
enable_if = dir_pw_third_party_nanopb != ""
}
+
+pw_test("stub_generation_test") {
+ deps = [ "..:test_protos.nanopb_rpc" ]
+ sources = [ "stub_generation_test.cc" ]
+ enable_if = dir_pw_third_party_nanopb != ""
+}
diff --git a/pw_rpc/nanopb/stub_generation_test.cc b/pw_rpc/nanopb/stub_generation_test.cc
new file mode 100644
index 0000000..18777bc
--- /dev/null
+++ b/pw_rpc/nanopb/stub_generation_test.cc
@@ -0,0 +1,29 @@
+// Copyright 2020 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+// This macro is used to remove the generated stubs from the proto files. Define
+// so that the generated stubs can be tested.
+#define _PW_RPC_COMPILE_GENERATED_SERVICE_STUBS
+
+#include "gtest/gtest.h"
+#include "pw_rpc_test_protos/test.rpc.pb.h"
+
+namespace {
+
+TEST(NanopbServiceStub, GeneratedStubCompiles) {
+ ::pw::rpc::test::TestService test_service;
+ EXPECT_STREQ(test_service.name(), "TestService");
+}
+
+} // namespace
diff --git a/pw_rpc/py/BUILD.gn b/pw_rpc/py/BUILD.gn
index 03f7b29..6590476 100644
--- a/pw_rpc/py/BUILD.gn
+++ b/pw_rpc/py/BUILD.gn
@@ -36,7 +36,6 @@
tests = [
"callback_client_test.py",
"client_test.py",
- "codegen_test.py",
"ids_test.py",
"packets_test.py",
]
diff --git a/pw_rpc/py/codegen_test.py b/pw_rpc/py/codegen_test.py
deleted file mode 100644
index ee96a8c..0000000
--- a/pw_rpc/py/codegen_test.py
+++ /dev/null
@@ -1,181 +0,0 @@
-#!/usr/bin/env python3
-# Copyright 2020 The Pigweed Authors
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may not
-# use this file except in compliance with the License. You may obtain a copy of
-# the License at
-#
-# https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations under
-# the License.
-"""Tests the generated pw_rpc code."""
-
-from pathlib import Path
-import os
-import subprocess
-import tempfile
-import unittest
-
-TEST_PROTO_FILE = b"""\
-syntax = "proto3";
-
-package pw.rpc.test;
-
-message TestRequest {
- float integer = 1;
-}
-
-message TestResponse {
- int32 value = 1;
-}
-
-message TestStreamResponse {
- bytes chunk = 1;
-}
-
-message Empty {}
-
-service TestService {
- rpc TestRpc(TestRequest) returns (TestResponse) {}
- rpc TestStreamRpc(Empty) returns (stream TestStreamResponse) {}
-}
-"""
-
-EXPECTED_NANOPB_CODE = """\
-#pragma once
-
-#include <array>
-#include <cstddef>
-#include <cstdint>
-#include <type_traits>
-
-#include "pw_rpc/internal/method.h"
-#include "pw_rpc/server_context.h"
-#include "pw_rpc/service.h"
-#include "test.pb.h"
-
-namespace pw::rpc::internal {
-
-template <auto>
-class ServiceMethodTraits;
-
-} // namespace pw::rpc::internal
-
-namespace pw::rpc::test {
-namespace generated {
-
-template <typename Implementation>
-class TestService : public ::pw::rpc::Service {
- public:
- using ServerContext = ::pw::rpc::ServerContext;
- template <typename T>
- using ServerWriter = ::pw::rpc::ServerWriter<T>;
-
- constexpr TestService()
- : ::pw::rpc::Service(kServiceId, kMethods) {}
-
- TestService(const TestService&) = delete;
- TestService& operator=(const TestService&) = delete;
-
- static constexpr const char* name() { return "TestService"; }
-
- // Used by ServiceMethodTraits to identify a base service.
- constexpr void _PwRpcInternalGeneratedBase() const {}
-
- private:
- // Hash of "pw.rpc.test.TestService".
- static constexpr uint32_t kServiceId = 0xcc0f6de0;
-
- static ::pw::Status Invoke_TestRpc(
- ::pw::rpc::internal::ServerCall& call,
- const pw_rpc_test_TestRequest& request,
- pw_rpc_test_TestResponse& response) {
- return static_cast<Implementation&>(call.service())
- .TestRpc(call.context(), request, response);
- }
-
- static void Invoke_TestStreamRpc(
- ::pw::rpc::internal::ServerCall& call,
- const pw_rpc_test_TestRequest& request,
- ServerWriter<pw_rpc_test_TestStreamResponse>& writer) {
- static_cast<Implementation&>(call.service())
- .TestStreamRpc(call.context(), request, writer);
- }
-
- static constexpr std::array<::pw::rpc::internal::Method, 2> kMethods = {
- ::pw::rpc::internal::Method::Unary<Invoke_TestRpc>(
- 0xbc924054, // Hash of "TestRpc"
- pw_rpc_test_TestRequest_fields,
- pw_rpc_test_TestResponse_fields),
- ::pw::rpc::internal::Method::ServerStreaming<Invoke_TestStreamRpc>(
- 0xd97a28fa, // Hash of "TestStreamRpc"
- pw_rpc_test_TestRequest_fields,
- pw_rpc_test_TestStreamResponse_fields),
- };
-
- template <auto impl_method>
- static constexpr const ::pw::rpc::internal::Method* MethodFor() {
- if constexpr (std::is_same_v<decltype(impl_method), decltype(&Implementation::TestRpc)>) {
- if constexpr (impl_method == &Implementation::TestRpc) {
- return &std::get<0>(kMethods);
- }
- }
- if constexpr (std::is_same_v<decltype(impl_method), decltype(&Implementation::TestStreamRpc)>) {
- if constexpr (impl_method == &Implementation::TestStreamRpc) {
- return &std::get<1>(kMethods);
- }
- }
- return nullptr;
- }
-
- template <auto>
- friend class ::pw::rpc::internal::ServiceMethodTraits;
-};
-
-} // namespace generated
-} // namespace pw::rpc::test
-"""
-
-
-class TestNanopbCodegen(unittest.TestCase):
- """Test case for nanopb code generation."""
- def setUp(self):
- self._output_dir = tempfile.TemporaryDirectory()
-
- def tearDown(self):
- self._output_dir.cleanup()
-
- def test_nanopb_codegen(self):
- root = Path(os.getenv('PW_ROOT'))
- proto_dir = root / 'pw_rpc' / 'pw_rpc_test_protos'
- proto_file = proto_dir / 'test.proto'
-
- venv_bin = 'Scripts' if os.name == 'nt' else 'bin'
- plugin = root / '.python3-env' / venv_bin / 'pw_rpc_codegen'
-
- command = (
- 'protoc',
- f'-I{proto_dir}',
- proto_file,
- '--plugin',
- f'protoc-gen-custom={plugin}',
- '--custom_out',
- self._output_dir.name,
- )
-
- subprocess.run(command)
-
- generated_files = os.listdir(self._output_dir.name)
- self.assertEqual(len(generated_files), 1)
- self.assertEqual(generated_files[0], 'test.rpc.pb.h')
-
- # Read the generated file, ignoring its preamble.
- generated_code = Path(self._output_dir.name,
- generated_files[0]).read_text()
- generated_code = generated_code[generated_code.index('#pragma'):]
-
- self.assertEqual(generated_code, EXPECTED_NANOPB_CODE)
diff --git a/pw_rpc/py/pw_rpc/codegen.py b/pw_rpc/py/pw_rpc/codegen.py
index 7dd5451..531a955 100644
--- a/pw_rpc/py/pw_rpc/codegen.py
+++ b/pw_rpc/py/pw_rpc/codegen.py
@@ -13,13 +13,150 @@
# the License.
"""Common RPC codegen utilities."""
+from datetime import datetime
+import os
+from typing import cast, Any, Callable, Iterable
+
from pw_protobuf.output_file import OutputFile
-from pw_protobuf.proto_tree import ProtoService
+from pw_protobuf.proto_tree import ProtoNode, ProtoService, ProtoServiceMethod
import pw_rpc.ids
+PLUGIN_NAME = 'pw_rpc_codegen'
+PLUGIN_VERSION = '0.2.0'
-def method_lookup_table(service: ProtoService, output: OutputFile) -> None:
+RPC_NAMESPACE = '::pw::rpc'
+
+STUB_REQUEST_TODO = (
+ '// TODO: Read the request as appropriate for your application')
+STUB_RESPONSE_TODO = (
+ '// TODO: Fill in the response as appropriate for your application')
+STUB_WRITER_TODO = (
+ '// TODO: Send responses with the writer as appropriate for your '
+ 'application')
+
+ServerWriterGenerator = Callable[[OutputFile], None]
+MethodGenerator = Callable[[ProtoServiceMethod, int, OutputFile], None]
+ServiceGenerator = Callable[[ProtoService, ProtoNode, OutputFile], None]
+IncludesGenerator = Callable[[Any, ProtoNode], Iterable[str]]
+
+
+def package(file_descriptor_proto, proto_package: ProtoNode,
+ output: OutputFile, includes: IncludesGenerator,
+ service: ServiceGenerator, client: ServiceGenerator) -> None:
+ """Generates service and client code for a package."""
+ assert proto_package.type() == ProtoNode.Type.PACKAGE
+
+ output.write_line(f'// {os.path.basename(output.name())} automatically '
+ f'generated by {PLUGIN_NAME} {PLUGIN_VERSION}')
+ output.write_line(f'// on {datetime.now().isoformat()}')
+ output.write_line('// clang-format off')
+ output.write_line('#pragma once\n')
+
+ output.write_line('#include <array>')
+ output.write_line('#include <cstdint>')
+ output.write_line('#include <type_traits>\n')
+
+ include_lines = [
+ '#include "pw_rpc/internal/method_lookup.h"',
+ '#include "pw_rpc/server_context.h"',
+ '#include "pw_rpc/service.h"',
+ ]
+ include_lines += includes(file_descriptor_proto, proto_package)
+
+ for include_line in sorted(include_lines):
+ output.write_line(include_line)
+
+ output.write_line()
+
+ if proto_package.cpp_namespace():
+ file_namespace = proto_package.cpp_namespace()
+ if file_namespace.startswith('::'):
+ file_namespace = file_namespace[2:]
+
+ output.write_line(f'namespace {file_namespace} {{')
+
+ for node in proto_package:
+ if node.type() == ProtoNode.Type.SERVICE:
+ service(cast(ProtoService, node), proto_package, output)
+ client(cast(ProtoService, node), proto_package, output)
+
+ if proto_package.cpp_namespace():
+ output.write_line(f'}} // namespace {file_namespace}')
+
+
+def service_class(service: ProtoService, root: ProtoNode, output: OutputFile,
+ server_writer_alias: ServerWriterGenerator,
+ method_union: str,
+ method_descriptor: MethodGenerator) -> None:
+ """Generates a C++ derived class for a nanopb RPC service."""
+
+ output.write_line('namespace generated {')
+
+ base_class = f'{RPC_NAMESPACE}::Service'
+ output.write_line('\ntemplate <typename Implementation>')
+ output.write_line(
+ f'class {service.cpp_namespace(root)} : public {base_class} {{')
+ output.write_line(' public:')
+
+ with output.indent():
+ output.write_line(
+ f'using ServerContext = {RPC_NAMESPACE}::ServerContext;')
+ server_writer_alias(output)
+ output.write_line()
+
+ output.write_line(f'constexpr {service.name()}()')
+ output.write_line(f' : {base_class}(kServiceId, kMethods) {{}}')
+
+ output.write_line()
+ output.write_line(
+ f'{service.name()}(const {service.name()}&) = delete;')
+ output.write_line(f'{service.name()}& operator='
+ f'(const {service.name()}&) = delete;')
+
+ output.write_line()
+ output.write_line(f'static constexpr const char* name() '
+ f'{{ return "{service.name()}"; }}')
+
+ output.write_line()
+ output.write_line(
+ '// Used by MethodLookup to identify the generated service base.')
+ output.write_line(
+ 'constexpr void _PwRpcInternalGeneratedBase() const {}')
+
+ service_name_hash = pw_rpc.ids.calculate(service.proto_path())
+ output.write_line('\n private:')
+
+ with output.indent():
+ output.write_line('friend class ::pw::rpc::internal::MethodLookup;\n')
+ output.write_line(f'// Hash of "{service.proto_path()}".')
+ output.write_line(
+ f'static constexpr uint32_t kServiceId = 0x{service_name_hash:08x};'
+ )
+
+ output.write_line()
+
+ # Generate the method table
+ output.write_line('static constexpr std::array<'
+ f'{RPC_NAMESPACE}::internal::{method_union},'
+ f' {len(service.methods())}> kMethods = {{')
+
+ with output.indent(4):
+ for method in service.methods():
+ method_descriptor(method, pw_rpc.ids.calculate(method.name()),
+ output)
+
+ output.write_line('};\n')
+
+ # Generate the method lookup table
+ _method_lookup_table(service, output)
+
+ output.write_line('};')
+
+ output.write_line('\n} // namespace generated\n')
+
+
+def _method_lookup_table(service: ProtoService, output: OutputFile) -> None:
"""Generates array of method IDs for looking up methods at compile time."""
output.write_line('static constexpr std::array<uint32_t, '
f'{len(service.methods())}> kMethodIds = {{')
@@ -28,6 +165,86 @@
for method in service.methods():
method_id = pw_rpc.ids.calculate(method.name())
output.write_line(
- f'0x{method_id:08x}, // Hash of {method.name()}')
+ f'0x{method_id:08x}, // Hash of "{method.name()}"')
+
+ output.write_line('};\n')
+
+
+StubFunction = Callable[[ProtoServiceMethod, OutputFile], None]
+
+_STUBS_COMMENT = r'''
+/*
+ ____ __ __ __ _
+ / _/___ ___ ____ / /__ ____ ___ ___ ____ / /_____ _/ /_(_)___ ____
+ / // __ `__ \/ __ \/ / _ \/ __ `__ \/ _ \/ __ \/ __/ __ `/ __/ / __ \/ __ \
+ _/ // / / / / / /_/ / / __/ / / / / / __/ / / / /_/ /_/ / /_/ / /_/ / / / /
+/___/_/ /_/ /_/ .___/_/\___/_/ /_/ /_/\___/_/ /_/\__/\__,_/\__/_/\____/_/ /_/
+ /_/
+ _____ __ __ __
+ / ___// /___ __/ /_ _____/ /
+ \__ \/ __/ / / / __ \/ ___/ /
+ ___/ / /_/ /_/ / /_/ (__ )_/
+/____/\__/\__,_/_.___/____(_)
+
+*/
+// This section provides stub implementations of the RPC services in this file.
+// The code below may be referenced or copied to serve as a starting point for
+// your RPC service implementations.
+'''
+
+
+def package_stubs(proto_package: ProtoNode, output: OutputFile,
+ unary_stub: StubFunction,
+ server_streaming_stub: StubFunction) -> None:
+
+ 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:]
+
+ output.write_line(f'namespace {file_namespace} {{')
+
+ 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()
+ output.write_line(
+ f'class {service.name()} '
+ f': public generated::{service.name()}<{service.name()}> {{')
+
+ output.write_line(' public:')
+
+ with output.indent():
+ blank_line = False
+
+ for method in service.methods():
+ if blank_line:
+ output.write_line()
+ 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')
output.write_line('};\n')
diff --git a/pw_rpc/py/pw_rpc/codegen_nanopb.py b/pw_rpc/py/pw_rpc/codegen_nanopb.py
index 2d0bd7d..c888365 100644
--- a/pw_rpc/py/pw_rpc/codegen_nanopb.py
+++ b/pw_rpc/py/pw_rpc/codegen_nanopb.py
@@ -13,25 +13,20 @@
# the License.
"""This module generates the code for nanopb-based pw_rpc services."""
-from datetime import datetime
import os
-from typing import Iterable, cast
+from typing import Iterable, Iterator
from pw_protobuf.output_file import OutputFile
from pw_protobuf.proto_tree import ProtoNode, ProtoService, ProtoServiceMethod
from pw_protobuf.proto_tree import build_node_tree
-import pw_rpc.ids
from pw_rpc import codegen
-
-PLUGIN_NAME = 'pw_rpc_codegen'
-PLUGIN_VERSION = '0.1.0'
+from pw_rpc.codegen import RPC_NAMESPACE
+import pw_rpc.ids
PROTO_H_EXTENSION = '.pb.h'
PROTO_CC_EXTENSION = '.pb.cc'
NANOPB_H_EXTENSION = '.pb.h'
-RPC_NAMESPACE = '::pw::rpc'
-
def _proto_filename_to_nanopb_header(proto_file: str) -> str:
"""Returns the generated nanopb header name for a .proto file."""
@@ -44,11 +39,10 @@
return f'{filename}.rpc{PROTO_H_EXTENSION}'
-def _generate_method_descriptor(method: ProtoServiceMethod,
+def _generate_method_descriptor(method: ProtoServiceMethod, method_id: int,
output: OutputFile) -> None:
"""Generates a nanopb method descriptor for an RPC method."""
- method_id = pw_rpc.ids.calculate(method.name())
req_fields = f'{method.request_type().nanopb_name()}_fields'
res_fields = f'{method.response_type().nanopb_name()}_fields'
impl_method = f'&Implementation::{method.name()}'
@@ -62,74 +56,16 @@
output.write_line(f'{res_fields}),')
+def _generate_server_writer_alias(output: OutputFile) -> None:
+ output.write_line('template <typename T>')
+ output.write_line('using ServerWriter = ::pw::rpc::ServerWriter<T>;')
+
+
def _generate_code_for_service(service: ProtoService, root: ProtoNode,
output: OutputFile) -> None:
"""Generates a C++ derived class for a nanopb RPC service."""
-
- output.write_line('namespace generated {')
-
- base_class = f'{RPC_NAMESPACE}::Service'
- output.write_line('\ntemplate <typename Implementation>')
- output.write_line(
- f'class {service.cpp_namespace(root)} : public {base_class} {{')
- output.write_line(' public:')
-
- with output.indent():
- output.write_line(
- f'using ServerContext = {RPC_NAMESPACE}::ServerContext;')
- output.write_line('template <typename T>')
- output.write_line(
- f'using ServerWriter = {RPC_NAMESPACE}::ServerWriter<T>;')
- output.write_line()
-
- output.write_line(f'constexpr {service.name()}()')
- output.write_line(f' : {base_class}(kServiceId, kMethods) {{}}')
-
- output.write_line()
- output.write_line(
- f'{service.name()}(const {service.name()}&) = delete;')
- output.write_line(f'{service.name()}& operator='
- f'(const {service.name()}&) = delete;')
-
- output.write_line()
- output.write_line(f'static constexpr const char* name() '
- f'{{ return "{service.name()}"; }}')
-
- output.write_line()
- output.write_line(
- '// Used by ServiceMethodTraits to identify a base service.')
- output.write_line(
- 'constexpr void _PwRpcInternalGeneratedBase() const {}')
-
- service_name_hash = pw_rpc.ids.calculate(service.proto_path())
- output.write_line('\n private:')
-
- with output.indent():
- output.write_line('friend class ::pw::rpc::internal::MethodLookup;\n')
- output.write_line(f'// Hash of "{service.proto_path()}".')
- output.write_line(
- f'static constexpr uint32_t kServiceId = 0x{service_name_hash:08x};'
- )
-
- output.write_line()
-
- # Generate the method table
- output.write_line('static constexpr std::array<'
- f'{RPC_NAMESPACE}::internal::NanopbMethodUnion,'
- f' {len(service.methods())}> kMethods = {{')
-
- with output.indent(4):
- for method in service.methods():
- _generate_method_descriptor(method, output)
-
- output.write_line('};\n')
-
- # Generate the method lookup table
- codegen.method_lookup_table(service, output)
-
- output.write_line('};')
-
- output.write_line('\n} // namespace generated\n')
+ codegen.service_class(service, root, output, _generate_server_writer_alias,
+ 'NanopbMethodUnion', _generate_method_descriptor)
def _generate_code_for_client_method(method: ProtoServiceMethod,
@@ -206,50 +142,54 @@
output.write_line('\n} // namespace nanopb\n')
-def generate_code_for_package(file_descriptor_proto, package: ProtoNode,
- output: OutputFile) -> None:
- """Generates code for a header file corresponding to a .proto file."""
-
- assert package.type() == ProtoNode.Type.PACKAGE
-
- output.write_line(f'// {os.path.basename(output.name())} automatically '
- f'generated by {PLUGIN_NAME} {PLUGIN_VERSION}')
- output.write_line(f'// on {datetime.now().isoformat()}')
- output.write_line('// clang-format off')
- output.write_line('#pragma once\n')
- output.write_line('#include <array>')
- output.write_line('#include <cstddef>')
- output.write_line('#include <cstdint>')
- output.write_line('#include <type_traits>\n')
- output.write_line('#include "pw_rpc/internal/method_lookup.h"')
- output.write_line('#include "pw_rpc/internal/nanopb_method_union.h"')
- output.write_line('#include "pw_rpc/nanopb_client_call.h"')
- output.write_line('#include "pw_rpc/server_context.h"')
- output.write_line('#include "pw_rpc/service.h"')
+def includes(proto_file, unused_package: ProtoNode) -> Iterator[str]:
+ yield '#include "pw_rpc/internal/nanopb_method_union.h"'
+ yield '#include "pw_rpc/nanopb_client_call.h"'
# Include the corresponding nanopb header file for this proto file, in which
# the file's messages and enums are generated. All other files imported from
# the .proto file are #included in there.
- nanopb_header = _proto_filename_to_nanopb_header(
- file_descriptor_proto.name)
- output.write_line(f'#include "{nanopb_header}"\n')
+ nanopb_header = _proto_filename_to_nanopb_header(proto_file.name)
+ yield f'#include "{nanopb_header}"'
- if package.cpp_namespace():
- file_namespace = package.cpp_namespace()
- if file_namespace.startswith('::'):
- file_namespace = file_namespace[2:]
- output.write_line(f'namespace {file_namespace} {{')
+def _generate_code_for_package(proto_file, package: ProtoNode,
+ output: OutputFile) -> None:
+ """Generates code for a header file corresponding to a .proto file."""
- for node in package:
- if node.type() == ProtoNode.Type.SERVICE:
- _generate_code_for_service(cast(ProtoService, node), package,
- output)
- _generate_code_for_client(cast(ProtoService, node), package,
- output)
+ codegen.package(proto_file, package, output, includes,
+ _generate_code_for_service, _generate_code_for_client)
- if package.cpp_namespace():
- output.write_line(f'}} // namespace {file_namespace}')
+
+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) {{')
+
+ with output.indent():
+ 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 process_proto_file(proto_file) -> Iterable[OutputFile]:
@@ -258,6 +198,10 @@
_, package_root = build_node_tree(proto_file)
output_filename = _proto_filename_to_generated_header(proto_file.name)
output_file = OutputFile(output_filename)
- generate_code_for_package(proto_file, package_root, output_file)
+ _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)
return [output_file]
diff --git a/pw_rpc/py/pw_rpc/codegen_raw.py b/pw_rpc/py/pw_rpc/codegen_raw.py
index 9b25bfe..8155b41 100644
--- a/pw_rpc/py/pw_rpc/codegen_raw.py
+++ b/pw_rpc/py/pw_rpc/codegen_raw.py
@@ -13,23 +13,17 @@
# the License.
"""This module generates the code for raw pw_rpc services."""
-from datetime import datetime
import os
-from typing import Iterable, cast
+from typing import Iterable
from pw_protobuf.output_file import OutputFile
from pw_protobuf.proto_tree import ProtoNode, ProtoService, ProtoServiceMethod
from pw_protobuf.proto_tree import build_node_tree
-import pw_rpc.ids
from pw_rpc import codegen
-
-PLUGIN_NAME = 'pw_rpc_codegen'
-PLUGIN_VERSION = '0.1.0'
+from pw_rpc.codegen import RPC_NAMESPACE
PROTO_H_EXTENSION = '.pb.h'
-RPC_NAMESPACE = '::pw::rpc'
-
def _proto_filename_to_generated_header(proto_file: str) -> str:
"""Returns the generated C++ RPC header name for a .proto file."""
@@ -37,11 +31,16 @@
return f'{filename}.raw_rpc{PROTO_H_EXTENSION}'
-def _generate_method_descriptor(method: ProtoServiceMethod,
+def _proto_filename_to_stub_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}.raw_rpc.stub{PROTO_H_EXTENSION}'
+
+
+def _generate_method_descriptor(method: ProtoServiceMethod, method_id: int,
output: OutputFile) -> None:
"""Generates a method descriptor for a raw RPC method."""
- method_id = pw_rpc.ids.calculate(method.name())
impl_method = f'&Implementation::{method.name()}'
output.write_line(
@@ -50,107 +49,60 @@
output.write_line(f' 0x{method_id:08x}), // Hash of "{method.name()}"')
+def _generate_server_writer_alias(output: OutputFile) -> None:
+ output.write_line(
+ f'using RawServerWriter = {RPC_NAMESPACE}::RawServerWriter;')
+
+
+def _generate_code_for_client(unused_service: ProtoService,
+ unused_root: ProtoNode,
+ output: OutputFile) -> None:
+ """Outputs client code for an RPC service."""
+ output.write_line('// Raw RPC clients are not yet implemented.\n')
+
+
def _generate_code_for_service(service: ProtoService, root: ProtoNode,
output: OutputFile) -> None:
"""Generates a C++ base class for a raw RPC service."""
-
- base_class = f'{RPC_NAMESPACE}::Service'
- output.write_line('\ntemplate <typename Implementation>')
- output.write_line(
- f'class {service.cpp_namespace(root)} : public {base_class} {{')
- output.write_line(' public:')
-
- with output.indent():
- output.write_line(
- f'using ServerContext = {RPC_NAMESPACE}::ServerContext;')
- output.write_line(
- f'using RawServerWriter = {RPC_NAMESPACE}::RawServerWriter;')
- output.write_line()
-
- output.write_line(f'constexpr {service.name()}()')
- output.write_line(f' : {base_class}(kServiceId, kMethods) {{}}')
-
- output.write_line()
- output.write_line(
- f'{service.name()}(const {service.name()}&) = delete;')
- output.write_line(f'{service.name()}& operator='
- f'(const {service.name()}&) = delete;')
-
- output.write_line()
- output.write_line(f'static constexpr const char* name() '
- f'{{ return "{service.name()}"; }}')
-
- output.write_line()
- output.write_line(
- '// Used by ServiceMethodTraits to identify a base service.')
- output.write_line(
- 'constexpr void _PwRpcInternalGeneratedBase() const {}')
-
- service_name_hash = pw_rpc.ids.calculate(service.proto_path())
- output.write_line('\n private:')
-
- with output.indent():
- output.write_line('friend class ::pw::rpc::internal::MethodLookup;\n')
- output.write_line(f'// Hash of "{service.proto_path()}".')
- output.write_line(
- f'static constexpr uint32_t kServiceId = 0x{service_name_hash:08x};'
- )
-
- output.write_line()
-
- # Generate the method table
- output.write_line('static constexpr std::array<'
- f'{RPC_NAMESPACE}::internal::RawMethodUnion,'
- f' {len(service.methods())}> kMethods = {{')
-
- with output.indent(4):
- for method in service.methods():
- _generate_method_descriptor(method, output)
-
- output.write_line('};')
-
- # Generate the method lookup table
- codegen.method_lookup_table(service, output)
-
- output.write_line('};')
+ codegen.service_class(service, root, output, _generate_server_writer_alias,
+ 'RawMethodUnion', _generate_method_descriptor)
-def _generate_code_for_package(package: ProtoNode, output: OutputFile) -> None:
+def _generate_code_for_package(proto_file, package: ProtoNode,
+ output: OutputFile) -> None:
"""Generates code for a header file corresponding to a .proto file."""
- assert package.type() == ProtoNode.Type.PACKAGE
+ includes = lambda *_: ['#include "pw_rpc/internal/raw_method_union.h"']
- output.write_line(f'// {os.path.basename(output.name())} automatically '
- f'generated by {PLUGIN_NAME} {PLUGIN_VERSION}')
- output.write_line(f'// on {datetime.now().isoformat()}')
- output.write_line('// clang-format off')
- output.write_line('#pragma once\n')
- output.write_line('#include <array>')
- output.write_line('#include <cstddef>')
- output.write_line('#include <cstdint>')
- output.write_line('#include <type_traits>\n')
- output.write_line('#include "pw_rpc/internal/method_lookup.h"')
- output.write_line('#include "pw_rpc/internal/raw_method_union.h"')
- output.write_line('#include "pw_rpc/server_context.h"')
- output.write_line('#include "pw_rpc/service.h"\n')
+ codegen.package(proto_file, package, output, includes,
+ _generate_code_for_service, _generate_code_for_client)
- if package.cpp_namespace():
- file_namespace = package.cpp_namespace()
- if file_namespace.startswith('::'):
- file_namespace = file_namespace[2:]
- output.write_line(f'namespace {file_namespace} {{')
+def _unary_stub(method: ProtoServiceMethod, output: OutputFile) -> None:
+ output.write_line(f'pw::StatusWithSize {method.name()}(ServerContext&, '
+ 'pw::ConstByteSpan request, pw::ByteSpan response) {')
- output.write_line('namespace generated {')
+ with output.indent():
+ 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();')
- for node in package:
- if node.type() == ProtoNode.Type.SERVICE:
- _generate_code_for_service(cast(ProtoService, node), package,
- output)
+ output.write_line('}')
- output.write_line('\n} // namespace generated')
- if package.cpp_namespace():
- output.write_line(f'}} // namespace {file_namespace}')
+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('}')
def process_proto_file(proto_file) -> Iterable[OutputFile]:
@@ -159,6 +111,10 @@
_, package_root = build_node_tree(proto_file)
output_filename = _proto_filename_to_generated_header(proto_file.name)
output_file = OutputFile(output_filename)
- _generate_code_for_package(package_root, output_file)
+ _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)
return [output_file]
diff --git a/pw_rpc/raw/BUILD b/pw_rpc/raw/BUILD
index 193ef17..d983b14 100644
--- a/pw_rpc/raw/BUILD
+++ b/pw_rpc/raw/BUILD
@@ -92,3 +92,10 @@
"//pw_rpc:internal_test_utils",
],
)
+
+pw_cc_test(
+ name = "stub_generation_test",
+ srcs = ["stub_generation_test.cc"],
+ # TODO(hepler): Figure out proto BUILD integration.
+ # deps = ["..:test_protos.raw_rpc"],
+)
diff --git a/pw_rpc/raw/BUILD.gn b/pw_rpc/raw/BUILD.gn
index e39214e..e2f4cfa 100644
--- a/pw_rpc/raw/BUILD.gn
+++ b/pw_rpc/raw/BUILD.gn
@@ -55,6 +55,7 @@
":codegen_test",
":raw_method_test",
":raw_method_union_test",
+ ":stub_generation_test",
]
}
@@ -89,3 +90,8 @@
]
sources = [ "raw_method_union_test.cc" ]
}
+
+pw_test("stub_generation_test") {
+ deps = [ "..:test_protos.raw_rpc" ]
+ sources = [ "stub_generation_test.cc" ]
+}
diff --git a/pw_rpc/raw/stub_generation_test.cc b/pw_rpc/raw/stub_generation_test.cc
new file mode 100644
index 0000000..dc67a02
--- /dev/null
+++ b/pw_rpc/raw/stub_generation_test.cc
@@ -0,0 +1,29 @@
+// Copyright 2020 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+// This macro is used to remove the generated stubs from the proto files. Define
+// so that the generated stubs can be tested.
+#define _PW_RPC_COMPILE_GENERATED_SERVICE_STUBS
+
+#include "gtest/gtest.h"
+#include "pw_rpc_test_protos/test.raw_rpc.pb.h"
+
+namespace {
+
+TEST(RawServiceStub, GeneratedStubCompiles) {
+ ::pw::rpc::test::TestService test_service;
+ EXPECT_STREQ(test_service.name(), "TestService");
+}
+
+} // namespace