pw_protobuf: Support codegen for StreamingEncoder

Introduces codegen support for the new StreamingEncoder and
MemoryEncoder classes. The MemoryEncoder will eventually replace what is
now the pw::protobuf::Encoder.

Bug: 384
Change-Id: Ib7e3c3def5b7e062d595db82c76455c491a08250
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/45362
Reviewed-by: Alexei Frolov <frolv@google.com>
Commit-Queue: Armando Montanez <amontanez@google.com>
diff --git a/pw_protobuf/codegen_test.cc b/pw_protobuf/codegen_test.cc
index b62ce14..bce8564 100644
--- a/pw_protobuf/codegen_test.cc
+++ b/pw_protobuf/codegen_test.cc
@@ -15,6 +15,7 @@
 
 #include "gtest/gtest.h"
 #include "pw_protobuf/encoder.h"
+#include "pw_stream/memory_stream.h"
 
 // These header files contain the code generated by the pw_protobuf plugin.
 // They are re-generated every time the tests are built and are used by the
@@ -36,47 +37,54 @@
 
 TEST(Codegen, Codegen) {
   std::byte encode_buffer[512];
-  NestedEncoder<20, 20> encoder(encode_buffer);
+  std::byte temp_buffer[512];
+  stream::MemoryWriter writer(encode_buffer);
 
-  Pigweed::Encoder pigweed(&encoder);
+  Pigweed::StreamEncoder pigweed(writer, temp_buffer);
   pigweed.WriteMagicNumber(73);
   pigweed.WriteZiggy(-111);
   pigweed.WriteErrorMessage("not a typewriter");
   pigweed.WriteBin(Pigweed::Protobuf::Binary::ZERO);
 
   {
-    Pigweed::Pigweed::Encoder pigweed_pigweed = pigweed.GetPigweedEncoder();
+    Pigweed::Pigweed::StreamEncoder pigweed_pigweed =
+        pigweed.GetPigweedEncoder();
     pigweed_pigweed.WriteStatus(Bool::FILE_NOT_FOUND);
+
+    ASSERT_EQ(pigweed_pigweed.status(), OkStatus());
   }
 
   {
-    Proto::Encoder proto = pigweed.GetProtoEncoder();
+    Proto::StreamEncoder proto = pigweed.GetProtoEncoder();
     proto.WriteBin(Proto::Binary::OFF);
     proto.WritePigweedPigweedBin(Pigweed::Pigweed::Binary::ZERO);
     proto.WritePigweedProtobufBin(Pigweed::Protobuf::Binary::ZERO);
 
     {
-      Pigweed::Protobuf::Compiler::Encoder meta = proto.GetMetaEncoder();
+      Pigweed::Protobuf::Compiler::StreamEncoder meta = proto.GetMetaEncoder();
       meta.WriteFileName("/etc/passwd");
       meta.WriteStatus(Pigweed::Protobuf::Compiler::Status::FUBAR);
     }
 
     {
-      Pigweed::Encoder nested_pigweed = proto.GetPigweedEncoder();
-      pigweed.WriteErrorMessage("here we go again");
-      pigweed.WriteMagicNumber(616);
+      Pigweed::StreamEncoder nested_pigweed = proto.GetPigweedEncoder();
+      nested_pigweed.WriteErrorMessage("here we go again");
+      nested_pigweed.WriteMagicNumber(616);
 
       {
-        DeviceInfo::Encoder device_info = nested_pigweed.GetDeviceInfoEncoder();
+        DeviceInfo::StreamEncoder device_info =
+            nested_pigweed.GetDeviceInfoEncoder();
 
         {
-          KeyValuePair::Encoder attributes = device_info.GetAttributesEncoder();
+          KeyValuePair::StreamEncoder attributes =
+              device_info.GetAttributesEncoder();
           attributes.WriteKey("version");
           attributes.WriteValue("5.3.1");
         }
 
         {
-          KeyValuePair::Encoder attributes = device_info.GetAttributesEncoder();
+          KeyValuePair::StreamEncoder attributes =
+              device_info.GetAttributesEncoder();
           attributes.WriteKey("chip");
           attributes.WriteValue("left-soc");
         }
@@ -87,7 +95,7 @@
   }
 
   for (int i = 0; i < 5; ++i) {
-    Proto::ID::Encoder id = pigweed.GetIdEncoder();
+    Proto::ID::StreamEncoder id = pigweed.GetIdEncoder();
     id.WriteId(5 * i * i + 3 * i + 49);
   }
 
@@ -166,11 +174,10 @@
   };
   // clang-format on
 
-  Result result = encoder.Encode();
-  ASSERT_EQ(result.status(), OkStatus());
-  EXPECT_EQ(result.value().size(), sizeof(expected_proto));
-  EXPECT_EQ(std::memcmp(
-                result.value().data(), expected_proto, sizeof(expected_proto)),
+  ConstByteSpan result = writer.WrittenData();
+  ASSERT_EQ(pigweed.status(), OkStatus());
+  EXPECT_EQ(result.size(), sizeof(expected_proto));
+  EXPECT_EQ(std::memcmp(result.data(), expected_proto, sizeof(expected_proto)),
             0);
 }
 
@@ -260,9 +267,7 @@
 
 TEST(CodegenRepeated, NonScalar) {
   std::byte encode_buffer[32];
-  NestedEncoder encoder(encode_buffer);
-
-  RepeatedTest::Encoder repeated_test(&encoder);
+  RepeatedTest::RamEncoder repeated_test(encode_buffer);
   constexpr const char* strings[] = {"the", "quick", "brown", "fox"};
   for (const char* s : strings) {
     repeated_test.WriteStrings(s);
@@ -271,11 +276,10 @@
   constexpr uint8_t expected_proto[] = {
       0x1a, 0x03, 't', 'h', 'e', 0x1a, 0x5, 'q',  'u', 'i', 'c', 'k',
       0x1a, 0x5,  'b', 'r', 'o', 'w',  'n', 0x1a, 0x3, 'f', 'o', 'x'};
-  Result result = encoder.Encode();
-  ASSERT_EQ(result.status(), OkStatus());
-  EXPECT_EQ(result.value().size(), sizeof(expected_proto));
-  EXPECT_EQ(std::memcmp(
-                result.value().data(), expected_proto, sizeof(expected_proto)),
+  ConstByteSpan result(repeated_test);
+  ASSERT_EQ(repeated_test.status(), OkStatus());
+  EXPECT_EQ(result.size(), sizeof(expected_proto));
+  EXPECT_EQ(std::memcmp(result.data(), expected_proto, sizeof(expected_proto)),
             0);
 }
 
diff --git a/pw_protobuf/encoding.rst b/pw_protobuf/encoding.rst
index a7259db..340e711 100644
--- a/pw_protobuf/encoding.rst
+++ b/pw_protobuf/encoding.rst
@@ -120,3 +120,95 @@
 While individual write calls on a proto encoder return pw::Status objects, the
 encoder tracks all status returns and "latches" onto the first error
 encountered. This status can be accessed via ``StreamingEncoder::status()``.
+
+Codegen
+-------
+pw_protobuf encoder codegen integration is supported in GN, Bazel, and CMake.
+The codegen is just a light wrapper around the ``StreamEncoder`` and
+``MemoryEncoder`` objects, providing named helper functions to write proto
+fields rather than requiring that field numbers are directly passed to an
+encoder. Namespaced proto enums are also generated, and used as the arguments
+when writing enum fields of a proto message.
+
+All generated messages provide a ``Fields`` enum that can be used directly for
+out-of-band encoding, or with the ``pw::protobuf::Decoder``.
+
+This module's codegen is available through the ``*.pwpb`` sub-target of a
+``pw_proto_library`` in GN, CMake, and Bazel. See :ref:`pw_protobuf_compiler's
+documentation <module-pw_protobuf_compiler>` for more information on build
+system integration for pw_protobuf codegen.
+
+Example ``BUILD.gn``:
+
+.. Code:: none
+
+  import("//build_overrides/pigweed.gni")
+
+  import("$dir_pw_build/target_types.gni")
+  import("$dir_pw_protobuf_compiler/proto.gni")
+
+  # This target controls where the *.pwpb.h headers end up on the include path.
+  # In this example, it's at "pet_daycare_protos/client.pwpb.h".
+  pw_proto_library("pet_daycare_protos") {
+    sources = [
+      "pet_daycare_protos/client.proto",
+    ]
+  }
+
+  pw_source_set("example_client") {
+    sources = [ "example_client.cc" ]
+    deps = [
+      ":pet_daycare_protos.pwpb",
+      dir_pw_bytes,
+      dir_pw_stream,
+    ]
+  }
+
+Example ``pet_daycare_protos/client.proto``:
+
+.. Code:: none
+
+  syntax = "proto3";
+  // The proto package controls the namespacing of the codegen. If this package
+  // were fuzzy.friends, the namespace for codegen would be fuzzy::friends::*.
+  package fuzzy_friends;
+
+  message Pet {
+    string name = 1;
+    string pet_type = 2;
+  }
+
+  message Client {
+    repeated Pet pets = 1;
+  }
+
+Example ``example_client.cc``:
+
+.. Code:: cpp
+
+  #include "pet_daycare_protos/client.pwpb.h"
+  #include "pw_protobuf/streaming_encoder.h"
+  #include "pw_stream/sys_io_stream.h"
+  #include "pw_bytes/span.h"
+
+  pw::stream::SysIoWriter sys_io_writer;
+  std::byte submessage_scratch_buffer[64];
+  // The constructor is the same as a pw::protobuf::StreamingEncoder.
+  fuzzy_friends::Client::StreamEncoder client(sys_io_writer,
+                                              submessage_scratch_buffer);
+
+  fuzzy_friends::Pet::StreamEncoder pet1 = client.GetPetsEncoder();
+
+  pet1.WriteName("Spot");
+  pet1.WritePetType("dog");
+  PW_CHECK_OK(pet1.Finalize());
+
+  {  // Since pet2 is scoped, it will automatically Finalize() on destruction.
+    fuzzy_friends::Pet::StreamEncoder pet2 = client.GetPetsEncoder();
+    pet2.WriteName("Slippers");
+    pet2.WritePetType("rabbit");
+  }
+
+  if (!client.status().ok()) {
+    PW_LOG_INFO("Failed to encode proto; %s", client.status().str());
+  }
diff --git a/pw_protobuf/public/pw_protobuf/streaming_encoder.h b/pw_protobuf/public/pw_protobuf/streaming_encoder.h
index 2e3d761..c19a21f 100644
--- a/pw_protobuf/public/pw_protobuf/streaming_encoder.h
+++ b/pw_protobuf/public/pw_protobuf/streaming_encoder.h
@@ -81,12 +81,11 @@
   ~StreamingEncoder() { Finalize(); }
 
   // Disallow copy/assign to avoid confusion about who owns the buffer.
-  StreamingEncoder(const StreamingEncoder& other) = delete;
   StreamingEncoder& operator=(const StreamingEncoder& other) = delete;
+  StreamingEncoder(const StreamingEncoder& other) = delete;
 
   // It's not safe to move an encoder as it could cause another encoder's
   // parent_ pointer to become invalid.
-  StreamingEncoder(StreamingEncoder&& other) = delete;
   StreamingEncoder& operator=(StreamingEncoder&& other) = delete;
 
   // Forwards the conservative write limit of the underlying pw::stream::Writer.
@@ -362,6 +361,39 @@
     return WriteBytes(field_number, std::as_bytes(std::span(value)));
   }
 
+  // Writes a proto string key-value pair.
+  //
+  // Precondition: Encoder has no active child encoder.
+  Status WriteString(uint32_t field_number, const char* value, size_t len) {
+    return WriteBytes(field_number, std::as_bytes(std::span(value, len)));
+  }
+
+  // Writes a proto string key-value pair.
+  // TODO(384): This function is not safe and will be removed as part of the
+  // transition away from the old protobuf encoder.
+  //
+  // Precondition: Encoder has no active child encoder.
+  Status WriteString(uint32_t field_number, const char* value) {
+    return WriteBytes(field_number,
+                      std::as_bytes(std::span(std::string_view(value))));
+  }
+
+ protected:
+  // We need this for codegen.
+  constexpr StreamingEncoder(StreamingEncoder&& other)
+      : writer_(&other.writer_ == &other.memory_writer_ ? memory_writer_
+                                                        : other.writer_),
+        status_(other.status_),
+        parent_(other.parent_),
+        nested_field_number_(other.nested_field_number_),
+        memory_writer_(std::move(other.memory_writer_)) {
+    PW_ASSERT(nested_field_number_ == 0);
+    // Make the nested encoder look like it has an open child to block writes
+    // for the remainder of the object's life.
+    other.nested_field_number_ = kFirstReservedNumber;
+    other.parent_ = nullptr;
+  }
+
  private:
   friend class MemoryEncoder;
 
@@ -518,11 +550,14 @@
 
   // It's not safe to move an encoder as it could cause another encoder's
   // parent_ pointer to become invalid.
-  MemoryEncoder(MemoryEncoder&& other) = delete;
   MemoryEncoder& operator=(MemoryEncoder&& other) = delete;
 
   const std::byte* data() const { return memory_writer_.data(); }
   size_t size() const { return memory_writer_.bytes_written(); }
+
+ protected:
+  // This is needed by codegen.
+  MemoryEncoder(MemoryEncoder&& other) = default;
 };
 
 }  // namespace pw::protobuf
diff --git a/pw_protobuf/py/pw_protobuf/codegen_pwpb.py b/pw_protobuf/py/pw_protobuf/codegen_pwpb.py
index e0b9293..d103018 100644
--- a/pw_protobuf/py/pw_protobuf/codegen_pwpb.py
+++ b/pw_protobuf/py/pw_protobuf/codegen_pwpb.py
@@ -15,6 +15,7 @@
 
 import abc
 from datetime import datetime
+import enum
 import os
 import sys
 from typing import Dict, Iterable, List, Tuple
@@ -34,7 +35,35 @@
 PROTO_CC_EXTENSION = '.pwpb.cc'
 
 PROTOBUF_NAMESPACE = 'pw::protobuf'
-BASE_PROTO_CLASS = 'ProtoMessageEncoder'
+
+
+class EncoderType(enum.Enum):
+    MEMORY = 1
+    STREAMING = 2
+    LEGACY = 3
+
+    def base_class_name(self) -> str:
+        """Returns the base class used by this encoder type."""
+        if self is self.LEGACY:
+            return 'ProtoMessageEncoder'
+        if self is self.STREAMING:
+            return 'StreamingEncoder'
+        if self is self.MEMORY:
+            return 'MemoryEncoder'
+
+        raise ValueError('Unknown encoder type')
+
+    def codegen_class_name(self) -> str:
+        """Returns the base class used by this encoder type."""
+        if self is self.LEGACY:
+            return 'Encoder'
+        if self is self.STREAMING:
+            return 'StreamEncoder'
+        if self is self.MEMORY:
+            # TODO(pwbug/384): Make this the 'Encoder'
+            return 'RamEncoder'
+
+        raise ValueError('Unknown encoder type')
 
 
 # protoc captures stdout, so we need to printf debug to stderr.
@@ -73,7 +102,7 @@
         """
 
     @abc.abstractmethod
-    def body(self) -> List[str]:
+    def body(self, encoder_type: EncoderType) -> List[str]:
         """Returns the method body as a list of source code lines.
 
         e.g.
@@ -84,7 +113,9 @@
         """
 
     @abc.abstractmethod
-    def return_type(self, from_root: bool = False) -> str:
+    def return_type(self,
+                    encoder_type: EncoderType,
+                    from_root: bool = False) -> str:
         """Returns the return type of the method, e.g. int.
 
         For non-primitive return types, the from_root argument determines
@@ -134,15 +165,27 @@
     def name(self) -> str:
         return 'Get{}Encoder'.format(self._field.name())
 
-    def return_type(self, from_root: bool = False) -> str:
-        return '{}::Encoder'.format(self._relative_type_namespace(from_root))
+    def return_type(self,
+                    encoder_type: EncoderType,
+                    from_root: bool = False) -> str:
+        if encoder_type == EncoderType.LEGACY:
+            return '{}::Encoder'.format(
+                self._relative_type_namespace(from_root))
+
+        return '{}::StreamEncoder'.format(
+            self._relative_type_namespace(from_root))
 
     def params(self) -> List[Tuple[str, str]]:
         return []
 
-    def body(self) -> List[str]:
-        line = 'return {}::Encoder(encoder_, {});'.format(
-            self._relative_type_namespace(), self.field_cast())
+    def body(self, encoder_type: EncoderType) -> List[str]:
+        line: str = ''
+        if encoder_type == EncoderType.LEGACY:
+            line = 'return {}::Encoder(encoder_, {});'.format(
+                self._relative_type_namespace(), self.field_cast())
+        else:
+            line = 'return {}::StreamEncoder(GetNestedEncoder({}));'.format(
+                self._relative_type_namespace(), self.field_cast())
         return [line]
 
     # Submessage methods are not defined within the class itself because the
@@ -164,13 +207,19 @@
     def name(self) -> str:
         return 'Write{}'.format(self._field.name())
 
-    def return_type(self, from_root: bool = False) -> str:
+    def return_type(self,
+                    encoder_type: EncoderType,
+                    from_root: bool = False) -> str:
         return '::pw::Status'
 
-    def body(self) -> List[str]:
+    def body(self, encoder_type: EncoderType) -> List[str]:
         params = ', '.join([pair[1] for pair in self.params()])
-        line = 'return encoder_->{}({}, {});'.format(self._encoder_fn(),
-                                                     self.field_cast(), params)
+        if encoder_type == EncoderType.LEGACY:
+            line = 'return encoder_->{}({}, {});'.format(
+                self._encoder_fn(), self.field_cast(), params)
+        else:
+            line = 'return {}({}, {});'.format(self._encoder_fn(),
+                                               self.field_cast(), params)
         return [line]
 
     def params(self) -> List[Tuple[str, str]]:
@@ -465,9 +514,14 @@
     def params(self) -> List[Tuple[str, str]]:
         return [(self._relative_type_namespace(), 'value')]
 
-    def body(self) -> List[str]:
-        line = 'return encoder_->WriteUint32(' \
-            '{}, static_cast<uint32_t>(value));'.format(self.field_cast())
+    def body(self, encoder_type: EncoderType) -> List[str]:
+        line: str = ''
+        if encoder_type == EncoderType.LEGACY:
+            line = 'return encoder_->WriteUint32(' \
+                '{}, static_cast<uint32_t>(value));'.format(self.field_cast())
+        else:
+            line = 'return WriteUint32(' \
+                '{}, static_cast<uint32_t>(value));'.format(self.field_cast())
         return [line]
 
     def in_class_definition(self) -> bool:
@@ -514,20 +568,33 @@
 
 
 def generate_code_for_message(message: ProtoMessage, root: ProtoNode,
-                              output: OutputFile) -> None:
+                              output: OutputFile,
+                              encoder_type: EncoderType) -> None:
     """Creates a C++ class for a protobuf message."""
     assert message.type() == ProtoNode.Type.MESSAGE
 
