pw_rpc: Check RPC method signature; improve errors

- Fix issue where an RPC method could be implemented with the signature
  of a different RPC type. Pass the expected method type when methods
  are created and check that it matches the type deduced from the method
  signature.
- Reduce code duplication to limit the number of places where
  compilation errors occur.
  - Combine variable RPC method traits classes into MethodTraits.
  - Move all Method creation functionality into the method classes.
  - Put the final method invocation logic into one function that
    supports member functions or static functions.
- Consolidate error messages for incorrect method signatures into one
  place. Expand the messages and describe the expected function
  signature.

Change-Id: Iae3c6d32df715e877e8c977ccd2232814ef590a7
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/25641
Commit-Queue: Wyatt Hepler <hepler@google.com>
Reviewed-by: Keir Mierle <keir@google.com>
diff --git a/pw_protobuf/py/pw_protobuf/proto_tree.py b/pw_protobuf/py/pw_protobuf/proto_tree.py
index 95abf44..de5a4e5 100644
--- a/pw_protobuf/py/pw_protobuf/proto_tree.py
+++ b/pw_protobuf/py/pw_protobuf/proto_tree.py
@@ -324,10 +324,14 @@
 class ProtoServiceMethod:
     """A method defined in a protobuf service."""
     class Type(enum.Enum):
-        UNARY = 0
-        SERVER_STREAMING = 1
-        CLIENT_STREAMING = 2
-        BIDIRECTIONAL_STREAMING = 3
+        UNARY = 'kUnary'
+        SERVER_STREAMING = 'kServerStreaming'
+        CLIENT_STREAMING = 'kClientStreaming'
+        BIDIRECTIONAL_STREAMING = 'kBidirectionalStreaming'
+
+        def cc_enum(self) -> str:
+            """Returns the pw_rpc MethodType C++ enum for this method type."""
+            return '::pw::rpc::internal::MethodType::' + self.value
 
     def __init__(self, name: str, method_type: Type, request_type: ProtoNode,
                  response_type: ProtoNode):
diff --git a/pw_rpc/BUILD b/pw_rpc/BUILD
index a7c4a83..260f2d3 100644
--- a/pw_rpc/BUILD
+++ b/pw_rpc/BUILD
@@ -25,8 +25,8 @@
 pw_cc_library(
     name = "client",
     srcs = [
-        "client.cc",
         "base_client_call.cc",
+        "client.cc",
     ],
     hdrs = [
         "public/pw_rpc/client.h",
@@ -34,7 +34,7 @@
     ],
     deps = [
         ":common",
-    ]
+    ],
 )
 
 pw_cc_library(
@@ -83,15 +83,6 @@
 )
 
 pw_cc_library(
-    name = "service_method_traits",
-    hdrs = [
-        "public/pw_rpc/internal/service_method_traits.h",
-    ],
-    includes = ["public"],
-    deps = [ ":server"  ],
-)
-
-pw_cc_library(
     name = "internal_test_utils",
     hdrs = [
         "public/pw_rpc/internal/test_method.h",
@@ -121,11 +112,9 @@
         "nanopb/public/pw_rpc/internal/nanopb_common.h",
         "nanopb/public/pw_rpc/internal/nanopb_method.h",
         "nanopb/public/pw_rpc/internal/nanopb_method_union.h",
-        "nanopb/public/pw_rpc/internal/nanopb_service_method_traits.h",
         "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/nanopb_service_method_traits_test.cc",
         "nanopb/test.pb.c",
         "nanopb/test.pb.h",
         "nanopb/test_rpc.pb.h",
diff --git a/pw_rpc/BUILD.gn b/pw_rpc/BUILD.gn
index 01228ad..a04c2e6 100644
--- a/pw_rpc/BUILD.gn
+++ b/pw_rpc/BUILD.gn
@@ -86,12 +86,6 @@
   friend = [ "./*" ]
 }
 
-pw_source_set("service_method_traits") {
-  public = [ "public/pw_rpc/internal/service_method_traits.h" ]
-  public_deps = [ ":server" ]
-  visibility = [ "./*" ]
-}
-
 pw_source_set("test_utils") {
   public = [
     "public/pw_rpc/internal/test_method.h",
diff --git a/pw_rpc/docs.rst b/pw_rpc/docs.rst
index 2e62c31..0d5d029 100644
--- a/pw_rpc/docs.rst
+++ b/pw_rpc/docs.rst
@@ -604,26 +604,18 @@
 ^^^^^^^^^^^^^^^^
 The RPC Server depends on the ``pw::rpc::internal::Method`` class. ``Method``
 serves as the bridge between the ``pw_rpc`` server library and the user-defined
-RPC functions. ``Method`` takes an RPC packet, decodes it using a protobuf
-library (if applicable), and calls the RPC function. Since ``Method`` interacts
-directly with the protobuf library, it must be implemented separately for each
-protobuf library.
+RPC functions. Each supported protobuf implementation extends ``Method`` to
+implement its request and response proto handling. The ``pw_rpc`` server
+calls into the ``Method`` implementation through the base class's ``Invoke``
+function.
 
-``pw::rpc::internal::Method`` is not implemented as a facade with different
-backends. Instead, there is a separate instance of the ``pw_rpc`` server library
-for each ``Method`` implementation. There are a few reasons for this.
+``Method`` implementations store metadata about each method, including a
+function pointer to the user-defined method implementation. They also provide
+``static constexpr`` functions for creating each type of method. ``Method``
+implementations must satisfy the ``MethodImplTester`` test class in
+``pw_rpc_private/method_impl_tester.h``.
 
-* ``Method`` is entirely internal to ``pw_rpc``. Users will never implement a
-  custom backend. Exposing a facade would unnecessarily expose implementation
-  details and make ``pw_rpc`` more difficult to use.
-* There is no common interface between ``pw_rpc`` / ``Method`` implementations.
-  It's not possible to swap between e.g. a Nanopb and a ``pw_protobuf`` RPC
-  server because the interface for the user-implemented RPCs changes completely.
-  This nullifies the primary benefit of facades.
-* The different ``Method`` implementations can be built easily alongside one
-  another in a cross-platform way. This makes testing simpler, since the tests
-  build with any backend configuration. Users can select which ``Method``
-  implementation to use simply by depending on the corresponding server library.
+See ``pw_rpc/internal/method.h`` for more details about ``Method``.
 
 Packet flow
 ^^^^^^^^^^^
diff --git a/pw_rpc/nanopb/BUILD.gn b/pw_rpc/nanopb/BUILD.gn
index 39edd13..b3ee46a 100644
--- a/pw_rpc/nanopb/BUILD.gn
+++ b/pw_rpc/nanopb/BUILD.gn
@@ -65,21 +65,11 @@
   }
 }
 
-pw_source_set("service_method_traits") {
-  public_configs = [ ":public" ]
-  public = [ "public/pw_rpc/internal/nanopb_service_method_traits.h" ]
-  public_deps = [
-    ":method_union",
-    "..:service_method_traits",
-  ]
-}
-
 pw_source_set("test_method_context") {
   public_configs = [ ":public" ]
   public = [ "public/pw_rpc/nanopb_test_method_context.h" ]
   public_deps = [
-    ":service_method_traits",
-    "..:server",
+    ":method",
     dir_pw_assert,
     dir_pw_containers,
   ]
@@ -110,7 +100,6 @@
     ":echo_service_test",
     ":nanopb_method_test",
     ":nanopb_method_union_test",
-    ":nanopb_service_method_traits_test",
   ]
 }
 
@@ -170,15 +159,3 @@
   sources = [ "echo_service_test.cc" ]
   enable_if = dir_pw_third_party_nanopb != ""
 }
