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)