pw_rpc: Generate service implementation stubs

- Generate a service implementation stub in the RPC proto header. This
  can be used as a reference or copied when implementing a service.
- Expand the documentation for code generation.
- Reduce code duplication in codegen.
- Remove codegen_test.py, which was not running because of a missing
  unittest.main() line. The test was intended as an end-to-end test for
  RPC codegen, but it was out of date and is no longer necessary because
  the C++ codegen tests cover it.

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