pw_protobuf_compiler: Add nanopb RPC generator

This adds a GN protobuf generator for compiling nanopb RPC code using
the pw_rpc compiler plugin.

Change-Id: Ida27ef6d2adf396a352227493f0982de2bfe7573
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/13340
Commit-Queue: Alexei Frolov <frolv@google.com>
Reviewed-by: Wyatt Hepler <hepler@google.com>
diff --git a/pw_protobuf_compiler/docs.rst b/pw_protobuf_compiler/docs.rst
index e33099e..4bb4cdc 100644
--- a/pw_protobuf_compiler/docs.rst
+++ b/pw_protobuf_compiler/docs.rst
@@ -16,18 +16,22 @@
 
 Protobuf code generation is currently supported for the following generators:
 
-+-------------+------------+---------------------------------------------------+
-| Generator   | Code       | Notes                                             |
-+-------------+------------+---------------------------------------------------+
-| pw_protobuf | ``pwpb``   | Compiles using ``pw_protobuf``.                   |
-+-------------+------------+---------------------------------------------------+
-| Go          | ``go``     | Compiles using the standard Go protobuf plugin    |
-|             |            | with gRPC service support.                        |
-+-------------+------------+---------------------------------------------------+
-| Nanopb      | ``nanopb`` | Compiles using Nanopb. The build argument         |
-|             |            | ``dir_pw_third_party_nanopb`` must be set to      |
-|             |            | point to a local nanopb installation.             |
-+-------------+------------+---------------------------------------------------+
++-------------+----------------+-----------------------------------------------+
+| Generator   | Code           | Notes                                         |
++-------------+----------------+-----------------------------------------------+
+| pw_protobuf | ``pwpb``       | Compiles using ``pw_protobuf``.               |
++-------------+----------------+-----------------------------------------------+
+| Go          | ``go``         | Compiles using the standard Go protobuf       |
+|             |                | plugin with gRPC service support.             |
++-------------+----------------+-----------------------------------------------+
+| Nanopb      | ``nanopb``     | Compiles using Nanopb. The build argument     |
+|             |                | ``dir_pw_third_party_nanopb`` must be set to  |
+|             |                | point to a local nanopb installation.         |
++-------------+----------------+-----------------------------------------------+
+| Nanopb RPC  | ``nanopb_rpc`` | Compiles pw_rpc service code for a nanopb     |
+|             |                | server. Requires the nanopb generator to be   |
+|             |                | configured as well.                           |
++-------------+----------------+-----------------------------------------------+
 
 The build variable ``pw_protobuf_GENERATORS`` tells the module the generators
 for which it should compile code. It is defined as a list of generator codes.
diff --git a/pw_protobuf_compiler/proto.gni b/pw_protobuf_compiler/proto.gni
index 09ebaa6..b9edfad 100644
--- a/pw_protobuf_compiler/proto.gni
+++ b/pw_protobuf_compiler/proto.gni
@@ -24,7 +24,7 @@
   # pw_proto_library template to determine which build targets to create.
   #
   # Supported generators:
-  #   "pwpb", "nanopb", "go"
+  #   "pwpb", "nanopb", "nanopb_rpc", "go"
   pw_protobuf_GENERATORS = [
     "pwpb",
     "go",
@@ -97,6 +97,78 @@
   }
 }
 