+    base_class_name = encoder_type.base_class_name()
+    encoder_name = encoder_type.codegen_class_name()
+
     # Message classes inherit from the base proto message class in codegen.h
     # and use its constructor.
-    base_class = f'{PROTOBUF_NAMESPACE}::{BASE_PROTO_CLASS}'
+    base_class = f'{PROTOBUF_NAMESPACE}::{base_class_name}'
     output.write_line(
-        f'class {message.cpp_namespace(root)}::Encoder : public {base_class} {{'
+        f'class {message.cpp_namespace(root)}::{encoder_name} ' \
+        f': public {base_class} {{'
     )
     output.write_line(' public:')
 
     with output.indent():
-        output.write_line(f'using {BASE_PROTO_CLASS}::{BASE_PROTO_CLASS};')
+        # Inherit the constructors from the base encoder.
+        output.write_line(f'using {base_class_name}::{base_class_name};')
+
+        # Declare a move constructor that takes a base encoder.
+        if encoder_type != EncoderType.LEGACY:
+            output.write_line(
+                f'constexpr {encoder_name}({base_class_name}&& parent) '\
+                f': {base_class_name}(std::move(parent)) {{}}'
+            )
 
         # Generate methods for each of the message's fields.
         for field in message.fields():
@@ -538,7 +605,7 @@
 
                 output.write_line()
                 method_signature = (
-                    f'{method.return_type()} '
+                    f'{method.return_type(encoder_type)} '
                     f'{method.name()}({method.param_string()})')
 
                 if not method.in_class_definition():