-
-pw_test("nanopb_service_method_traits_test") {
-  deps = [
-    ":echo_service",
-    ":method",
-    ":service_method_traits",
-    ":test_method_context",
-    "..:test_protos.nanopb_rpc",
-  ]
-  sources = [ "nanopb_service_method_traits_test.cc" ]
-  enable_if = dir_pw_third_party_nanopb != ""
-}
diff --git a/pw_rpc/nanopb/codegen_test.cc b/pw_rpc/nanopb/codegen_test.cc
index 7c7667e..c43ccef 100644
--- a/pw_rpc/nanopb/codegen_test.cc
+++ b/pw_rpc/nanopb/codegen_test.cc
@@ -31,9 +31,10 @@
     return static_cast<Status::Code>(request.status_code);
   }
 
-  void TestStreamRpc(ServerContext&,
-                     const pw_rpc_test_TestRequest& request,
-                     ServerWriter<pw_rpc_test_TestStreamResponse>& writer) {
+  static void TestStreamRpc(
+      ServerContext&,
+      const pw_rpc_test_TestRequest& request,
+      ServerWriter<pw_rpc_test_TestStreamResponse>& writer) {
     for (int i = 0; i < request.integer; ++i) {
       writer.Write({.chunk = {}, .number = static_cast<uint32_t>(i)});
     }
diff --git a/pw_rpc/nanopb/nanopb_method_test.cc b/pw_rpc/nanopb/nanopb_method_test.cc
index fa0072c..6c0b736 100644
--- a/pw_rpc/nanopb/nanopb_method_test.cc
+++ b/pw_rpc/nanopb/nanopb_method_test.cc
@@ -32,7 +32,7 @@
 pw_rpc_test_TestRequest last_request;
 ServerWriter<pw_rpc_test_TestResponse> last_writer;
 
-Status AddFive(ServerCall&,
+Status AddFive(ServerContext&,
                const pw_rpc_test_TestRequest& request,
                pw_rpc_test_TestResponse& response) {
   last_request = request;
@@ -40,11 +40,11 @@
   return Status::Unauthenticated();
 }
 
-Status DoNothing(ServerCall&, const pw_rpc_test_Empty&, pw_rpc_test_Empty&) {
+Status DoNothing(ServerContext&, const pw_rpc_test_Empty&, pw_rpc_test_Empty&) {
   return Status::Unknown();
 }
 
-void StartStream(ServerCall&,
+void StartStream(ServerContext&,
                  const pw_rpc_test_TestRequest& request,
                  ServerWriter<pw_rpc_test_TestResponse>& writer) {
   last_request = request;
diff --git a/pw_rpc/nanopb/nanopb_method_union_test.cc b/pw_rpc/nanopb/nanopb_method_union_test.cc
index ccd8493..5df140d 100644
--- a/pw_rpc/nanopb/nanopb_method_union_test.cc
+++ b/pw_rpc/nanopb/nanopb_method_union_test.cc
@@ -32,13 +32,15 @@
   constexpr FakeGeneratedService(uint32_t id) : Service(id, kMethods) {}
 
   static constexpr std::array<NanopbMethodUnion, 4> kMethods = {
-      GetNanopbOrRawMethodFor<&Implementation::DoNothing>(
+      GetNanopbOrRawMethodFor<&Implementation::DoNothing, MethodType::kUnary>(
           10u, pw_rpc_test_Empty_fields, pw_rpc_test_Empty_fields),
-      GetNanopbOrRawMethodFor<&Implementation::RawStream>(
+      GetNanopbOrRawMethodFor<&Implementation::RawStream,
+                              MethodType::kServerStreaming>(
           11u, pw_rpc_test_TestRequest_fields, pw_rpc_test_TestResponse_fields),
-      GetNanopbOrRawMethodFor<&Implementation::AddFive>(
+      GetNanopbOrRawMethodFor<&Implementation::AddFive, MethodType::kUnary>(
           12u, pw_rpc_test_TestRequest_fields, pw_rpc_test_TestResponse_fields),
-      GetNanopbOrRawMethodFor<&Implementation::StartStream>(
+      GetNanopbOrRawMethodFor<&Implementation::StartStream,
+                              MethodType::kServerStreaming>(
           13u, pw_rpc_test_TestRequest_fields, pw_rpc_test_TestResponse_fields),
   };
 };
diff --git a/pw_rpc/nanopb/nanopb_service_method_traits_test.cc b/pw_rpc/nanopb/nanopb_service_method_traits_test.cc
deleted file mode 100644
index 8043e12..0000000
--- a/pw_rpc/nanopb/nanopb_service_method_traits_test.cc
+++ /dev/null
@@ -1,31 +0,0 @@
-// 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.
-
-#include "pw_rpc/internal/nanopb_service_method_traits.h"
-
-#include <type_traits>
-
-#include "pw_rpc/echo_service_nanopb.h"
-#include "pw_rpc/internal/hash.h"
-
-namespace pw::rpc::internal {
-namespace {
-
-static_assert(
-    std::is_same_v<decltype(NanopbServiceMethodTraits<&EchoService::Echo,
-                                                      Hash("Echo")>::method()),
-                   const NanopbMethod&>);
-
-}  // namespace
-}  // namespace pw::rpc::internal
diff --git a/pw_rpc/nanopb/public/pw_rpc/internal/nanopb_method.h b/pw_rpc/nanopb/public/pw_rpc/internal/nanopb_method.h
index 1808b34..33cbaf7 100644
--- a/pw_rpc/nanopb/public/pw_rpc/internal/nanopb_method.h
+++ b/pw_rpc/nanopb/public/pw_rpc/internal/nanopb_method.h
@@ -53,22 +53,14 @@
 
 namespace internal {
 
+class NanopbMethod;
 class Packet;
 
-// Templated false value for use in static_assert(false) statements.
-template <typename...>
-constexpr std::false_type kFalseValue{};
-
-// Extracts the request and response proto types from a method.
-template <typename Method>
-struct RpcTraits {
-  static_assert(kFalseValue<Method>,
-                "The selected function is not an RPC service method");
-};
-
-// Specialization for unary RPCs.
+// MethodTraits specialization for a static unary method.
 template <typename RequestType, typename ResponseType>
-struct RpcTraits<Status (*)(ServerCall&, const RequestType&, ResponseType&)> {
+struct MethodTraits<Status (*)(
+    ServerContext&, const RequestType&, ResponseType&)> {
+  using Implementation = NanopbMethod;
   using Request = RequestType;
   using Response = ResponseType;
 
@@ -77,10 +69,20 @@
   static constexpr bool kClientStreaming = false;
 };
 
-// Specialization for server streaming RPCs.
+// MethodTraits specialization for a unary method.
+template <typename T, typename RequestType, typename ResponseType>
+struct MethodTraits<Status (T::*)(
+    ServerContext&, const RequestType&, ResponseType&)>
+    : public MethodTraits<Status (*)(
+          ServerContext&, const RequestType&, ResponseType&)> {
+  using Service = T;
+};
+
+// MethodTraits specialization for a static server streaming method.
 template <typename RequestType, typename ResponseType>
-struct RpcTraits<void (*)(
-    ServerCall&, const RequestType&, ServerWriter<ResponseType>&)> {
+struct MethodTraits<void (*)(
+    ServerContext&, const RequestType&, ServerWriter<ResponseType>&)> {
+  using Implementation = NanopbMethod;
   using Request = RequestType;
   using Response = ResponseType;
 
@@ -89,29 +91,20 @@
   static constexpr bool kClientStreaming = false;
 };
 
-// Member function specialization for unary RPCs.
+// MethodTraits specialization for a server streaming method.
 template <typename T, typename RequestType, typename ResponseType>
-struct RpcTraits<Status (T::*)(
-    ServerContext&, const RequestType&, ResponseType&)>
-    : public RpcTraits<Status (*)(
-          ServerCall&, const RequestType&, ResponseType&)> {
-  using Service = T;
-};
-
-// Member function specialization for server streaming RPCs.
-template <typename T, typename RequestType, typename ResponseType>
-struct RpcTraits<void (T::*)(
+struct MethodTraits<void (T::*)(
     ServerContext&, const RequestType&, ServerWriter<ResponseType>&)>
-    : public RpcTraits<void (*)(
-          ServerCall&, const RequestType&, ServerWriter<ResponseType>&)> {
+    : public MethodTraits<void (*)(
+          ServerContext&, const RequestType&, ServerWriter<ResponseType>&)> {
   using Service = T;
 };
 
 template <auto method>
-using Request = typename RpcTraits<decltype(method)>::Request;
+using Request = typename MethodTraits<decltype(method)>::Request;
 
 template <auto method>
-using Response = typename RpcTraits<decltype(method)>::Response;
+using Response = typename MethodTraits<decltype(method)>::Response;
 
 // The NanopbMethod class invokes user-defined service methods. When a
 // pw::rpc::Server receives an RPC request packet, it looks up the matching
@@ -125,6 +118,11 @@
 // structs.
 class NanopbMethod : public Method {
  public:
+  template <auto method>
+  static constexpr bool matches() {
+    return std::is_same_v<MethodImplementation<method>, NanopbMethod>;
+  }
+
   // Creates a NanopbMethod for a unary RPC.
   template <auto method>
   static constexpr NanopbMethod Unary(uint32_t id,
@@ -136,18 +134,19 @@
     //
     // In optimized builds, the compiler inlines the user-defined function into
     // this wrapper, elminating any overhead.
-    return NanopbMethod(
-        id,
-        UnaryInvoker<AllocateSpaceFor<Request<method>>(),
-                     AllocateSpaceFor<Response<method>>()>,
-        Function{.unary =
-                     [](ServerCall& call, const void* req, void* resp) {
-                       return method(call,
-                                     *static_cast<const Request<method>*>(req),
-                                     *static_cast<Response<method>*>(resp));
-                     }},
-        request,
-        response);
+    constexpr UnaryFunction wrapper =
+        [](ServerCall& call, const void* req, void* resp) {
+          return CallMethodImplFunction<method>(
+              call,
+              *static_cast<const Request<method>*>(req),
+              *static_cast<Response<method>*>(resp));
+        };
+    return NanopbMethod(id,
+                        UnaryInvoker<AllocateSpaceFor<Request<method>>(),
+                                     AllocateSpaceFor<Response<method>>()>,
+                        Function{.unary = wrapper},
+                        request,
+                        response);
   }
 
   // Creates a NanopbMethod for a server-streaming RPC.
@@ -160,22 +159,26 @@
     // struct as void* and a BaseServerWriter instead of the templated
     // ServerWriter class. This wrapper is stored generically in the Function
     // union, defined below.
+    constexpr ServerStreamingFunction wrapper =
+        [](ServerCall& call, const void* req, BaseServerWriter& writer) {
+          return CallMethodImplFunction<method>(
+              call,
+              *static_cast<const Request<method>*>(req),
+              static_cast<ServerWriter<Response<method>>&>(writer));
+        };
     return NanopbMethod(
         id,
         ServerStreamingInvoker<AllocateSpaceFor<Request<method>>()>,
-        Function{.server_streaming =
-                     [](ServerCall& call,
-                        const void* req,
-                        BaseServerWriter& writer) {
-                       method(call,
-                              *static_cast<const Request<method>*>(req),
-                              static_cast<ServerWriter<Response<method>>&>(
-                                  writer));
-                     }},
+        Function{.server_streaming = wrapper},
         request,
         response);
   }
 
+  // Represents an invalid method. Used to reduce error message verbosity.
+  static constexpr NanopbMethod Invalid() {
+    return {0, InvalidInvoker, {}, nullptr, nullptr};
+  }
+
   // Encodes a response protobuf with Nanopb to the provided buffer.
   StatusWithSize EncodeResponse(const void* proto_struct,
                                 std::span<std::byte> buffer) const {
diff --git a/pw_rpc/nanopb/public/pw_rpc/internal/nanopb_method_union.h b/pw_rpc/nanopb/public/pw_rpc/internal/nanopb_method_union.h
index eef6e47..d73813b 100644
--- a/pw_rpc/nanopb/public/pw_rpc/internal/nanopb_method_union.h
+++ b/pw_rpc/nanopb/public/pw_rpc/internal/nanopb_method_union.h
@@ -40,86 +40,20 @@
   } impl_;
 };
 
-// Specialization for a nanopb unary method.
-template <typename T, typename RequestType, typename ResponseType>
-struct MethodTraits<Status (T::*)(
-    ServerContext&, const RequestType&, ResponseType&)> {
-  static constexpr MethodType kType = MethodType::kUnary;
-
-  using Service = T;
-  using Implementation = NanopbMethod;
-  using Request = RequestType;
-  using Response = ResponseType;
-};
-
-// Specialization for a nanopb server streaming method.
-template <typename T, typename RequestType, typename ResponseType>
-struct MethodTraits<void (T::*)(
-    ServerContext&, const RequestType&, ServerWriter<ResponseType>&)> {
-  static constexpr MethodType kType = MethodType::kServerStreaming;
-
-  using Service = T;
-  using Implementation = NanopbMethod;
-  using Request = RequestType;
-  using Response = ResponseType;
-};
-
-template <auto method>
-constexpr bool kIsNanopb =
-    std::is_same_v<MethodImplementation<method>, NanopbMethod>;
-
-// Deduces the type of an implemented nanopb service method from its signature,
-// and returns the appropriate Method object to invoke it.
-template <auto method>
-constexpr NanopbMethod GetNanopbMethodFor(
-    uint32_t id,
-    NanopbMessageDescriptor request_fields,
-    NanopbMessageDescriptor response_fields) {
-  static_assert(
-      kIsNanopb<method>,
-      "GetNanopbMethodFor should only be called on nanopb RPC methods");
-
-  using Traits = MethodTraits<decltype(method)>;
-  using ServiceImpl = typename Traits::Service;
-
-  if constexpr (Traits::kType == MethodType::kUnary) {
-    constexpr auto invoker = +[](ServerCall& call,
-                                 const typename Traits::Request& request,
-                                 typename Traits::Response& response) {
-      return (static_cast<ServiceImpl&>(call.service()).*method)(
-          call.context(), request, response);
-    };
-    return NanopbMethod::Unary<invoker>(id, request_fields, response_fields);
-  }
-
-  if constexpr (Traits::kType == MethodType::kServerStreaming) {
-    constexpr auto invoker =
-        +[](ServerCall& call,
-            const typename Traits::Request& request,
-            ServerWriter<typename Traits::Response>& writer) {
-          (static_cast<ServiceImpl&>(call.service()).*method)(
-              call.context(), request, writer);
-        };
-    return NanopbMethod::ServerStreaming<invoker>(
-        id, request_fields, response_fields);
-  }
-
-  constexpr auto fake_invoker =
-      +[](ServerCall&, const int&, ServerWriter<int>&) {};
-  return NanopbMethod::ServerStreaming<fake_invoker>(0, nullptr, nullptr);
-}
-
-// Returns either a raw or nanopb method object, depending on an implemented
+// Returns either a raw or nanopb method object, depending on the implemented
 // function's signature.
-template <auto method>
+template <auto method, MethodType type>
 constexpr auto GetNanopbOrRawMethodFor(
     uint32_t id,
     [[maybe_unused]] NanopbMessageDescriptor request_fields,
     [[maybe_unused]] NanopbMessageDescriptor response_fields) {
-  if constexpr (kIsRaw<method>) {
-    return GetRawMethodFor<method>(id);
+  if constexpr (RawMethod::matches<method>()) {
+    return GetMethodFor<method, RawMethod, type>(id);
+  } else if constexpr (NanopbMethod::matches<method>()) {
+    return GetMethodFor<method, NanopbMethod, type>(
+        id, request_fields, response_fields);
   } else {
-    return GetNanopbMethodFor<method>(id, request_fields, response_fields);
+    return InvalidMethod<method, type, RawMethod>(id);
   }
 };
 
diff --git a/pw_rpc/nanopb/public/pw_rpc/internal/nanopb_service_method_traits.h b/pw_rpc/nanopb/public/pw_rpc/internal/nanopb_service_method_traits.h
deleted file mode 100644
index 866ceef..0000000
--- a/pw_rpc/nanopb/public/pw_rpc/internal/nanopb_service_method_traits.h
+++ /dev/null
@@ -1,27 +0,0 @@
-// 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.
-#pragma once
-
-#include "pw_rpc/internal/nanopb_method_union.h"
-#include "pw_rpc/internal/service_method_traits.h"
-
-namespace pw::rpc::internal {
-
-template <auto impl_method, uint32_t method_id>
-using NanopbServiceMethodTraits =
-    ServiceMethodTraits<&MethodBaseService<impl_method>::NanopbMethodFor,
-                        impl_method,
-                        method_id>;
-
-}  // namespace pw::rpc::internal
diff --git a/pw_rpc/nanopb/public/pw_rpc/nanopb_test_method_context.h b/pw_rpc/nanopb/public/pw_rpc/nanopb_test_method_context.h
index 7909429..0ee76fe 100644
--- a/pw_rpc/nanopb/public/pw_rpc/nanopb_test_method_context.h
+++ b/pw_rpc/nanopb/public/pw_rpc/nanopb_test_method_context.h
@@ -22,7 +22,6 @@
 #include "pw_rpc/channel.h"
 #include "pw_rpc/internal/hash.h"
 #include "pw_rpc/internal/nanopb_method.h"
-#include "pw_rpc/internal/nanopb_service_method_traits.h"
 #include "pw_rpc/internal/packet.h"
 #include "pw_rpc/internal/server.h"
 
@@ -71,12 +70,13 @@
 //   PW_NANOPB_TEST_METHOD_CONTEXT(MyService, BestMethod, 3, 256) context;
 //   ASSERT_EQ(3u, context.responses().max_size());
 //
-
 #define PW_NANOPB_TEST_METHOD_CONTEXT(service, method, ...)              \
-  ::pw::rpc::NanopbTestMethodContext<&service::method,                   \
+  ::pw::rpc::NanopbTestMethodContext<service,                            \
+                                     &service::method,                   \
                                      ::pw::rpc::internal::Hash(#method), \
                                      ##__VA_ARGS__>
-template <auto method,
+template <typename Service,
+          auto method,
           uint32_t method_id,
           size_t max_responses = 4,
           size_t output_size_bytes = 128>
@@ -121,8 +121,18 @@
   Status last_status_;
 };
 
+template <typename Service, uint32_t method_id>
+constexpr const internal::NanopbMethod& GetNanopbMethod() {
+  constexpr const internal::NanopbMethod* nanopb_method =
+      GeneratedService<Service>::NanopbMethodFor(method_id);
+  static_assert(nanopb_method != nullptr,
+                "The selected function is not an RPC service method");
+  return *nanopb_method;
+}
+
 // Collects everything needed to invoke a particular RPC.
-template <auto method,
+template <typename Service,
+          auto method,
           uint32_t method_id,
           size_t max_responses,
           size_t output_size>
@@ -132,22 +142,20 @@
 
   template <typename... Args>
   InvocationContext(Args&&... args)
-      : output(NanopbServiceMethodTraits<method, method_id>::method(),
-               responses,
-               buffer),
+      : output(GetNanopbMethod<Service, method_id>(), responses, buffer),
         channel(Channel::Create<123>(&output)),
         server(std::span(&channel, 1)),
         service(std::forward<Args>(args)...),
         call(static_cast<internal::Server&>(server),
              static_cast<internal::Channel&>(channel),
              service,
-             NanopbServiceMethodTraits<method, method_id>::method()) {}
+             GetNanopbMethod<Service, method_id>()) {}
 
   MessageOutput<Response> output;
 
   rpc::Channel channel;
   rpc::Server server;
-  typename NanopbServiceMethodTraits<method, method_id>::Service service;
+  Service service;
   Vector<Response, max_responses> responses;
   std::array<std::byte, output_size> buffer = {};
 
@@ -156,10 +164,10 @@
 
 // Method invocation context for a unary RPC. Returns the status in call() and
 // provides the response through the response() method.
-template <auto method, uint32_t method_id, size_t output_size>
+template <typename Service, auto method, uint32_t method_id, size_t output_size>
 class UnaryContext {
  private:
-  InvocationContext<method, method_id, 1, output_size> ctx_;
+  InvocationContext<Service, method, method_id, 1, output_size> ctx_;
 
  public:
   using Request = typename decltype(ctx_)::Request;
@@ -173,8 +181,8 @@
     ctx_.output.clear();
     ctx_.responses.emplace_back();
     ctx_.responses.back() = {};
-    return (ctx_.service.*method)(
-        ctx_.call.context(), request, ctx_.responses.back());
+    return CallMethodImplFunction<method>(
+        ctx_.call, request, ctx_.responses.back());
   }
 
   // Gives access to the RPC's response.
@@ -185,13 +193,15 @@
 };
 
 // Method invocation context for a server streaming RPC.
-template <auto method,
+template <typename Service,
+          auto method,
           uint32_t method_id,
           size_t max_responses,
           size_t output_size>
 class ServerStreamingContext {
  private:
-  InvocationContext<method, method_id, max_responses, output_size> ctx_;
+  InvocationContext<Service, method, method_id, max_responses, output_size>
+      ctx_;
 
  public:
   using Request = typename decltype(ctx_)::Request;
@@ -204,8 +214,8 @@
   void call(const Request& request) {
     ctx_.output.clear();
     internal::BaseServerWriter server_writer(ctx_.call);
-    return (ctx_.service.*method)(
-        ctx_.call.context(),
+    return CallMethodImplFunction<method>(
+        ctx_.call,
         request,
         static_cast<ServerWriter<Response>&>(server_writer));
   }
@@ -239,11 +249,19 @@
 
 // Alias to select the type of the context object to use based on which type of
 // RPC it is for.
-template <auto method, uint32_t method_id, size_t responses, size_t output_size>
+template <typename Service,
+          auto method,
+          uint32_t method_id,
+          size_t responses,
+          size_t output_size>
 using Context = std::tuple_element_t<
-    static_cast<size_t>(internal::RpcTraits<decltype(method)>::kType),
-    std::tuple<UnaryContext<method, method_id, output_size>,
-               ServerStreamingContext<method, method_id, responses, output_size>
+    static_cast<size_t>(internal::MethodTraits<decltype(method)>::kType),
+    std::tuple<UnaryContext<Service, method, method_id, output_size>,
+               ServerStreamingContext<Service,
+                                      method,
+                                      method_id,
+                                      responses,
+                                      output_size>
                // TODO(hepler): Support client and bidi streaming
                >>;
 
@@ -290,20 +308,27 @@
 
 }  // namespace internal::test::nanopb
 
-template <auto method,
+template <typename Service,
+          auto method,
           uint32_t method_id,
           size_t max_responses,
           size_t output_size_bytes>
 class NanopbTestMethodContext
-    : public internal::test::nanopb::
-          Context<method, method_id, max_responses, output_size_bytes> {
+    : public internal::test::nanopb::Context<Service,
+                                             method,
+                                             method_id,
+                                             max_responses,
+                                             output_size_bytes> {
  public:
   // Forwards constructor arguments to the service class.
   template <typename... ServiceArgs>
   NanopbTestMethodContext(ServiceArgs&&... service_args)
-      : internal::test::nanopb::
-            Context<method, method_id, max_responses, output_size_bytes>(
-                std::forward<ServiceArgs>(service_args)...) {}
+      : internal::test::nanopb::Context<Service,
+                                        method,
+                                        method_id,
+                                        max_responses,
+                                        output_size_bytes>(
+            std::forward<ServiceArgs>(service_args)...) {}
 };
 
 }  // namespace pw::rpc
diff --git a/pw_rpc/public/pw_rpc/internal/method.h b/pw_rpc/public/pw_rpc/internal/method.h
index cd9155b..aae02df 100644
--- a/pw_rpc/public/pw_rpc/internal/method.h
+++ b/pw_rpc/public/pw_rpc/internal/method.h
@@ -15,6 +15,7 @@
 
 #include <cstddef>
 #include <cstdint>
+#include <utility>
 
 #include "pw_rpc/internal/call.h"
 
@@ -22,7 +23,36 @@
 
 class Packet;
 
-// RPC server implementations provide a class that dervies from Method.
+// Each supported protobuf implementation provides a class that derives from
+// Method. The implementation classes provide the following public interface:
+/*
+class MethodImpl : public Method {
+  // True if the provided function signature is valid for this method impl.
+  template <auto method>
+  static constexpr bool matches();
+
+  // Creates a unary method instance.
+  template <auto method>
+  static constexpr MethodImpl Unary(uint32_t id, [optional args]);
+
+  // Creates a server streaming method instance.
+  template <auto method>
+  static constexpr MethodImpl ServerStreaming(uint32_t id, [optional args]);
+
+  // Creates a client streaming method instance.
+  static constexpr MethodImpl ClientStreaming(uint32_t id, [optional args]);
+
+  // Creates a bidirectional streaming method instance.
+  static constexpr MethodImpl BidirectionalStreaming(uint32_t id,
+                                                     [optional args]);
+
+  // Creates a method instance for when the method implementation function has
+  // an incorrect signature. Having this helps reduce error message verbosity.
+  static constexpr MethodImpl Invalid();
+};
+*/
+// Method implementations must pass a test that uses the MethodImplTester class
+// in pw_rpc_private/method_impl_tester.h.
 class Method {
  public:
   constexpr uint32_t id() const { return id_; }
@@ -38,6 +68,10 @@
  protected:
   using Invoker = void (&)(const Method&, ServerCall&, const Packet&);
 
+  static constexpr void InvalidInvoker(const Method&,
+                                       ServerCall&,
+                                       const Packet&) {}
+
   constexpr Method(uint32_t id, Invoker invoker) : id_(id), invoker_(invoker) {}
 
  private:
@@ -45,4 +79,50 @@
   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.
+template <typename Method>
+struct MethodTraits {
+  // Specializations must set Implementation as an alias for their method
+  // implementation class.
+  using Implementation = Method;
+
+  // Specializations must set kType to the MethodType.
+  // static constexpr MethodType kType = (method type);
+
+  // Specializations for member function types must set Service as an alias to
+  // the implemented service class.
+  using Service = rpc::Service;
+};
+
+template <auto method>
+using MethodImplementation =
+    typename MethodTraits<decltype(method)>::Implementation;
+
+// Function that calls a user-defined method implementation function from a
+// ServerCall object.
+template <auto method, typename... Args>
+constexpr auto CallMethodImplFunction(ServerCall& call, Args&&... args) {
+  // If the method impl is a member function, deduce the type of the
+  // user-defined service from it, then call the method on the service.
+  if constexpr (std::is_member_function_pointer_v<decltype(method)>) {
+    return (static_cast<typename MethodTraits<decltype(method)>::Service&>(
+                call.service()).*
+            method)(call.context(), std::forward<Args>(args)...);
+  } else {
+    return method(call.context(), std::forward<Args>(args)...);
+  }
+}
+
+// Identifies a base class from a member function it defines. This should be
+// used with decltype to retrieve the base service class.
+template <typename T, typename U>
+T BaseFromMember(U T::*);
+
+// The base generated service of an RPC service class.
+template <typename Service>
+using GeneratedService =
+    decltype(BaseFromMember(&Service::_PwRpcInternalGeneratedBase));
+
 }  // namespace pw::rpc::internal
diff --git a/pw_rpc/public/pw_rpc/internal/method_union.h b/pw_rpc/public/pw_rpc/internal/method_union.h
index 9d491af..154667d 100644
--- a/pw_rpc/public/pw_rpc/internal/method_union.h
+++ b/pw_rpc/public/pw_rpc/internal/method_union.h
@@ -14,8 +14,10 @@
 #pragma once
 
 #include <type_traits>
+#include <utility>
 
 #include "pw_rpc/internal/method.h"
+#include "pw_rpc/internal/method_type.h"
 
 namespace pw::rpc::internal {
 
@@ -27,44 +29,6 @@
   constexpr const Method& method() const;
 };
 
-// Templated false value for use in static_assert(false) statements.
-template <typename...>
-constexpr std::false_type kFalse{};
-
-// Traits struct that determines the type of an RPC service method from its
-// signature. Derived MethodUnions should provide specializations for their
-// method types.
-template <typename Method>
-struct MethodTraits {
-  static_assert(kFalse<Method>,
-                "The selected function is not an RPC service method");
-
-  // Specializations must set Implementation as an alias for their method
-  // implementation class.
-  using Implementation = Method;
-
-  // Specializations must set Service as an alias to the implemented service
-  // class.
-  using Service = rpc::Service;
-};
-
-template <auto method>
-using MethodImplementation =
-    typename MethodTraits<decltype(method)>::Implementation;
-
-template <auto method>
-using MethodService = typename MethodTraits<decltype(method)>::Service;
-
-// Identifies a base class from a member function it defines. This should be
-// used with decltype to retrieve the base class.
-template <typename T, typename U>
-T BaseFromMember(U T::*);
-
-// The base generated service of an implemented RPC method.
-template <auto method>
-using MethodBaseService = decltype(
-    BaseFromMember(&MethodService<method>::_PwRpcInternalGeneratedBase));
-
 class CoreMethodUnion : public MethodUnion {
  public:
   constexpr const Method& method() const { return impl_.method; }
@@ -84,4 +48,84 @@
   return static_cast<const CoreMethodUnion*>(this)->method();
 }
 
+// Templated false value for use in static_assert(false) statements.
+template <typename...>
+constexpr std::false_type kCheckMethodSignature{};
+
+// In static_assert messages, use newlines in GCC since it displays them
+// correctly. Clang displays \n, which is not helpful.
+#ifdef __clang__
+#define _PW_RPC_FORMAT_ERROR_MESSAGE(msg, signature) msg " " signature
+#else
+#define _PW_RPC_FORMAT_ERROR_MESSAGE(msg, signature) \
+  "\n" msg "\n\n    " signature "\n"
+#endif  // __clang__
+
+#define _PW_RPC_FUNCTION_ERROR(type, return_type, args)                   \
+  _PW_RPC_FORMAT_ERROR_MESSAGE(                                           \
+      "This RPC is a " type                                               \
+      " RPC, but its function signature is not correct. The function "    \
+      "signature is determined by the protobuf library in use, but " type \
+      " RPC implementations generally take the form:",                    \
+      return_type " MethodName(ServerContext&, " args ")")
+
+// This function is called if an RPC method implementation's signature is not
+// correct. It triggers a static_assert with an error message tailored to the
+// expected RPC type.
+template <auto method,
+          MethodType expected,
+          typename InvalidImpl = MethodImplementation<method>>
+constexpr auto InvalidMethod(uint32_t) {
+  if constexpr (expected == MethodType::kUnary) {
+    static_assert(
+        kCheckMethodSignature<decltype(method)>,
+        _PW_RPC_FUNCTION_ERROR("unary", "Status", "Request, Response"));
+  } else if constexpr (expected == MethodType::kServerStreaming) {
+    static_assert(
+        kCheckMethodSignature<decltype(method)>,
+        _PW_RPC_FUNCTION_ERROR(
+            "server streaming", "void", "Request, ServerWriter<Response>&"));
+  } else if constexpr (expected == MethodType::kClientStreaming) {
+    static_assert(
+        kCheckMethodSignature<decltype(method)>,
+        _PW_RPC_FUNCTION_ERROR(
+            "client streaming", "Status", "ServerReader<Request>&, Response"));
+  } else if constexpr (expected == MethodType::kBidirectionalStreaming) {
+    static_assert(kCheckMethodSignature<decltype(method)>,
+                  _PW_RPC_FUNCTION_ERROR(
+                      "bidirectional streaming",
+                      "void",
+                      "ServerReader<Request>&, ServerWriter<Response>&"));
+  } else {
+    static_assert(kCheckMethodSignature<decltype(method)>,
+                  "Unsupported MethodType");
+  }
+  return InvalidImpl::Invalid();
+}
+
+#undef _PW_RPC_FORMAT_ERROR_MESSAGE
+#undef _PW_RPC_FUNCTION_ERROR
+
+// This function checks the type of the method and calls the appropriate
+// function to create the method instance.
+template <auto method, typename MethodImpl, MethodType type, typename... Args>
+constexpr auto GetMethodFor(uint32_t id, Args&&... args) {
+  if constexpr (MethodTraits<decltype(method)>::kType != type) {
+    return InvalidMethod<method, type>(id);
+  } else if constexpr (type == MethodType::kUnary) {
+    return MethodImpl::template Unary<method>(id, std::forward<Args>(args)...);
+  } else if constexpr (type == MethodType::kServerStreaming) {
+    return MethodImpl::template ServerStreaming<method>(
+        id, std::forward<Args>(args)...);
+  } else if constexpr (type == MethodType::kClientStreaming) {
+    return MethodImpl::template ClientStreaming<method>(
+        id, std::forward<Args>(args)...);
+  } else if constexpr (type == MethodType::kBidirectionalStreaming) {
+    return MethodImpl::template BidirectionalStreaming<method>(
+        id, std::forward<Args>(args)...);
+  } else {
+    static_assert(kCheckMethodSignature<MethodImpl>, "Invalid MethodType");
+  }
+}
+
 }  // namespace pw::rpc::internal
diff --git a/pw_rpc/public/pw_rpc/internal/service_method_traits.h b/pw_rpc/public/pw_rpc/internal/service_method_traits.h
deleted file mode 100644
index 0e26b8d..0000000
--- a/pw_rpc/public/pw_rpc/internal/service_method_traits.h
+++ /dev/null
@@ -1,42 +0,0 @@
-// 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.
-#pragma once
-
-#include "pw_rpc/internal/method_union.h"
-
-namespace pw::rpc::internal {
-
-// Gets information about a service and method at compile-time. Uses a pointer
-// to a member function of the service implementation to identify the service
-// class, generated service class, and Method object.
-template <auto lookup_function, auto impl_method, uint32_t method_id>
-class ServiceMethodTraits {
- public:
-  ServiceMethodTraits() = delete;
-
-  // Type of the service implementation derived class.
-  using Service = MethodService<impl_method>;
-
-  using MethodImpl = MethodImplementation<impl_method>;
-
-  // Reference to the Method object corresponding to this method.
-  static constexpr const MethodImpl& method() {
-    return *lookup_function(method_id);
-  }
-
-  static_assert(lookup_function(method_id) != nullptr,
-                "The selected function is not an RPC service method");
-};
-
-}  // namespace pw::rpc::internal
diff --git a/pw_rpc/py/pw_rpc/codegen_nanopb.py b/pw_rpc/py/pw_rpc/codegen_nanopb.py
index 9e5ca78..0e33498 100644
--- a/pw_rpc/py/pw_rpc/codegen_nanopb.py
+++ b/pw_rpc/py/pw_rpc/codegen_nanopb.py
@@ -53,7 +53,8 @@
     impl_method = f'&Implementation::{method.name()}'
 
     output.write_line(
-        f'{RPC_NAMESPACE}::internal::GetNanopbOrRawMethodFor<{impl_method}>(')
+        f'{RPC_NAMESPACE}::internal::GetNanopbOrRawMethodFor<{impl_method}, '
+        f'{method.type().cc_enum()}>(')
     with output.indent(4):
         output.write_line(f'0x{method_id:08x},  // Hash of "{method.name()}"')
         output.write_line(f'{req_fields},')
diff --git a/pw_rpc/py/pw_rpc/codegen_raw.py b/pw_rpc/py/pw_rpc/codegen_raw.py
index 6babe1c..6a271cf 100644
--- a/pw_rpc/py/pw_rpc/codegen_raw.py
+++ b/pw_rpc/py/pw_rpc/codegen_raw.py
@@ -44,7 +44,8 @@
     impl_method = f'&Implementation::{method.name()}'
 
     output.write_line(
-        f'{RPC_NAMESPACE}::internal::GetRawMethodFor<{impl_method}>(')
+        f'{RPC_NAMESPACE}::internal::GetRawMethodFor<{impl_method}, '
+        f'{method.type().cc_enum()}>(')
     output.write_line(f'    0x{method_id:08x}),  // Hash of "{method.name()}"')
 
 
diff --git a/pw_rpc/raw/BUILD b/pw_rpc/raw/BUILD
index 229718e..193ef17 100644
--- a/pw_rpc/raw/BUILD
+++ b/pw_rpc/raw/BUILD
@@ -33,7 +33,7 @@
     deps = [
         "//pw_bytes",
         "//pw_rpc:server",
-    ]
+    ],
 )
 
 pw_cc_library(
@@ -43,18 +43,7 @@
     ],
     deps = [
         ":method",
-    ]
-)
-
-pw_cc_library(
-    name = "service_method_traits",
-    hdrs = [
-        "public/pw_rpc/internal/raw_service_method_traits.h",
     ],
-    deps = [
-        ":method_union",
-        "//pw_rpc:service_method_traits",
-    ]
 )
 
 pw_cc_library(
@@ -66,7 +55,7 @@
         ":method_union",
         "//pw_assert",
         "//pw_containers",
-    ]
+    ],
 )
 
 pw_cc_test(
diff --git a/pw_rpc/raw/BUILD.gn b/pw_rpc/raw/BUILD.gn
index 89d0492..e39214e 100644
--- a/pw_rpc/raw/BUILD.gn
+++ b/pw_rpc/raw/BUILD.gn
@@ -40,20 +40,11 @@
   public_deps = [ ":method" ]
 }
 
-pw_source_set("service_method_traits") {
-  public_configs = [ ":public" ]
-  public = [ "public/pw_rpc/internal/raw_service_method_traits.h" ]
-  public_deps = [
-    ":method_union",
-    "..:service_method_traits",
-  ]
-}
-
 pw_source_set("test_method_context") {
   public_configs = [ ":public" ]
   public = [ "public/pw_rpc/raw_test_method_context.h" ]
   public_deps = [
-    ":service_method_traits",
+    ":method",
     dir_pw_assert,
     dir_pw_containers,
   ]
@@ -79,8 +70,10 @@
 
 pw_test("raw_method_test") {
   deps = [
+    ":method",
     ":method_union",
     "..:test_protos.pwpb",
+    "..:test_protos.raw_rpc",
     "..:test_utils",
     dir_pw_protobuf,
   ]
diff --git a/pw_rpc/raw/codegen_test.cc b/pw_rpc/raw/codegen_test.cc
index 6194031..722c5de 100644
--- a/pw_rpc/raw/codegen_test.cc
+++ b/pw_rpc/raw/codegen_test.cc
@@ -24,9 +24,9 @@
 
 class TestService final : public generated::TestService<TestService> {
  public:
-  StatusWithSize TestRpc(ServerContext&,
-                         ConstByteSpan request,
-                         ByteSpan response) {
+  static StatusWithSize TestRpc(ServerContext&,
+                                ConstByteSpan request,
+                                ByteSpan response) {
     int64_t integer;
     Status status;
     DecodeRequest(request, integer, status);
@@ -57,7 +57,9 @@
   }
 
  private:
-  void DecodeRequest(ConstByteSpan request, int64_t& integer, Status& status) {
+  static void DecodeRequest(ConstByteSpan request,
+                            int64_t& integer,
+                            Status& status) {
     protobuf::Decoder decoder(request);
 
     while (decoder.Next().ok()) {
diff --git a/pw_rpc/raw/public/pw_rpc/internal/raw_method.h b/pw_rpc/raw/public/pw_rpc/internal/raw_method.h
index a1e3b66..6d2e898 100644
--- a/pw_rpc/raw/public/pw_rpc/internal/raw_method.h
+++ b/pw_rpc/raw/public/pw_rpc/internal/raw_method.h
@@ -49,26 +49,33 @@
 class RawMethod : public Method {
  public:
   template <auto method>
-  constexpr static RawMethod Unary(uint32_t id) {
-    return RawMethod(
-        id,
-        UnaryInvoker,
-        {.unary = [](ServerCall& call, ConstByteSpan req, ByteSpan res) {
-          return method(call, req, res);
-        }});
+  static constexpr bool matches() {
+    return std::is_same_v<MethodImplementation<method>, RawMethod>;
   }
 
   template <auto method>
-  constexpr static RawMethod ServerStreaming(uint32_t id) {
-    return RawMethod(id,
-                     ServerStreamingInvoker,
-                     Function{.server_streaming = [](ServerCall& call,
-                                                     ConstByteSpan req,
-                                                     BaseServerWriter& writer) {
-                       method(call, req, static_cast<RawServerWriter&>(writer));
-                     }});
+  static constexpr RawMethod Unary(uint32_t id) {
+    constexpr UnaryFunction wrapper =
+        [](ServerCall& call, ConstByteSpan req, ByteSpan res) {
+          return CallMethodImplFunction<method>(call, req, res);
+        };
+    return RawMethod(id, UnaryInvoker, Function{.unary = wrapper});
   }
 
+  template <auto method>
+  static constexpr RawMethod ServerStreaming(uint32_t id) {
+    constexpr ServerStreamingFunction wrapper =
+        [](ServerCall& call, ConstByteSpan request, BaseServerWriter& writer) {
+          CallMethodImplFunction<method>(
+              call, request, static_cast<RawServerWriter&>(writer));
+        };
+    return RawMethod(
+        id, ServerStreamingInvoker, Function{.server_streaming = wrapper});
+  }
+
+  // Represents an invalid method. Used to reduce error message verbosity.
+  static constexpr RawMethod Invalid() { return {0, InvalidInvoker, {}}; }
+
  private:
   using UnaryFunction = StatusWithSize (*)(ServerCall&,
                                            ConstByteSpan,
@@ -105,5 +112,38 @@
   Function function_;
 };
 
+// MethodTraits specialization for a static raw unary method.
+template <>
+struct MethodTraits<StatusWithSize (*)(
+    ServerContext&, ConstByteSpan, ByteSpan)> {
+  using Implementation = RawMethod;
+  static constexpr MethodType kType = MethodType::kUnary;
+};
+
+// MethodTraits specialization for a raw unary method.
+template <typename T>
+struct MethodTraits<StatusWithSize (T::*)(
+    ServerContext&, ConstByteSpan, ByteSpan)> {
+  using Implementation = RawMethod;
+  static constexpr MethodType kType = MethodType::kUnary;
+  using Service = T;
+};
+
+// MethodTraits specialization for a static raw server streaming method.
+template <>
+struct MethodTraits<void (*)(ServerContext&, ConstByteSpan, RawServerWriter&)> {
+  using Implementation = RawMethod;
+  static constexpr MethodType kType = MethodType::kServerStreaming;
+};
+
+// MethodTraits specialization for a raw server streaming method.
+template <typename T>
+struct MethodTraits<void (T::*)(
+    ServerContext&, ConstByteSpan, RawServerWriter&)> {
+  using Implementation = RawMethod;
+  static constexpr MethodType kType = MethodType::kServerStreaming;
+  using Service = T;
+};
+
 }  // namespace internal
 }  // namespace pw::rpc
diff --git a/pw_rpc/raw/public/pw_rpc/internal/raw_method_union.h b/pw_rpc/raw/public/pw_rpc/internal/raw_method_union.h
index 90267c5..7b377d7 100644
--- a/pw_rpc/raw/public/pw_rpc/internal/raw_method_union.h
+++ b/pw_rpc/raw/public/pw_rpc/internal/raw_method_union.h
@@ -36,58 +36,15 @@
   } impl_;
 };
 
-// MethodTraits specialization for a unary method.
-template <typename T>
-struct MethodTraits<StatusWithSize (T::*)(
-    ServerContext&, ConstByteSpan, ByteSpan)> {
-  static constexpr MethodType kType = MethodType::kUnary;
-  using Service = T;
-  using Implementation = RawMethod;
-};
-
-// MethodTraits specialization for a raw server streaming method.
-template <typename T>
-struct MethodTraits<void (T::*)(
-    ServerContext&, ConstByteSpan, RawServerWriter&)> {
-  static constexpr MethodType kType = MethodType::kServerStreaming;
-  using Service = T;
-  using Implementation = RawMethod;
-};
-
-template <auto method>
-constexpr bool kIsRaw = std::is_same_v<MethodImplementation<method>, RawMethod>;
-
 // Deduces the type of an implemented service method from its signature, and
 // returns the appropriate MethodUnion object to invoke it.
-template <auto method>
+template <auto method, MethodType type>
 constexpr RawMethod GetRawMethodFor(uint32_t id) {
-  static_assert(kIsRaw<method>,
-                "GetRawMethodFor should only be called on raw RPC methods");
-
-  using Traits = MethodTraits<decltype(method)>;
-  using ServiceImpl = typename Traits::Service;
-
-  if constexpr (Traits::kType == MethodType::kUnary) {
-    constexpr auto invoker =
-        +[](ServerCall& call, ConstByteSpan request, ByteSpan response) {
-          return (static_cast<ServiceImpl&>(call.service()).*method)(
-              call.context(), request, response);
-        };
-    return RawMethod::Unary<invoker>(id);
+  if constexpr (RawMethod::matches<method>()) {
+    return GetMethodFor<method, RawMethod, type>(id);
+  } else {
+    return InvalidMethod<method, type, RawMethod>(id);
   }
-
-  if constexpr (Traits::kType == MethodType::kServerStreaming) {
-    constexpr auto invoker =
-        +[](ServerCall& call, ConstByteSpan request, RawServerWriter& writer) {
-          (static_cast<ServiceImpl&>(call.service()).*method)(
-              call.context(), request, writer);
-        };
-    return RawMethod::ServerStreaming<invoker>(id);
-  }
-
-  constexpr auto fake_invoker =
-      +[](ServerCall&, ConstByteSpan, RawServerWriter&) {};
-  return RawMethod::ServerStreaming<fake_invoker>(0);
 };
 
 }  // namespace pw::rpc::internal
diff --git a/pw_rpc/raw/public/pw_rpc/internal/raw_service_method_traits.h b/pw_rpc/raw/public/pw_rpc/internal/raw_service_method_traits.h
deleted file mode 100644
index e304fc2..0000000
--- a/pw_rpc/raw/public/pw_rpc/internal/raw_service_method_traits.h
+++ /dev/null
@@ -1,27 +0,0 @@
-// 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.
-#pragma once
-
-#include "pw_rpc/internal/raw_method_union.h"
-#include "pw_rpc/internal/service_method_traits.h"
-
-namespace pw::rpc::internal {
-
-template <auto impl_method, uint32_t method_id>
-using RawServiceMethodTraits =
-    ServiceMethodTraits<&MethodBaseService<impl_method>::RawMethodFor,
-                        impl_method,
-                        method_id>;
-
-}  // namespace pw::rpc::internal
diff --git a/pw_rpc/raw/public/pw_rpc/raw_test_method_context.h b/pw_rpc/raw/public/pw_rpc/raw_test_method_context.h
index 14e831e..371e0f5 100644
--- a/pw_rpc/raw/public/pw_rpc/raw_test_method_context.h
+++ b/pw_rpc/raw/public/pw_rpc/raw_test_method_context.h
@@ -21,7 +21,7 @@
 #include "pw_rpc/channel.h"
 #include "pw_rpc/internal/hash.h"
 #include "pw_rpc/internal/packet.h"
-#include "pw_rpc/internal/raw_service_method_traits.h"
+#include "pw_rpc/internal/raw_method.h"
 #include "pw_rpc/internal/server.h"
 
 namespace pw::rpc {
@@ -73,10 +73,12 @@
 //   ASSERT_EQ(3u, context.responses().max_size());
 //
 #define PW_RAW_TEST_METHOD_CONTEXT(service, method, ...)              \
-  ::pw::rpc::RawTestMethodContext<&service::method,                   \
+  ::pw::rpc::RawTestMethodContext<service,                            \
+                                  &service::method,                   \
                                   ::pw::rpc::internal::Hash(#method), \
                                   ##__VA_ARGS__>
-template <auto method,
+template <typename Service,
+          auto method,
           uint32_t method_id,
           size_t max_responses = 4,
           size_t output_size_bytes = 128>
@@ -129,8 +131,17 @@
   Status last_status_;
 };
 
+template <typename Service, uint32_t method_id>
+constexpr const internal::RawMethod& GetRawMethod() {
+  constexpr const internal::RawMethod* raw_method =
+      GeneratedService<Service>::RawMethodFor(method_id);
+  static_assert(raw_method != nullptr,
+                "The selected function is not an RPC service method");
+  return *raw_method;
+}
+
 // Collects everything needed to invoke a particular RPC.
-template <auto method,
+template <typename Service,
           uint32_t method_id,
           size_t max_responses,
           size_t output_size>
@@ -144,10 +155,9 @@
         call(static_cast<internal::Server&>(server),
              static_cast<internal::Channel&>(channel),
              service,
-             RawServiceMethodTraits<method, method_id>::method()) {}
+             GetRawMethod<Service, method_id>()) {}
 
   using ResponseBuffer = std::array<std::byte, output_size>;
-  using Service = typename RawServiceMethodTraits<method, method_id>::Service;
 
   MessageOutput<output_size> output;
   rpc::Channel channel;
@@ -161,17 +171,17 @@
 
 // Method invocation context for a unary RPC. Returns the status in call() and
 // provides the response through the response() method.
-template <auto method, uint32_t method_id, size_t output_size>
+template <typename Service, auto method, uint32_t method_id, size_t output_size>
 class UnaryContext {
  private:
-  using Context = InvocationContext<method, method_id, 1, output_size>;
+  using Context = InvocationContext<Service, method_id, 1, output_size>;
   Context ctx_;
 
  public:
   template <typename... Args>
   UnaryContext(Args&&... args) : ctx_(std::forward<Args>(args)...) {}
 
-  typename Context::Service& service() { return ctx_.service; }
+  Service& service() { return ctx_.service; }
 
   // Invokes the RPC with the provided request. Returns RPC's StatusWithSize.
   StatusWithSize call(ConstByteSpan request) {
@@ -181,7 +191,7 @@
     ctx_.responses.emplace_back();
     auto& response = ctx_.responses.back();
     response = {ctx_.buffers.back().data(), ctx_.buffers.back().size()};
-    auto sws = (ctx_.service.*method)(ctx_.call.context(), request, response);
+    auto sws = CallMethodImplFunction<method>(ctx_.call, request, response);
     response = response.first(sws.size());
     return sws;
   }
@@ -194,29 +204,29 @@
 };
 
 // Method invocation context for a server streaming RPC.
-template <auto method,
+template <typename Service,
+          auto method,
           uint32_t method_id,
           size_t max_responses,
           size_t output_size>
 class ServerStreamingContext {
  private:
   using Context =
-      InvocationContext<method, method_id, max_responses, output_size>;
+      InvocationContext<Service, method_id, max_responses, output_size>;
   Context ctx_;
 
  public:
   template <typename... Args>
   ServerStreamingContext(Args&&... args) : ctx_(std::forward<Args>(args)...) {}
 
-  typename Context::Service& service() { return ctx_.service; }
+  Service& service() { return ctx_.service; }
 
   // Invokes the RPC with the provided request.
   void call(ConstByteSpan request) {
     ctx_.output.clear();
     BaseServerWriter server_writer(ctx_.call);
-    return (ctx_.service.*method)(ctx_.call.context(),
-                                  request,
-                                  static_cast<RawServerWriter&>(server_writer));
+    return CallMethodImplFunction<method>(
+        ctx_.call, request, static_cast<RawServerWriter&>(server_writer));
   }
 
   // Returns a server writer which writes responses into the context's buffer.
@@ -248,11 +258,19 @@
 
 // Alias to select the type of the context object to use based on which type of
 // RPC it is for.
-template <auto method, uint32_t method_id, size_t responses, size_t output_size>
+template <typename Service,
+          auto method,
+          uint32_t method_id,
+          size_t responses,
+          size_t output_size>
 using Context = std::tuple_element_t<
     static_cast<size_t>(MethodTraits<decltype(method)>::kType),
-    std::tuple<UnaryContext<method, method_id, output_size>,
-               ServerStreamingContext<method, method_id, responses, output_size>
+    std::tuple<UnaryContext<Service, method, method_id, output_size>,
+               ServerStreamingContext<Service,
+                                      method,
+                                      method_id,
+                                      responses,
+                                      output_size>
                // TODO(hepler): Support client and bidi streaming
                >>;
 
@@ -294,20 +312,27 @@
 
 }  // namespace internal::test::raw
 
-template <auto method,
+template <typename Service,
+          auto method,
           uint32_t method_id,
           size_t max_responses,
           size_t output_size_bytes>
 class RawTestMethodContext
-    : public internal::test::raw::
-          Context<method, method_id, max_responses, output_size_bytes> {
+    : public internal::test::raw::Context<Service,
+                                          method,
+                                          method_id,
+                                          max_responses,
+                                          output_size_bytes> {
  public:
   // Forwards constructor arguments to the service class.
   template <typename... ServiceArgs>
   RawTestMethodContext(ServiceArgs&&... service_args)
-      : internal::test::raw::
-            Context<method, method_id, max_responses, output_size_bytes>(
-                std::forward<ServiceArgs>(service_args)...) {}
+      : internal::test::raw::Context<Service,
+                                     method,
+                                     method_id,
+                                     max_responses,
+                                     output_size_bytes>(
+            std::forward<ServiceArgs>(service_args)...) {}
 };
 
 }  // namespace pw::rpc
diff --git a/pw_rpc/raw/raw_method_test.cc b/pw_rpc/raw/raw_method_test.cc
index 7679660..c2dfb3e 100644
--- a/pw_rpc/raw/raw_method_test.cc
+++ b/pw_rpc/raw/raw_method_test.cc
@@ -53,7 +53,9 @@
   }
 };
 
-StatusWithSize AddFive(ServerCall&, ConstByteSpan request, ByteSpan response) {
+StatusWithSize AddFive(ServerContext&,
+                       ConstByteSpan request,
+                       ByteSpan response) {
   DecodeRawTestRequest(request);
 
   protobuf::NestedEncoder encoder(response);
@@ -65,7 +67,9 @@
   return StatusWithSize::Unauthenticated(payload.size());
 }
 
-void StartStream(ServerCall&, ConstByteSpan request, RawServerWriter& writer) {
+void StartStream(ServerContext&,
+                 ConstByteSpan request,
+                 RawServerWriter& writer) {
   DecodeRawTestRequest(request);
   last_writer = std::move(writer);
 }
diff --git a/pw_rpc/raw/raw_method_union_test.cc b/pw_rpc/raw/raw_method_union_test.cc
index 599601e..77cfd5d 100644
--- a/pw_rpc/raw/raw_method_union_test.cc
+++ b/pw_rpc/raw/raw_method_union_test.cc
@@ -34,9 +34,10 @@
   constexpr FakeGeneratedService(uint32_t id) : Service(id, kMethods) {}
 
   static constexpr std::array<RawMethodUnion, 3> kMethods = {
-      GetRawMethodFor<&Implementation::DoNothing>(10u),
-      GetRawMethodFor<&Implementation::AddFive>(11u),
-      GetRawMethodFor<&Implementation::StartStream>(12u),
+      GetRawMethodFor<&Implementation::DoNothing, MethodType::kUnary>(10u),
+      GetRawMethodFor<&Implementation::AddFive, MethodType::kUnary>(11u),
+      GetRawMethodFor<&Implementation::StartStream,
+                      MethodType::kServerStreaming>(12u),
   };
 };