+# Generates nanopb RPC code for proto files, creating a source_set of the
+# generated files. This is internal and should not be used outside of this file.
+# Use pw_proto_library instead.
+#
+# Args:
+#  protos: List of input .proto files.
+#
+template("_pw_nanopb_rpc_proto_library") {
+  assert(defined(dir_pw_third_party_nanopb) && dir_pw_third_party_nanopb != "",
+         "\$dir_pw_third_party_nanopb must be set to compile nanopb protobufs")
+
+  _proto_gen_dir = "$root_gen_dir/protos"
+  _module_path = get_path_info(".", "abspath")
+  _relative_proto_paths = rebase_path(invoker.protos, _module_path)
+
+  _outputs = []
+  foreach(_proto, _relative_proto_paths) {
+    _output_h = string_replace(_proto, ".proto", "_rpc.pb.h")
+    _outputs += [ "$_proto_gen_dir/$_output_h" ]
+  }
+
+  # Create a target which runs protoc configured with the nanopb_rpc plugin to
+  # generate the C++ proto RPC headers.
+  _gen_target = "${target_name}_gen"
+  pw_python_script(_gen_target) {
+    forward_variables_from(invoker, _forwarded_vars)
+    script = _gen_script_path
+    args = [
+             "--language",
+             "nanopb_rpc",
+             "--module-path",
+             _module_path,
+             "--include-paths",
+             "$dir_pw_third_party_nanopb/generator/proto",
+             "--include-file",
+             invoker.include_file,
+             "--out-dir",
+             _proto_gen_dir,
+           ] + get_path_info(invoker.protos, "abspath")
+    inputs = invoker.protos
+    outputs = _outputs
+
+    deps = invoker.deps
+    if (defined(invoker.protoc_deps)) {
+      deps += invoker.protoc_deps
+    }
+  }
+
+  # For C++ proto files, the generated proto directory is added as an include
+  # path for the code. This requires using "all_dependent_configs" to force the
+  # include on any code that transitively depends on the generated protos.
+  _include_root = rebase_path(get_path_info(".", "abspath"), "//")
+  _include_config_target = "${target_name}_includes"
+  config(_include_config_target) {
+    include_dirs = [
+      "$_proto_gen_dir",
+      "$_proto_gen_dir/$_include_root",
+    ]
+  }
+
+  # Create a library with the generated source files.
+  pw_source_set(target_name) {
+    all_dependent_configs = [ ":$_include_config_target" ]
+    deps = [ ":$_gen_target" ]
+    public_deps = [
+                    dir_pw_third_party_nanopb,
+                    "$dir_pw_rpc:nanopb_server",
+                  ] + invoker.gen_deps
+    public = get_target_outputs(":$_gen_target")
+  }
+}
+
 # Generates nanopb code for proto files, creating a source_set of the generated
 # files. This is internal and should not be used outside of this file. Use
 # pw_proto_library instead.
@@ -272,11 +344,27 @@
     _deps += [ ":$_input_target_name" ]
   }
 