@@ -549,7 +616,7 @@
 
                 output.write_line(f'{method_signature} {{')
                 with output.indent():
-                    for line in method.body():
+                    for line in method.body(encoder_type):
                         output.write_line(line)
                 output.write_line('}')
 
@@ -557,7 +624,8 @@
 
 
 def define_not_in_class_methods(message: ProtoMessage, root: ProtoNode,
-                                output: OutputFile) -> None:
+                                output: OutputFile,
+                                encoder_type: EncoderType) -> None:
     """Defines methods for a message class that were previously declared."""
     assert message.type() == ProtoNode.Type.MESSAGE
 
@@ -568,25 +636,26 @@
                 continue
 
             output.write_line()
-            class_name = f'{message.cpp_namespace(root)}::Encoder'
+            class_name = (f'{message.cpp_namespace(root)}::'
+                          f'{encoder_type.codegen_class_name()}')
             method_signature = (
-                f'inline {method.return_type(from_root=True)} '
+                f'inline {method.return_type(encoder_type, from_root=True)} '
                 f'{class_name}::{method.name()}({method.param_string()})')
             output.write_line(f'{method_signature} {{')
             with output.indent():
-                for line in method.body():
+                for line in method.body(encoder_type):
                     output.write_line(line)
             output.write_line('}')
 
 
