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