pw_rpc: Introduce pw::rpc::internal::MethodInfo
MethodInfo specializations are generated for each method. This makes it
trivial to access information about RPCs at compile time with a clean
API.
Change-Id: I0c136f8744cc0d6fd00b397a74e45e6ed0e90ee6
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/61040
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_rpc/BUILD.bazel b/pw_rpc/BUILD.bazel
index 2680440..9631ac1 100644
--- a/pw_rpc/BUILD.bazel
+++ b/pw_rpc/BUILD.bazel
@@ -51,6 +51,7 @@
"public/pw_rpc/internal/endpoint.h",
"public/pw_rpc/internal/hash.h",
"public/pw_rpc/internal/method.h",
+ "public/pw_rpc/internal/method_info.h",
"public/pw_rpc/internal/method_lookup.h",
"public/pw_rpc/internal/method_union.h",
"public/pw_rpc/internal/open_call.h",
@@ -160,6 +161,7 @@
hdrs = [
"public/pw_rpc/internal/fake_channel_output.h",
"public/pw_rpc/internal/method_impl_tester.h",
+ "public/pw_rpc/internal/method_info_tester.h",
"public/pw_rpc/internal/test_method.h",
"public/pw_rpc/internal/test_method_context.h",
"public/pw_rpc/internal/test_utils.h",
@@ -190,6 +192,7 @@
"nanopb/common.cc",
"nanopb/echo_service_test.cc",
"nanopb/method.cc",
+ "nanopb/method_info_test.cc",
"nanopb/method_lookup_test.cc",
"nanopb/method_test.cc",
"nanopb/method_union_test.cc",
diff --git a/pw_rpc/BUILD.gn b/pw_rpc/BUILD.gn
index 98c44a4..85fad79 100644
--- a/pw_rpc/BUILD.gn
+++ b/pw_rpc/BUILD.gn
@@ -118,6 +118,7 @@
"channel.cc",
"packet.cc",
"public/pw_rpc/internal/channel.h",
+ "public/pw_rpc/internal/method_info.h",
"public/pw_rpc/internal/packet.h",
"public/pw_rpc/method_type.h",
]
@@ -145,6 +146,7 @@
public = [
"public/pw_rpc/internal/fake_channel_output.h",
"public/pw_rpc/internal/method_impl_tester.h",
+ "public/pw_rpc/internal/method_info_tester.h",
"public/pw_rpc/internal/test_method.h",
"public/pw_rpc/internal/test_method_context.h",
"public/pw_rpc/internal/test_utils.h",
diff --git a/pw_rpc/nanopb/BUILD.gn b/pw_rpc/nanopb/BUILD.gn
index 70aec83..3c8d2a6 100644
--- a/pw_rpc/nanopb/BUILD.gn
+++ b/pw_rpc/nanopb/BUILD.gn
@@ -114,6 +114,7 @@
":echo_service_test",
":method_lookup_test",
":method_test",
+ ":method_info_test",
":method_union_test",
":server_reader_writer_test",
":stub_generation_test",
@@ -156,6 +157,16 @@
enable_if = dir_pw_third_party_nanopb != ""
}
+pw_test("method_info_test") {
+ deps = [
+ "..:common",
+ "..:test_protos.nanopb_rpc",
+ "..:test_utils",
+ ]
+ sources = [ "method_info_test.cc" ]
+ enable_if = dir_pw_third_party_nanopb != ""
+}
+
pw_test("method_lookup_test") {
deps = [
":method",
diff --git a/pw_rpc/nanopb/method_info_test.cc b/pw_rpc/nanopb/method_info_test.cc
new file mode 100644
index 0000000..eecf006
--- /dev/null
+++ b/pw_rpc/nanopb/method_info_test.cc
@@ -0,0 +1,56 @@
+// Copyright 2021 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.
+
+#include "pw_rpc/internal/method_info.h"
+
+#include "gtest/gtest.h"
+#include "pw_rpc/internal/method_info_tester.h"
+#include "pw_rpc_test_protos/test.rpc.pb.h"
+
+namespace pw::rpc::internal {
+namespace {
+
+using GeneratedService = pw::rpc::test::pw_rpc::nanopb::TestService;
+
+class TestService final : public GeneratedService::Service<TestService> {
+ public:
+ Status TestUnaryRpc(ServerContext&,
+ const pw_rpc_test_TestRequest&,
+ pw_rpc_test_TestResponse&) {
+ return OkStatus();
+ }
+
+ void TestAnotherUnaryRpc(ServerContext&,
+ const pw_rpc_test_TestRequest&,
+ NanopbServerResponder<pw_rpc_test_TestResponse>&) {}
+
+ static void TestServerStreamRpc(
+ ServerContext&,
+ const pw_rpc_test_TestRequest&,
+ ServerWriter<pw_rpc_test_TestStreamResponse>&) {}
+
+ void TestClientStreamRpc(
+ ServerContext&,
+ ServerReader<pw_rpc_test_TestRequest, pw_rpc_test_TestStreamResponse>&) {}
+
+ void TestBidirectionalStreamRpc(
+ ServerContext&,
+ ServerReaderWriter<pw_rpc_test_TestRequest,
+ pw_rpc_test_TestStreamResponse>&) {}
+};
+
+static_assert(MethodInfoTests<GeneratedService, TestService>().Pass());
+
+} // namespace
+} // namespace pw::rpc::internal
diff --git a/pw_rpc/public/pw_rpc/internal/method.h b/pw_rpc/public/pw_rpc/internal/method.h
index 5154be1..e3c5bbe 100644
--- a/pw_rpc/public/pw_rpc/internal/method.h
+++ b/pw_rpc/public/pw_rpc/internal/method.h
@@ -79,9 +79,9 @@
Invoker invoker_;
};
-// Traits struct that determines the type of an RPC service method from its
-// signature. Derived Methods should provide specializations for their method
-// types.
+// MethodTraits inspects an RPC implementation function. It determines which
+// Method API is in use and the type of the RPC based on the function signature.
+// pw_rpc Method implementations specialize MethodTraits for each RPC type.
template <typename Method>
struct MethodTraits {
// Specializations must set Implementation as an alias for their method
diff --git a/pw_rpc/public/pw_rpc/internal/method_info.h b/pw_rpc/public/pw_rpc/internal/method_info.h
new file mode 100644
index 0000000..d849066
--- /dev/null
+++ b/pw_rpc/public/pw_rpc/internal/method_info.h
@@ -0,0 +1,47 @@
+// Copyright 2021 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.
+#pragma once
+
+#include <cstdint>
+#include <type_traits>
+
+namespace pw::rpc::internal {
+
+template <auto>
+constexpr std::false_type kIsGeneratedRpcFunction{};
+
+// The MethodInfo class provides metadata about an RPC. This class is
+// specialized for the static RPC client functions in the generated code. This
+// makes it possible for pw_rpc to access method metadata with a simple API. The
+// user passes the method name (e.g. pw_rpc::raw::MyService::MyMethod) as a
+// template parameter, and pw_rpc can extract information like method and
+// service IDs at compile time.
+template <auto kRpcFunction>
+struct MethodInfo {
+ // MethodInfo specializations always provide the service and method IDs and a
+ // function that returns the implementation method for this RPC given a
+ // service implementation class.
+ static constexpr uint32_t kServiceId = 0;
+ static constexpr uint32_t kMethodId = 0;
+
+ template <typename ServiceImpl>
+ static constexpr void Function() {}
+
+ static_assert(kIsGeneratedRpcFunction<kRpcFunction>,
+ "The provided argument is not a generated pw_rpc function. "
+ "Pass a pw_rpc function such as "
+ "my_pkg::pw_rpc::raw:::MyService::MyMethod instead.");
+};
+
+} // namespace pw::rpc::internal
diff --git a/pw_rpc/public/pw_rpc/internal/method_info_tester.h b/pw_rpc/public/pw_rpc/internal/method_info_tester.h
new file mode 100644
index 0000000..6e80099
--- /dev/null
+++ b/pw_rpc/public/pw_rpc/internal/method_info_tester.h
@@ -0,0 +1,73 @@
+// Copyright 2021 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.
+#pragma once
+
+#include "pw_rpc/internal/hash.h"
+#include "pw_rpc/internal/method_info.h"
+
+namespace pw::rpc::internal {
+
+// Tests the MethodTraits specializations for test.proto for any implementation.
+template <typename GeneratedClass, typename ServiceImpl>
+class MethodInfoTests {
+ public:
+ constexpr bool Pass() const {
+ return Ids().Pass() && MethodFunction().Pass();
+ }
+
+ private:
+ struct Ids {
+ constexpr bool Pass() const { return true; }
+
+#define PW_RPC_TEST_METHOD_INFO_IDS(function) \
+ static_assert(MethodInfo<GeneratedClass::function>::kServiceId == \
+ Hash("pw.rpc.test.TestService"), \
+ #function " service ID doesn't match!"); \
+ static_assert( \
+ MethodInfo<GeneratedClass::function>::kMethodId == Hash(#function), \
+ #function " method ID doesn't match!")
+
+ PW_RPC_TEST_METHOD_INFO_IDS(TestUnaryRpc);
+ PW_RPC_TEST_METHOD_INFO_IDS(TestAnotherUnaryRpc);
+ PW_RPC_TEST_METHOD_INFO_IDS(TestServerStreamRpc);
+ PW_RPC_TEST_METHOD_INFO_IDS(TestClientStreamRpc);
+ PW_RPC_TEST_METHOD_INFO_IDS(TestBidirectionalStreamRpc);
+#undef PW_RPC_TEST_METHOD_INFO_IDS
+ };
+
+ static_assert(MethodInfo<GeneratedClass::TestClientStreamRpc>::kServiceId !=
+ Hash("TestService"),
+ "Wrong service name should not match");
+ static_assert(
+ MethodInfo<GeneratedClass::TestBidirectionalStreamRpc>::kMethodId !=
+ Hash("TestUnaryRpc"),
+ "Wrong method name should not match");
+
+ struct MethodFunction {
+ constexpr bool Pass() const { return true; }
+
+#define PW_RPC_TEST_METHOD_INFO_FUNCTION(function) \
+ static_assert(MethodInfo<GeneratedClass::function>::template Function< \
+ ServiceImpl>() == &ServiceImpl::function)
+
+ PW_RPC_TEST_METHOD_INFO_FUNCTION(TestUnaryRpc);
+ PW_RPC_TEST_METHOD_INFO_FUNCTION(TestAnotherUnaryRpc);
+ PW_RPC_TEST_METHOD_INFO_FUNCTION(TestServerStreamRpc);
+ PW_RPC_TEST_METHOD_INFO_FUNCTION(TestClientStreamRpc);
+ PW_RPC_TEST_METHOD_INFO_FUNCTION(TestBidirectionalStreamRpc);
+#undef PW_RPC_TEST_METHOD_INFO_FUNCTION
+ };
+};
+
+} // namespace pw::rpc::internal
diff --git a/pw_rpc/py/pw_rpc/codegen.py b/pw_rpc/py/pw_rpc/codegen.py
index 8c6ad37..1918900 100644
--- a/pw_rpc/py/pw_rpc/codegen.py
+++ b/pw_rpc/py/pw_rpc/codegen.py
@@ -85,6 +85,13 @@
def client_static_function(self, method: ProtoServiceMethod) -> None:
"""Generates method static functions that instantiate a Client."""
+ @abc.abstractmethod
+ def method_info_specialization(self, method: ProtoServiceMethod) -> None:
+ """Generates impl-specific additions to the MethodInfo specialization.
+
+ May be empty if the generator has nothing to add to the MethodInfo.
+ """
+
def generate_package(file_descriptor_proto, proto_package: ProtoNode,
gen: CodeGenerator) -> None:
@@ -102,8 +109,10 @@
gen.line('#include <type_traits>\n')
include_lines = [
+ '#include "pw_rpc/internal/method_info.h"',
'#include "pw_rpc/internal/method_lookup.h"',
'#include "pw_rpc/internal/service_client.h"',
+ '#include "pw_rpc/method_type.h"',
'#include "pw_rpc/server_context.h"',
'#include "pw_rpc/service.h"',
]
@@ -145,6 +154,12 @@
if file_namespace:
gen.line('} // namespace ' + file_namespace)
+ gen.line()
+ gen.line('// Specialize MethodInfo for each RPC to provide metadata at '
+ 'compile time.')
+ for service in services:
+ _generate_info(gen, file_namespace, service)
+
def _generate_service_and_client(gen: CodeGenerator,
service: ProtoService) -> None:
@@ -200,6 +215,39 @@
gen.line()
+def _generate_info(gen: CodeGenerator, namespace: str,
+ service: ProtoService) -> None:
+ """Generates MethodInfo for each method."""
+ service_id = f'0x{pw_rpc.ids.calculate(service.proto_path()):08x}'
+ info = f'struct {RPC_NAMESPACE.lstrip(":")}::internal::MethodInfo'
+
+ for method in service.methods():
+ gen.line('template <>')
+ gen.line(f'{info}<{namespace}::pw_rpc::{gen.name()}::'
+ f'{service.name()}::{method.name()}> {{')
+
+ with gen.indent():
+ gen.line(f'static constexpr uint32_t kServiceId = {service_id};')
+ gen.line(f'static constexpr uint32_t kMethodId = '
+ f'0x{pw_rpc.ids.calculate(method.name()):08x};')
+ gen.line(f'static constexpr {RPC_NAMESPACE}::MethodType kType = '
+ f'{method.type().cc_enum()};')
+ gen.line()
+
+ gen.line('template <typename ServiceImpl>')
+ gen.line('static constexpr auto Function() {')
+
+ with gen.indent():
+ gen.line(f'return &ServiceImpl::{method.name()};')
+
+ gen.line('}')
+
+ gen.method_info_specialization(method)
+
+ gen.line('};')
+ gen.line()
+
+
def _generate_deprecated_aliases(gen: CodeGenerator,
services: Sequence[ProtoService]) -> None:
"""Generates aliases for the original, deprecated naming scheme."""
diff --git a/pw_rpc/py/pw_rpc/codegen_nanopb.py b/pw_rpc/py/pw_rpc/codegen_nanopb.py
index a977f62..154b928 100644
--- a/pw_rpc/py/pw_rpc/codegen_nanopb.py
+++ b/pw_rpc/py/pw_rpc/codegen_nanopb.py
@@ -218,6 +218,21 @@
self.line('}')
+ def method_info_specialization(self, method: ProtoServiceMethod) -> None:
+ self.line()
+ self.line(f'using Request = {method.request_type().nanopb_name()};')
+ self.line(f'using Response = {method.response_type().nanopb_name()};')
+ self.line()
+ self.line(f'static constexpr {RPC_NAMESPACE}::internal::'
+ 'NanopbMethodSerde serde() {')
+
+ with self.indent():
+ self.line('return {'
+ f'{method.request_type().nanopb_name()}_fields, '
+ f'{method.response_type().nanopb_name()}_fields}};')
+
+ self.line('}')
+
def _client_functions(method: ProtoServiceMethod) -> tuple:
res = method.response_type().nanopb_name()
@@ -308,7 +323,6 @@
generator = NanopbCodeGenerator(output_filename)
codegen.generate_package(proto_file, package_root, generator)
- generator.line()
codegen.package_stubs(package_root, generator.output, StubGenerator())
return [generator.output]
diff --git a/pw_rpc/py/pw_rpc/codegen_raw.py b/pw_rpc/py/pw_rpc/codegen_raw.py
index 33c5825..6314b3f 100644
--- a/pw_rpc/py/pw_rpc/codegen_raw.py
+++ b/pw_rpc/py/pw_rpc/codegen_raw.py
@@ -70,6 +70,9 @@
self.line('// Raw RPC clients are not yet implemented.')
self.line(f'static void {method.name()}();')
+ def method_info_specialization(self, method: ProtoServiceMethod) -> None:
+ pass
+
class StubGenerator(codegen.StubGenerator):
def unary_signature(self, method: ProtoServiceMethod, prefix: str) -> str:
@@ -110,8 +113,6 @@
generator = RawCodeGenerator(output_filename)
codegen.generate_package(proto_file, package_root, generator)
- generator.line()
-
codegen.package_stubs(package_root, generator.output, StubGenerator())
return [generator.output]
diff --git a/pw_rpc/raw/BUILD.bazel b/pw_rpc/raw/BUILD.bazel
index 242d260..856f0ee 100644
--- a/pw_rpc/raw/BUILD.bazel
+++ b/pw_rpc/raw/BUILD.bazel
@@ -92,6 +92,20 @@
)
pw_cc_test(
+ name = "method_info_test",
+ srcs = [
+ "method_info_test.cc",
+ ],
+ deps = [
+ "//pw_rpc:common",
+ "//pw_rpc:internal_test_utils",
+ "//pw_rpc:pw_rpc_test_pwpb",
+ # TODO(hepler): RPC deps not used directly should be provided by the proto library
+ "//pw_rpc/raw:method_union",
+ ],
+)
+
+pw_cc_test(
name = "method_union_test",
srcs = [
"method_union_test.cc",
diff --git a/pw_rpc/raw/BUILD.gn b/pw_rpc/raw/BUILD.gn
index 6e0febd..524eb94 100644
--- a/pw_rpc/raw/BUILD.gn
+++ b/pw_rpc/raw/BUILD.gn
@@ -64,6 +64,7 @@
tests = [
":codegen_test",
":method_test",
+ ":method_info_test",
":method_union_test",
":server_reader_writer_test",
":stub_generation_test",
@@ -92,6 +93,15 @@
sources = [ "method_test.cc" ]
}
+pw_test("method_info_test") {
+ deps = [
+ "..:common",
+ "..:test_protos.raw_rpc",
+ "..:test_utils",
+ ]
+ sources = [ "method_info_test.cc" ]
+}
+
pw_test("method_union_test") {
deps = [
":method_union",
diff --git a/pw_rpc/raw/method_info_test.cc b/pw_rpc/raw/method_info_test.cc
new file mode 100644
index 0000000..c274b7a
--- /dev/null
+++ b/pw_rpc/raw/method_info_test.cc
@@ -0,0 +1,46 @@
+// Copyright 2021 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.
+
+#include "pw_rpc/internal/method_info.h"
+
+#include "gtest/gtest.h"
+#include "pw_rpc/internal/method_info_tester.h"
+#include "pw_rpc_test_protos/test.raw_rpc.pb.h"
+
+namespace pw::rpc::internal {
+namespace {
+
+class TestService final
+ : public pw::rpc::test::generated::TestService<TestService> {
+ public:
+ static StatusWithSize TestUnaryRpc(ServerContext&, ConstByteSpan, ByteSpan) {
+ return StatusWithSize(0);
+ }
+
+ void TestAnotherUnaryRpc(ServerContext&, ConstByteSpan, RawServerResponder&) {
+ }
+
+ void TestServerStreamRpc(ServerContext&, ConstByteSpan, RawServerWriter&) {}
+
+ void TestClientStreamRpc(ServerContext&, RawServerReader&) {}
+
+ void TestBidirectionalStreamRpc(ServerContext&, RawServerReaderWriter&) {}
+};
+
+static_assert(
+ MethodInfoTests<pw::rpc::test::pw_rpc::raw::TestService, TestService>()
+ .Pass());
+
+} // namespace
+} // namespace pw::rpc::internal