-def generate_code_for_enum(enum: ProtoEnum, root: ProtoNode,
+def generate_code_for_enum(proto_enum: ProtoEnum, root: ProtoNode,
                            output: OutputFile) -> None:
     """Creates a C++ enum for a proto enum."""
-    assert enum.type() == ProtoNode.Type.ENUM
+    assert proto_enum.type() == ProtoNode.Type.ENUM
 
-    output.write_line(f'enum class {enum.cpp_namespace(root)} {{')
+    output.write_line(f'enum class {proto_enum.cpp_namespace(root)} {{')
     with output.indent():
-        for name, number in enum.values():
+        for name, number in proto_enum.values():
             output.write_line(f'{name} = {number},')
     output.write_line('};')
 
@@ -608,6 +677,9 @@
     # Declare the message's encoder class and all of its enums.
     output.write_line()
     output.write_line('class Encoder;')
+    output.write_line('class StreamEncoder;')
+    output.write_line('class RamEncoder;')
+
     for child in node.children():
         if child.type() == ProtoNode.Type.ENUM:
             output.write_line()
@@ -616,6 +688,23 @@
     output.write_line(f'}}  // namespace {namespace}')
 
 
+def generate_encoder_wrappers(package: ProtoNode, encoder_type: EncoderType,
+                              output: OutputFile):
+    # Run through all messages in the file, generating a class for each.
+    for node in package:
+        if node.type() == ProtoNode.Type.MESSAGE:
+            output.write_line()
+            generate_code_for_message(cast(ProtoMessage, node), package,
+                                      output, encoder_type)
+
+    # Run a second pass through the classes, this time defining all of the
+    # methods which were previously only declared.
+    for node in package:
+        if node.type() == ProtoNode.Type.MESSAGE:
+            define_not_in_class_methods(cast(ProtoMessage, node), package,
+                                        output, encoder_type)
+
+
 def _proto_filename_to_generated_header(proto_file: str) -> str:
     """Returns the generated C++ header name for a .proto file."""
     return os.path.splitext(proto_file)[0] + PROTO_H_EXTENSION
@@ -635,6 +724,7 @@
     output.write_line('#include <cstdint>')
     output.write_line('#include <span>\n')
     output.write_line('#include "pw_protobuf/codegen.h"')
+    output.write_line('#include "pw_protobuf/streaming_encoder.h"')
 
     for imported_file in file_descriptor_proto.dependency:
         generated_header = _proto_filename_to_generated_header(imported_file)
@@ -657,19 +747,9 @@
             output.write_line()
             generate_code_for_enum(cast(ProtoEnum, node), package, output)
 
-    # Run through all messages in the file, generating a class for each.
-    for node in package:
-        if node.type() == ProtoNode.Type.MESSAGE:
-            output.write_line()
-            generate_code_for_message(cast(ProtoMessage, node), package,
-                                      output)
-
-    # Run a second pass through the classes, this time defining all of the
-    # methods which were previously only declared.
-    for node in package:
-        if node.type() == ProtoNode.Type.MESSAGE:
-            define_not_in_class_methods(cast(ProtoMessage, node), package,
-                                        output)
+    generate_encoder_wrappers(package, EncoderType.LEGACY, output)
+    generate_encoder_wrappers(package, EncoderType.STREAMING, output)
+    generate_encoder_wrappers(package, EncoderType.MEMORY, output)
 
     if package.cpp_namespace():
         output.write_line(f'\n}}  // namespace {package.cpp_namespace()}')
diff --git a/pw_protobuf_compiler/docs.rst b/pw_protobuf_compiler/docs.rst
index fc9ed7b..99e3d18 100644
--- a/pw_protobuf_compiler/docs.rst
+++ b/pw_protobuf_compiler/docs.rst
@@ -33,10 +33,9 @@
 
 GN template
 ===========
-The ``pw_proto_library`` GN template is provided by the module.
-
-It defines a collection of protobuf files that should be compiled together. The
-template creates a sub-target for each supported generator, named
+This module provides a ``pw_proto_library`` GN template that defines a
+collection of protobuf files that should be compiled together. The template
+creates a sub-target for each supported generator, named
 ``<target_name>.<generator>``. These sub-targets generate their respective
 protobuf code, and expose it to the build system appropriately (e.g. a
 ``pw_source_set`` for C/C++).
@@ -72,6 +71,20 @@
   //path/to/my_protos:my_protos.python.lint
   //path/to/my_protos:python.lint
 
+**Supported Codegen**
+
+GN supports the following compiled proto libraries via the specified
+sub-targets generated by a ``pw_proto_library``.
+
+* ``${target_name}.pwpb`` - Generated C++ pw_protobuf code
+* ``${target_name}.nanopb`` - Generated C++ nanopb code (requires Nanopb)
+* ``${target_name}.nanopb_rpc`` - Generated C++ Nanopb pw_rpc code (requires
+  Nanopb)
+* ``${target_name}.raw_rpc`` - Generated C++ raw pw_rpc code (no protobuf
+  library)
+* ``${target_name}.go`` - Generated GO protobuf libraries
+* ``${target_name}.python`` - Generated Python protobuf libraries
+
 **Arguments**
 
 * ``sources``: List of input .proto files.
@@ -303,6 +316,16 @@
 
   #include "my_other_protos/baz.pwpb.h"
 
+**Supported Codegen**
+
+CMake supports the following compiled proto libraries via the specified
+sub-targets generated by a ``pw_proto_library``.
+
+* ``${NAME}.pwpb`` - Generated C++ pw_protobuf code
+* ``${NAME}.nanopb`` - Generated C++ nanopb code (requires Nanopb)
+* ``${NAME}.nanopb_rpc`` - Generated C++ Nanopb pw_rpc code (requires Nanopb)
+* ``${NAME}.raw_rpc`` - Generated C++ raw pw_rpc code (no protobuf library)
+
 Bazel
 =====
 Bazel provides a ``pw_proto_library`` rule with similar features as the
@@ -356,4 +379,14 @@
 
 .. code:: cpp
 
-  #include "my_protos/bar.pwpb.h"
\ No newline at end of file
+  #include "my_protos/bar.pwpb.h"
+
+**Supported Codegen**
+
+Bazel supports the following compiled proto libraries via the specified
+sub-targets generated by a ``pw_proto_library``.
+
+* ``${NAME}.pwpb`` - Generated C++ pw_protobuf code
+* ``${NAME}.nanopb`` - Generated C++ nanopb code (requires Nanopb)
+* ``${NAME}.nanopb_rpc`` - Generated C++ Nanopb pw_rpc code (requires Nanopb)
+* ``${NAME}.raw_rpc`` - Generated C++ raw pw_rpc code (no protobuf library)