-  foreach(_gen, pw_protobuf_GENERATORS) {
+  # If the nanopb_rpc generator is selected, make sure that nanopb is also
+  # selected.
+  has_nanopb_rpc = pw_protobuf_GENERATORS + [ "nanopb_rpc" ] -
+                   [ "nanopb_rpc" ] != pw_protobuf_GENERATORS
+  if (has_nanopb_rpc) {
+    _generators =
+        pw_protobuf_GENERATORS + [ "nanopb" ] - [ "nanopb" ] + [ "nanopb" ]
+  } else {
+    _generators = pw_protobuf_GENERATORS
+  }
+
+  foreach(_gen, _generators) {
     _lang_target = "${target_name}_${_gen}"
     _gen_deps = []
     if (defined(invoker.deps)) {
       _gen_deps = process_file_template(invoker.deps, "{{source}}_${_gen}")
+
+      if (_gen == "nanopb_rpc") {
+        # Generated RPC code also depends on the core generated protos.
+        _gen_deps += process_file_template(invoker.deps, "{{source}}_nanopb")
+      }
     }
 
     if (_gen == "pwpb") {
@@ -291,6 +379,18 @@
         # generated code if they are modified.
         protoc_deps = [ "$dir_pw_protobuf:codegen_protoc_plugin" ]
       }
+    } else if (_gen == "nanopb_rpc") {
+      _pw_nanopb_rpc_proto_library(_lang_target) {
+        forward_variables_from(invoker, _forwarded_vars)
+        protos = invoker.sources
+        deps = _deps
+        include_file = _include_metadata_file
+        gen_deps = _gen_deps
+
+        # List the pw_protobuf plugin's files as a dependency to recompile
+        # generated code if they are modified.
+        protoc_deps = [ "$dir_pw_rpc:nanopb_protoc_plugin" ]
+      }
     } else if (_gen == "nanopb") {
       _pw_nanopb_proto_library(_lang_target) {
         forward_variables_from(invoker, _forwarded_vars)
@@ -322,11 +422,12 @@
   _protobuf_generators = [
     "pwpb",
     "nanopb",
+    "nanopb_rpc",
     "go",
   ]
 
   # Create stub versions of the proto library for other protobuf generators.
-  foreach(_gen, _protobuf_generators - pw_protobuf_GENERATORS) {
+  foreach(_gen, _protobuf_generators - _generators) {
     pw_python_script("${target_name}_${_gen}") {
       forward_variables_from(invoker, _forwarded_vars)
       script = string_join("/",
diff --git a/pw_protobuf_compiler/py/pw_protobuf_compiler/generate_protos.py b/pw_protobuf_compiler/py/pw_protobuf_compiler/generate_protos.py
index 96c8179..79c369a 100644
--- a/pw_protobuf_compiler/py/pw_protobuf_compiler/generate_protos.py
+++ b/pw_protobuf_compiler/py/pw_protobuf_compiler/generate_protos.py
@@ -77,12 +77,20 @@
     ]
 
 
+def protoc_nanopb_rpc_args(args: argparse.Namespace) -> List[str]:
+    return [
+        '--plugin', f'protoc-gen-custom={shutil.which("pw_rpc_codegen")}',
+        '--custom_out', args.out_dir
+    ]
+
+
 # Default additional protoc arguments for each supported language.
 # TODO(frolv): Make these overridable with a command-line argument.
 DEFAULT_PROTOC_ARGS: Dict[str, Callable[[argparse.Namespace], List[str]]] = {
     'cc': protoc_cc_args,
     'go': protoc_go_args,
     'nanopb': protoc_nanopb_args,
+    'nanopb_rpc': protoc_nanopb_rpc_args,
 }
 
 
diff --git a/pw_rpc/BUILD.gn b/pw_rpc/BUILD.gn
index 2cc9a8f..62fbe3c 100644
--- a/pw_rpc/BUILD.gn
+++ b/pw_rpc/BUILD.gn
@@ -127,6 +127,15 @@
   sources = [ "pw_rpc_protos/packet.proto" ]
 }
 
+# Source files for pw_protobuf's protoc plugin.
+pw_input_group("nanopb_protoc_plugin") {
+  inputs = [
+    "py/pw_rpc/codegen_nanopb.py",
+    "py/pw_rpc/plugin.py",
+    "py/pw_rpc/ids.py",
+  ]
+}
+
 pw_doc_group("docs") {
   sources = [ "docs.rst" ]
 }
@@ -137,6 +146,7 @@
     ":channel_test",
     ":packet_test",
     ":server_test",
+    "nanopb:codegen_test",
     "nanopb:method_test",
   ]
 }
diff --git a/pw_rpc/nanopb/BUILD.gn b/pw_rpc/nanopb/BUILD.gn
index a8b0cc8..f5b4689 100644
--- a/pw_rpc/nanopb/BUILD.gn
+++ b/pw_rpc/nanopb/BUILD.gn
@@ -45,3 +45,12 @@
   sources = [ "method_test.cc" ]
   enable_if = dir_pw_third_party_nanopb != ""
 }
+
+pw_test("codegen_test") {
+  deps = [
+    "..:nanopb_server",
+    "..:test_protos_nanopb_rpc",
+  ]
+  sources = [ "codegen_test.cc" ]
+  enable_if = dir_pw_third_party_nanopb != ""
+}
diff --git a/pw_rpc/py/pw_rpc/codegen_nanopb.py b/pw_rpc/py/pw_rpc/codegen_nanopb.py
index 27b2573..f9f3f05 100644
--- a/pw_rpc/py/pw_rpc/codegen_nanopb.py
+++ b/pw_rpc/py/pw_rpc/codegen_nanopb.py
@@ -112,8 +112,8 @@
                           f'{RPC_NAMESPACE}::ServerWriter<T>;')
         output.write_line()
 
-        output.write_line(f'constexpr {service.name()}()'
-                          f' : {base_class}(kServiceId, kMethods) {{}}')
+        output.write_line(f'constexpr {service.name()}()')
+        output.write_line(f'    : {base_class}(kServiceId, kMethods) {{}}')
 
         output.write_line()
         output.write_line(
@@ -121,6 +121,10 @@
         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()}"; }}')
+
         for method in service.methods():
             _generate_code_for_method(method, output)
 
@@ -132,8 +136,6 @@
         output.write_line(
             f'static constexpr uint32_t kServiceId = {hex(service_name_hash)};'
         )
-        output.write_line(
-            f'static constexpr char* kServiceName = "{service.name()}";')
         output.write_line()
 
         output.write_line(