Add pw_protobuf module
This change adds a pw_protobuf module containing a lightweight protobuf
wire format encoder. The encoder comes with a Python script that plugs
into protoc to generate C++ classes from Protobuf files that wrap its
functionality.
Bug: 20
Change-Id: I867655ab64c2f6ddd2a731054b1fbe7ccc97ba70
diff --git a/.pylintrc b/.pylintrc
index cac6c64..3445b61 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -356,7 +356,7 @@
# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E1101 when accessed. Python regular
# expressions are accepted.
-generated-members=
+generated-members=descriptor_pb2.*,plugin_pb2.*
# Tells whether missing members accessed in mixin class should be ignored. A
# mixin class is detected if its name ends with "mixin" (case insensitive).
diff --git a/BUILD.gn b/BUILD.gn
index 5514010..869c619 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -41,6 +41,7 @@
"$dir_pigweed/docs",
"$dir_pw_bloat",
"$dir_pw_preprocessor",
+ "$dir_pw_protobuf",
"$dir_pw_span",
"$dir_pw_status",
"$dir_pw_string",
@@ -53,6 +54,7 @@
pw_test_group("pw_module_tests") {
group_deps = [
"$dir_pw_preprocessor:tests",
+ "$dir_pw_protobuf:tests",
"$dir_pw_span:tests",
"$dir_pw_status:tests",
"$dir_pw_string:tests",
diff --git a/modules.gni b/modules.gni
index 19e6b07..2b30893 100644
--- a/modules.gni
+++ b/modules.gni
@@ -34,6 +34,7 @@
dir_pw_module = "$dir_pigweed/pw_module"
dir_pw_preprocessor = "$dir_pigweed/pw_preprocessor"
dir_pw_presubmit = "$dir_pigweed/pw_presubmit"
+dir_pw_protobuf = "$dir_pigweed/pw_protobuf"
dir_pw_protobuf_compiler = "$dir_pigweed/pw_protobuf_compiler"
dir_pw_span = "$dir_pigweed/pw_span"
dir_pw_status = "$dir_pigweed/pw_status"
diff --git a/pw_build/pigweed.bzl b/pw_build/pigweed.bzl
index 278cdd9..94ecc71 100644
--- a/pw_build/pigweed.bzl
+++ b/pw_build/pigweed.bzl
@@ -43,6 +43,7 @@
"-Ipw_bloat/public",
"-Ipw_dumb_io/public",
"-Ipw_preprocessor/public",
+ "-Ipw_protobuf/public",
"-Ipw_span/public",
"-Ipw_status/public",
"-Ipw_string/public",
diff --git a/pw_protobuf/BUILD b/pw_protobuf/BUILD
new file mode 100644
index 0000000..6b97ea4
--- /dev/null
+++ b/pw_protobuf/BUILD
@@ -0,0 +1,52 @@
+# Copyright 2019 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.
+
+load(
+ "//pw_build:pigweed.bzl",
+ "pw_cc_library",
+ "pw_cc_test",
+)
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"]) # Apache License 2.0
+
+pw_cc_library(
+ name = "pw_protobuf",
+ srcs = [
+ "encoder.cc",
+ ],
+ hdrs = [
+ "public/pw_protobuf/codegen.h",
+ "public/pw_protobuf/encoder.h",
+ ],
+ deps = [
+ "//pw_status",
+ "//pw_varint",
+ ],
+)
+
+pw_cc_test(
+ name = "encoder_test",
+ srcs = ["encoder_test.cc"],
+ deps = ["//pw_protobuf"],
+)
+
+# TODO(frolv): Figure out how to integrate pw_protobuf codegen into Bazel.
+filegroup(
+ name = "codegen_test",
+ srcs = [
+ "codegen_test.cc",
+ ],
+)
diff --git a/pw_protobuf/BUILD.gn b/pw_protobuf/BUILD.gn
new file mode 100644
index 0000000..3afa792
--- /dev/null
+++ b/pw_protobuf/BUILD.gn
@@ -0,0 +1,86 @@
+# Copyright 2019 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.
+
+import("$dir_pw_bloat/bloat.gni")
+import("$dir_pw_build/input_group.gni")
+import("$dir_pw_build/pw_executable.gni")
+import("$dir_pw_protobuf_compiler/proto.gni")
+import("$dir_pw_unit_test/test.gni")
+
+config("default_config") {
+ include_dirs = [ "public" ]
+}
+
+source_set("pw_protobuf") {
+ public_configs = [
+ "$dir_pw_build:pw_default_cpp",
+ ":default_config",
+ ]
+ public_deps = [
+ "$dir_pw_status",
+ "$dir_pw_varint",
+ ]
+ sources = [
+ "encoder.cc",
+ "public/pw_protobuf/codegen.h",
+ "public/pw_protobuf/encoder.h",
+ ]
+ public = [
+ "public/pw_protobuf/codegen.h",
+ "public/pw_protobuf/encoder.h",
+ ]
+}
+
+# Source files for pw_protobuf's protoc plugin.
+pw_input_group("codegen_protoc_plugin") {
+ inputs = [
+ "py/pw_protobuf/codegen.py",
+ "py/pw_protobuf/methods.py",
+ "py/pw_protobuf/proto_structures.py",
+ ]
+}
+
+pw_test_group("tests") {
+ tests = [
+ ":codegen_test",
+ ":encoder_test",
+ ]
+}
+
+pw_test("encoder_test") {
+ deps = [
+ ":pw_protobuf",
+ ]
+ sources = [
+ "encoder_test.cc",
+ ]
+}
+
+pw_test("codegen_test") {
+ deps = [
+ ":codegen_test_protos_cc",
+ ":pw_protobuf",
+ ]
+ sources = [
+ "codegen_test.cc",
+ ]
+}
+
+pw_proto_library("codegen_test_protos") {
+ sources = [
+ "pw_protobuf_protos/test_protos/full_test.proto",
+ "pw_protobuf_protos/test_protos/proto2.proto",
+ "pw_protobuf_protos/test_protos/repeated.proto",
+ ]
+}
diff --git a/pw_protobuf/README.md b/pw_protobuf/README.md
new file mode 100644
index 0000000..1321e96
--- /dev/null
+++ b/pw_protobuf/README.md
@@ -0,0 +1 @@
+# pw\_protobuf: A tiny Protocol Buffers implementation
diff --git a/pw_protobuf/codegen_test.cc b/pw_protobuf/codegen_test.cc
new file mode 100644
index 0000000..a441f3a
--- /dev/null
+++ b/pw_protobuf/codegen_test.cc
@@ -0,0 +1,277 @@
+// Copyright 2019 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "gtest/gtest.h"
+#include "pw_protobuf/encoder.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
+// tests to ensure that the interface remains consistent.
+//
+// The purpose of the tests in this file is primarily to verify that the
+// generated C++ interface is valid rather than the correctness of the
+// low-level encoder.
+#include "pw_protobuf_protos/test_protos/full_test.pb.h"
+#include "pw_protobuf_protos/test_protos/proto2.pb.h"
+#include "pw_protobuf_protos/test_protos/repeated.pb.h"
+
+namespace pw::protobuf {
+namespace {
+
+using namespace pw::protobuf::test;
+
+TEST(Codegen, Codegen) {
+ uint8_t encode_buffer[512];
+ NestedEncoder<20, 20> encoder(encode_buffer);
+
+ Pigweed::Encoder pigweed(&encoder);
+ pigweed.WriteMagicNumber(73);
+ pigweed.WriteZiggy(-111);
+ pigweed.WriteErrorMessage("not a typewriter");
+ pigweed.WriteBin(Pigweed::Protobuf::Binary::kZero);
+
+ {
+ Pigweed::Pigweed::Encoder pigweed_pigweed = pigweed.GetPigweedEncoder();
+ pigweed_pigweed.WriteStatus(Bool::kFileNotFound);
+ }
+
+ {
+ Proto::Encoder proto = pigweed.GetProtoEncoder();
+ proto.WriteBin(Proto::Binary::kOff);
+ proto.WritePigweedPigweedBin(Pigweed::Pigweed::Binary::kZero);
+ proto.WritePigweedProtobufBin(Pigweed::Protobuf::Binary::kZero);
+
+ {
+ Pigweed::Protobuf::Compiler::Encoder meta = proto.GetMetaEncoder();
+ meta.WriteFileName("/etc/passwd");
+ meta.WriteStatus(Pigweed::Protobuf::Compiler::Status::kFubar);
+ }
+
+ {
+ Pigweed::Encoder nested_pigweed = proto.GetPigweedEncoder();
+ pigweed.WriteErrorMessage("here we go again");
+ pigweed.WriteMagicNumber(616);
+
+ {
+ DeviceInfo::Encoder device_info = nested_pigweed.GetDeviceInfoEncoder();
+
+ {
+ KeyValuePair::Encoder attributes = device_info.GetAttributesEncoder();
+ attributes.WriteKey("version");
+ attributes.WriteValue("5.3.1");
+ }
+
+ {
+ KeyValuePair::Encoder attributes = device_info.GetAttributesEncoder();
+ attributes.WriteKey("chip");
+ attributes.WriteValue("left-soc");
+ }
+
+ device_info.WriteStatus(DeviceInfo::DeviceStatus::kPanic);
+ }
+ }
+ }
+
+ for (int i = 0; i < 5; ++i) {
+ Proto::ID::Encoder id = pigweed.GetIdEncoder();
+ id.WriteId(5 * i * i + 3 * i + 49);
+ }
+
+ // clang-format off
+ constexpr uint8_t expected_proto[] = {
+ // pigweed.magic_number
+ 0x08, 0x49,
+ // pigweed.ziggy
+ 0x10, 0xdd, 0x01,
+ // pigweed.error_message
+ 0x2a, 0x10, 'n', 'o', 't', ' ', 'a', ' ',
+ 't', 'y', 'p', 'e', 'w', 'r', 'i', 't', 'e', 'r',
+ // pigweed.bin
+ 0x40, 0x01,
+ // pigweed.pigweed
+ 0x3a, 0x02,
+ // pigweed.pigweed.status
+ 0x08, 0x02,
+ // pigweed.proto
+ 0x4a, 0x56,
+ // pigweed.proto.bin
+ 0x10, 0x00,
+ // pigweed.proto.pigweed_pigweed_bin
+ 0x18, 0x00,
+ // pigweed.proto.pigweed_protobuf_bin
+ 0x20, 0x01,
+ // pigweed.proto.meta
+ 0x2a, 0x0f,
+ // pigweed.proto.meta.file_name
+ 0x0a, 0x0b, '/', 'e', 't', 'c', '/', 'p', 'a', 's', 's', 'w', 'd',
+ // pigweed.proto.meta.status
+ 0x10, 0x02,
+ // pigweed.proto.nested_pigweed
+ 0x0a, 0x3d,
+ // pigweed.proto.nested_pigweed.error_message
+ 0x2a, 0x10, 'h', 'e', 'r', 'e', ' ', 'w', 'e', ' ',
+ 'g', 'o', ' ', 'a', 'g', 'a', 'i', 'n',
+ // pigweed.proto.nested_pigweed.magic_number
+ 0x08, 0xe8, 0x04,
+ // pigweed.proto.nested_pigweed.device_info
+ 0x32, 0x26,
+ // pigweed.proto.nested_pigweed.device_info.attributes[0]
+ 0x22, 0x10,
+ // pigweed.proto.nested_pigweed.device_info.attributes[0].key
+ 0x0a, 0x07, 'v', 'e', 'r', 's', 'i', 'o', 'n',
+ // pigweed.proto.nested_pigweed.device_info.attributes[0].value
+ 0x12, 0x05, '5', '.', '3', '.', '1',
+ // pigweed.proto.nested_pigweed.device_info.attributes[1]
+ 0x22, 0x10,
+ // pigweed.proto.nested_pigweed.device_info.attributes[1].key
+ 0x0a, 0x04, 'c', 'h', 'i', 'p',
+ // pigweed.proto.nested_pigweed.device_info.attributes[1].value
+ 0x12, 0x08, 'l', 'e', 'f', 't', '-', 's', 'o', 'c',
+ // pigweed.proto.nested_pigweed.device_info.status
+ 0x18, 0x03,
+ // pigweed.id[0]
+ 0x52, 0x02,
+ // pigweed.id[0].id
+ 0x08, 0x31,
+ // pigweed.id[1]
+ 0x52, 0x02,
+ // pigweed.id[1].id
+ 0x08, 0x39,
+ // pigweed.id[2]
+ 0x52, 0x02,
+ // pigweed.id[2].id
+ 0x08, 0x4b,
+ // pigweed.id[3]
+ 0x52, 0x02,
+ // pigweed.id[3].id
+ 0x08, 0x67,
+ // pigweed.id[4]
+ 0x52, 0x03,
+ // pigweed.id[4].id
+ 0x08, 0x8d, 0x01
+ };
+ // clang-format on
+
+ span<const uint8_t> proto;
+ EXPECT_EQ(encoder.Encode(&proto), Status::OK);
+ EXPECT_EQ(proto.size(), sizeof(expected_proto));
+ EXPECT_EQ(std::memcmp(proto.data(), expected_proto, sizeof(expected_proto)),
+ 0);
+}
+
+TEST(CodegenRepeated, NonPackedScalar) {
+ uint8_t encode_buffer[32];
+ NestedEncoder encoder(encode_buffer);
+
+ RepeatedTest::Encoder repeated_test(&encoder);
+ for (int i = 0; i < 4; ++i) {
+ repeated_test.WriteUint32s(i * 16);
+ }
+
+ constexpr uint8_t expected_proto[] = {
+ 0x08, 0x00, 0x08, 0x10, 0x08, 0x20, 0x08, 0x30};
+
+ span<const uint8_t> proto;
+ EXPECT_EQ(encoder.Encode(&proto), Status::OK);
+ EXPECT_EQ(proto.size(), sizeof(expected_proto));
+ EXPECT_EQ(std::memcmp(proto.data(), expected_proto, sizeof(expected_proto)),
+ 0);
+}
+
+TEST(CodegenRepeated, PackedScalar) {
+ uint8_t encode_buffer[32];
+ NestedEncoder encoder(encode_buffer);
+
+ RepeatedTest::Encoder repeated_test(&encoder);
+ constexpr uint32_t values[] = {0, 16, 32, 48};
+ repeated_test.WriteUint32s(values);
+
+ constexpr uint8_t expected_proto[] = {0x0a, 0x04, 0x00, 0x10, 0x20, 0x30};
+ span<const uint8_t> proto;
+ EXPECT_EQ(encoder.Encode(&proto), Status::OK);
+ EXPECT_EQ(proto.size(), sizeof(expected_proto));
+ EXPECT_EQ(std::memcmp(proto.data(), expected_proto, sizeof(expected_proto)),
+ 0);
+}
+
+TEST(CodegenRepeated, NonScalar) {
+ uint8_t encode_buffer[32];
+ NestedEncoder encoder(encode_buffer);
+
+ RepeatedTest::Encoder repeated_test(&encoder);
+ constexpr const char* strings[] = {"the", "quick", "brown", "fox"};
+ for (const char* s : strings) {
+ repeated_test.WriteStrings(s);
+ }
+
+ 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'};
+ span<const uint8_t> proto;
+ EXPECT_EQ(encoder.Encode(&proto), Status::OK);
+ EXPECT_EQ(proto.size(), sizeof(expected_proto));
+ EXPECT_EQ(std::memcmp(proto.data(), expected_proto, sizeof(expected_proto)),
+ 0);
+}
+
+TEST(CodegenRepeated, Message) {
+ uint8_t encode_buffer[64];
+ NestedEncoder<1, 3> encoder(encode_buffer);
+
+ RepeatedTest::Encoder repeated_test(&encoder);
+ for (int i = 0; i < 3; ++i) {
+ auto structs = repeated_test.GetStructsEncoder();
+ structs.WriteOne(i * 1);
+ structs.WriteTwo(i * 2);
+ }
+
+ // clang-format off
+ constexpr uint8_t expected_proto[] = {
+ 0x2a, 0x04, 0x08, 0x00, 0x10, 0x00, 0x2a, 0x04, 0x08,
+ 0x01, 0x10, 0x02, 0x2a, 0x04, 0x08, 0x02, 0x10, 0x04};
+ // clang-format on
+
+ span<const uint8_t> proto;
+ EXPECT_EQ(encoder.Encode(&proto), Status::OK);
+ EXPECT_EQ(proto.size(), sizeof(expected_proto));
+ EXPECT_EQ(std::memcmp(proto.data(), expected_proto, sizeof(expected_proto)),
+ 0);
+}
+
+TEST(Codegen, Proto2) {
+ uint8_t encode_buffer[64];
+ NestedEncoder<1, 3> encoder(encode_buffer);
+
+ Foo::Encoder foo(&encoder);
+ foo.WriteInt(3);
+
+ {
+ constexpr std::byte data[] = {
+ std::byte(0xde), std::byte(0xad), std::byte(0xbe), std::byte(0xef)};
+ Bar::Encoder bar = foo.GetBarEncoder();
+ bar.WriteData(data);
+ }
+
+ constexpr uint8_t expected_proto[] = {
+ 0x08, 0x03, 0x1a, 0x06, 0x0a, 0x04, 0xde, 0xad, 0xbe, 0xef};
+
+ span<const uint8_t> proto;
+ EXPECT_EQ(encoder.Encode(&proto), Status::OK);
+ EXPECT_EQ(proto.size(), sizeof(expected_proto));
+ EXPECT_EQ(std::memcmp(proto.data(), expected_proto, sizeof(expected_proto)),
+ 0);
+}
+
+} // namespace
+} // namespace pw::protobuf
diff --git a/pw_protobuf/encoder.cc b/pw_protobuf/encoder.cc
new file mode 100644
index 0000000..701e22d
--- /dev/null
+++ b/pw_protobuf/encoder.cc
@@ -0,0 +1,183 @@
+// Copyright 2019 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_protobuf/encoder.h"
+
+namespace pw::protobuf {
+
+Status Encoder::WriteUint64(uint32_t field_number, uint64_t value) {
+ uint8_t* original_cursor = cursor_;
+ WriteFieldKey(field_number, WireType::kVarint);
+ Status status = WriteVarint(value);
+ IncreaseParentSize(cursor_ - original_cursor);
+ return status;
+}
+
+// Encodes a base-128 varint to the buffer.
+Status Encoder::WriteVarint(uint64_t integer) {
+ if (!encode_status_.ok()) {
+ return encode_status_;
+ }
+
+ span varint_buf = buffer_.last(RemainingSize());
+ if (varint_buf.empty()) {
+ encode_status_ = Status::RESOURCE_EXHAUSTED;
+ return encode_status_;
+ }
+
+ size_t written = pw::varint::EncodeLittleEndianBase128(integer, varint_buf);
+ if (written == 0) {
+ encode_status_ = Status::RESOURCE_EXHAUSTED;
+ return encode_status_;
+ }
+
+ cursor_ += written;
+ return Status::OK;
+}
+
+Status Encoder::WriteRawBytes(const std::byte* ptr, size_t size) {
+ if (!encode_status_.ok()) {
+ return encode_status_;
+ }
+
+ if (size > RemainingSize()) {
+ encode_status_ = Status::RESOURCE_EXHAUSTED;
+ return encode_status_;
+ }
+
+ memcpy(cursor_, ptr, size);
+ cursor_ += size;
+ return Status::OK;
+}
+
+Status Encoder::Push(uint32_t field_number) {
+ if (!encode_status_.ok()) {
+ return encode_status_;
+ }
+
+ if (blob_count_ == blob_locations_.size() || depth_ == blob_stack_.size()) {
+ encode_status_ = Status::RESOURCE_EXHAUSTED;
+ return encode_status_;
+ }
+
+ // Write the key for the nested field.
+ uint8_t* original_cursor = cursor_;
+ if (Status status = WriteFieldKey(field_number, WireType::kDelimited);
+ !status.ok()) {
+ encode_status_ = status;
+ return status;
+ }
+
+ if (sizeof(SizeType) > RemainingSize()) {
+ // Rollback if there isn't enough space.
+ cursor_ = original_cursor;
+ encode_status_ = Status::RESOURCE_EXHAUSTED;
+ return encode_status_;
+ }
+
+ // Update parent size with the written key.
+ IncreaseParentSize(cursor_ - original_cursor);
+
+ union {
+ uint8_t* cursor;
+ SizeType* size_cursor;
+ };
+
+ // Create a size entry for the new blob and append it to both the nesting
+ // stack and location list.
+ cursor = cursor_;
+ *size_cursor = 0;
+ blob_locations_[blob_count_++] = size_cursor;
+ blob_stack_[depth_++] = size_cursor;
+
+ cursor_ += sizeof(*size_cursor);
+ return Status::OK;
+}
+
+Status Encoder::Pop() {
+ if (!encode_status_.ok()) {
+ return encode_status_;
+ }
+
+ if (depth_ == 0) {
+ encode_status_ = Status::FAILED_PRECONDITION;
+ return encode_status_;
+ }
+
+ // Update the parent's size with how much total space the child will take
+ // after its size field is varint encoded.
+ SizeType child_size = *blob_stack_[--depth_];
+ IncreaseParentSize(child_size + VarintSizeBytes(child_size));
+
+ return Status::OK;
+}
+
+Status Encoder::Encode(span<const uint8_t>* out) {
+ if (!encode_status_.ok()) {
+ *out = span<const uint8_t>();
+ return encode_status_;
+ }
+
+ if (blob_count_ == 0) {
+ // If there are no nested blobs, the buffer already contains a valid proto.
+ *out = buffer_.first(EncodedSize());
+ return Status::OK;
+ }
+
+ union {
+ uint8_t* read_cursor;
+ SizeType* size_cursor;
+ };
+
+ // Starting from the first blob, encode each size field as a varint and
+ // shift all subsequent data downwards.
+ unsigned int blob = 0;
+ size_cursor = blob_locations_[blob];
+ uint8_t* write_cursor = read_cursor;
+
+ while (read_cursor < cursor_) {
+ SizeType nested_size = *size_cursor;
+
+ span<uint8_t> varint_buf(write_cursor, sizeof(*size_cursor));
+ size_t varint_size =
+ pw::varint::EncodeLittleEndianBase128(nested_size, varint_buf);
+
+ // Place the write cursor after the encoded varint and the read cursor at
+ // the location of the next proto field.
+ write_cursor += varint_size;
+ read_cursor += varint_buf.size();
+
+ size_t to_copy;
+
+ if (blob == blob_count_ - 1) {
+ to_copy = cursor_ - read_cursor;
+ } else {
+ uint8_t* end = reinterpret_cast<uint8_t*>(blob_locations_[blob + 1]);
+ to_copy = end - read_cursor;
+ }
+
+ memmove(write_cursor, read_cursor, to_copy);
+ write_cursor += to_copy;
+ read_cursor += to_copy;
+
+ ++blob;
+ }
+
+ // Point the cursor to the end of the encoded proto.
+ cursor_ = write_cursor;
+ *out = buffer_.first(EncodedSize());
+ return Status::OK;
+}
+
+} // namespace pw::protobuf
diff --git a/pw_protobuf/encoder_test.cc b/pw_protobuf/encoder_test.cc
new file mode 100644
index 0000000..4da077e
--- /dev/null
+++ b/pw_protobuf/encoder_test.cc
@@ -0,0 +1,348 @@
+// Copyright 2019 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_protobuf/encoder.h"
+
+#include "gtest/gtest.h"
+
+namespace pw::protobuf {
+namespace {
+
+// The tests in this file use the following proto message schemas.
+//
+// message TestProto {
+// uint32 magic_number = 1;
+// sint32 ziggy = 2;
+// fixed64 cycles = 3;
+// float ratio = 4;
+// string error_message = 5;
+// NestedProto nested = 6;
+// }
+//
+// message NestedProto {
+// string hello = 1;
+// uint32 id = 2;
+// repeated DoubleNestedProto pair = 3;
+// }
+//
+// message DoubleNestedProto {
+// string key = 1;
+// string value = 2;
+// }
+//
+
+constexpr uint32_t kTestProtoMagicNumberField = 1;
+constexpr uint32_t kTestProtoZiggyField = 2;
+constexpr uint32_t kTestProtoCyclesField = 3;
+constexpr uint32_t kTestProtoRatioField = 4;
+constexpr uint32_t kTestProtoErrorMessageField = 5;
+constexpr uint32_t kTestProtoNestedField = 6;
+
+constexpr uint32_t kNestedProtoHelloField = 1;
+constexpr uint32_t kNestedProtoIdField = 2;
+constexpr uint32_t kNestedProtoPairField = 3;
+
+constexpr uint32_t kDoubleNestedProtoKeyField = 1;
+constexpr uint32_t kDoubleNestedProtoValueField = 2;
+
+TEST(Encoder, EncodePrimitives) {
+ // TestProto tp;
+ // tp.magic_number = 42;
+ // tp.ziggy = -13;
+ // tp.cycles = 0xdeadbeef8badf00d;
+ // tp.ratio = 1.618034;
+ // tp.error_message = "broken 💩";
+
+ // Hand-encoded version of the above.
+ // clang-format off
+ constexpr uint8_t encoded_proto[] = {
+ // magic_number [varint k=1]
+ 0x08, 0x2a,
+ // ziggy [varint k=2]
+ 0x10, 0x19,
+ // cycles [fixed64 k=3]
+ 0x19, 0x0d, 0xf0, 0xad, 0x8b, 0xef, 0xbe, 0xad, 0xde,
+ // ratio [fixed32 k=4]
+ 0x25, 0xbd, 0x1b, 0xcf, 0x3f,
+ // error_message [delimited k=5],
+ 0x2a, 0x0b, 'b', 'r', 'o', 'k', 'e', 'n', ' ',
+ // poop!
+ 0xf0, 0x9f, 0x92, 0xa9,
+ };
+ // clang-format on
+
+ uint8_t encode_buffer[32];
+ NestedEncoder encoder(encode_buffer);
+
+ EXPECT_EQ(encoder.WriteUint32(kTestProtoMagicNumberField, 42), Status::OK);
+ EXPECT_EQ(encoder.WriteSint32(kTestProtoZiggyField, -13), Status::OK);
+ EXPECT_EQ(encoder.WriteFixed64(kTestProtoCyclesField, 0xdeadbeef8badf00d),
+ Status::OK);
+ EXPECT_EQ(encoder.WriteFloat(kTestProtoRatioField, 1.618034), Status::OK);
+ EXPECT_EQ(encoder.WriteString(kTestProtoErrorMessageField, "broken 💩"),
+ Status::OK);
+
+ span<const uint8_t> encoded;
+ EXPECT_EQ(encoder.Encode(&encoded), Status::OK);
+ EXPECT_EQ(encoded.size(), sizeof(encoded_proto));
+ EXPECT_EQ(std::memcmp(encoded.data(), encoded_proto, encoded.size()), 0);
+}
+
+TEST(Encoder, EncodeInsufficientSpace) {
+ uint8_t encode_buffer[12];
+ NestedEncoder encoder(encode_buffer);
+
+ // 2 bytes.
+ EXPECT_EQ(encoder.WriteUint32(kTestProtoMagicNumberField, 42), Status::OK);
+ // 2 bytes.
+ EXPECT_EQ(encoder.WriteSint32(kTestProtoZiggyField, -13), Status::OK);
+ // 9 bytes; not enough space! The encoder will start writing the field but
+ // should rollback when it realizes it doesn't have enough space.
+ EXPECT_EQ(encoder.WriteFixed64(kTestProtoCyclesField, 0xdeadbeef8badf00d),
+ Status::RESOURCE_EXHAUSTED);
+ // Any further write operations should fail.
+ EXPECT_EQ(encoder.WriteFloat(kTestProtoRatioField, 1.618034),
+ Status::RESOURCE_EXHAUSTED);
+
+ span<const uint8_t> encoded;
+ EXPECT_EQ(encoder.Encode(&encoded), Status::RESOURCE_EXHAUSTED);
+ EXPECT_EQ(encoded.size(), 0u);
+}
+
+TEST(Encoder, EncodeInvalidArguments) {
+ uint8_t encode_buffer[12];
+ NestedEncoder encoder(encode_buffer);
+
+ EXPECT_EQ(encoder.WriteUint32(kTestProtoMagicNumberField, 42), Status::OK);
+ // Invalid proto field numbers.
+ EXPECT_EQ(encoder.WriteUint32(0, 1337), Status::INVALID_ARGUMENT);
+ encoder.Clear();
+
+ EXPECT_EQ(encoder.WriteString(1u << 31, "ha"), Status::INVALID_ARGUMENT);
+ encoder.Clear();
+
+ EXPECT_EQ(encoder.WriteBool(19091, false), Status::INVALID_ARGUMENT);
+ span<const uint8_t> encoded;
+ EXPECT_EQ(encoder.Encode(&encoded), Status::INVALID_ARGUMENT);
+ EXPECT_EQ(encoded.size(), 0u);
+}
+
+TEST(Encoder, Nested) {
+ uint8_t encode_buffer[128];
+ NestedEncoder<5, 10> encoder(encode_buffer);
+
+ // TestProto test_proto;
+ // test_proto.magic_number = 42;
+ EXPECT_EQ(encoder.WriteUint32(kTestProtoMagicNumberField, 42), Status::OK);
+
+ {
+ // NestedProto& nested_proto = test_proto.nested;
+ EXPECT_EQ(encoder.Push(kTestProtoNestedField), Status::OK);
+ // nested_proto.hello = "world";
+ EXPECT_EQ(encoder.WriteString(kNestedProtoHelloField, "world"), Status::OK);
+ // nested_proto.id = 999;
+ EXPECT_EQ(encoder.WriteUint32(kNestedProtoIdField, 999), Status::OK);
+
+ {
+ // DoubleNestedProto& double_nested_proto = nested_proto.append_pair();
+ EXPECT_EQ(encoder.Push(kNestedProtoPairField), Status::OK);
+ // double_nested_proto.key = "version";
+ EXPECT_EQ(encoder.WriteString(kDoubleNestedProtoKeyField, "version"),
+ Status::OK);
+ // double_nested_proto.value = "2.9.1";
+ EXPECT_EQ(encoder.WriteString(kDoubleNestedProtoValueField, "2.9.1"),
+ Status::OK);
+
+ EXPECT_EQ(encoder.Pop(), Status::OK);
+ } // end DoubleNestedProto
+
+ {
+ // DoubleNestedProto& double_nested_proto = nested_proto.append_pair();
+ EXPECT_EQ(encoder.Push(kNestedProtoPairField), Status::OK);
+ // double_nested_proto.key = "device";
+ EXPECT_EQ(encoder.WriteString(kDoubleNestedProtoKeyField, "device"),
+ Status::OK);
+ // double_nested_proto.value = "left-soc";
+ EXPECT_EQ(encoder.WriteString(kDoubleNestedProtoValueField, "left-soc"),
+ Status::OK);
+
+ EXPECT_EQ(encoder.Pop(), Status::OK);
+ } // end DoubleNestedProto
+
+ EXPECT_EQ(encoder.Pop(), Status::OK);
+ } // end NestedProto
+
+ // test_proto.ziggy = -13;
+ EXPECT_EQ(encoder.WriteSint32(kTestProtoZiggyField, -13), Status::OK);
+
+ // clang-format off
+ constexpr uint8_t encoded_proto[] = {
+ // magic_number
+ 0x08, 0x2a,
+ // nested header (key, size)
+ 0x32, 0x30,
+ // nested.hello
+ 0x0a, 0x05, 'w', 'o', 'r', 'l', 'd',
+ // nested.id
+ 0x10, 0xe7, 0x07,
+ // nested.pair[0] header (key, size)
+ 0x1a, 0x10,
+ // nested.pair[0].key
+ 0x0a, 0x07, 'v', 'e', 'r', 's', 'i', 'o', 'n',
+ // nested.pair[0].value
+ 0x12, 0x05, '2', '.', '9', '.', '1',
+ // nested.pair[1] header (key, size)
+ 0x1a, 0x12,
+ // nested.pair[1].key
+ 0x0a, 0x06, 'd', 'e', 'v', 'i', 'c', 'e',
+ // nested.pair[1].value
+ 0x12, 0x08, 'l', 'e', 'f', 't', '-', 's', 'o', 'c',
+ // ziggy
+ 0x10, 0x19
+ };
+ // clang-format on
+
+ span<const uint8_t> encoded;
+ EXPECT_EQ(encoder.Encode(&encoded), Status::OK);
+ EXPECT_EQ(encoded.size(), sizeof(encoded_proto));
+ EXPECT_EQ(std::memcmp(encoded.data(), encoded_proto, encoded.size()), 0);
+}
+
+TEST(Encoder, NestedDepthLimit) {
+ uint8_t encode_buffer[128];
+ NestedEncoder<2, 10> encoder(encode_buffer);
+
+ // One level of nesting.
+ EXPECT_EQ(encoder.Push(2), Status::OK);
+ // Two levels of nesting.
+ EXPECT_EQ(encoder.Push(1), Status::OK);
+ // Three levels of nesting: error!
+ EXPECT_EQ(encoder.Push(1), Status::RESOURCE_EXHAUSTED);
+
+ // Further operations should fail.
+ EXPECT_EQ(encoder.Pop(), Status::RESOURCE_EXHAUSTED);
+ EXPECT_EQ(encoder.Pop(), Status::RESOURCE_EXHAUSTED);
+ EXPECT_EQ(encoder.Pop(), Status::RESOURCE_EXHAUSTED);
+}
+
+TEST(Encoder, NestedBlobLimit) {
+ uint8_t encode_buffer[128];
+ NestedEncoder<5, 3> encoder(encode_buffer);
+
+ // Write first blob.
+ EXPECT_EQ(encoder.Push(1), Status::OK);
+ EXPECT_EQ(encoder.Pop(), Status::OK);
+
+ // Write second blob.
+ EXPECT_EQ(encoder.Push(2), Status::OK);
+
+ // Write nested third blob.
+ EXPECT_EQ(encoder.Push(3), Status::OK);
+ EXPECT_EQ(encoder.Pop(), Status::OK);
+
+ // End second blob.
+ EXPECT_EQ(encoder.Pop(), Status::OK);
+
+ // Write fourth blob: error!.
+ EXPECT_EQ(encoder.Push(4), Status::RESOURCE_EXHAUSTED);
+ // Nothing to pop.
+ EXPECT_EQ(encoder.Pop(), Status::RESOURCE_EXHAUSTED);
+}
+
+TEST(Encoder, RepeatedField) {
+ uint8_t encode_buffer[32];
+ NestedEncoder encoder(encode_buffer);
+
+ // repeated uint32 values = 1;
+ constexpr uint32_t values[] = {0, 50, 100, 150, 200};
+ for (int i = 0; i < 5; ++i) {
+ encoder.WriteUint32(1, values[i]);
+ }
+
+ constexpr uint8_t encoded_proto[] = {
+ 0x08, 0x00, 0x08, 0x32, 0x08, 0x64, 0x08, 0x96, 0x01, 0x08, 0xc8, 0x01};
+
+ span<const uint8_t> encoded;
+ EXPECT_EQ(encoder.Encode(&encoded), Status::OK);
+ EXPECT_EQ(encoded.size(), sizeof(encoded_proto));
+ EXPECT_EQ(std::memcmp(encoded.data(), encoded_proto, encoded.size()), 0);
+}
+
+TEST(Encoder, PackedVarint) {
+ uint8_t encode_buffer[32];
+ NestedEncoder encoder(encode_buffer);
+
+ // repeated uint32 values = 1;
+ constexpr uint32_t values[] = {0, 50, 100, 150, 200};
+ encoder.WritePackedUint32(1, values);
+
+ constexpr uint8_t encoded_proto[] = {
+ 0x0a, 0x07, 0x00, 0x32, 0x64, 0x96, 0x01, 0xc8, 0x01};
+ // key size v[0] v[1] v[2] v[3] v[4]
+
+ span<const uint8_t> encoded;
+ EXPECT_EQ(encoder.Encode(&encoded), Status::OK);
+ EXPECT_EQ(encoded.size(), sizeof(encoded_proto));
+ EXPECT_EQ(std::memcmp(encoded.data(), encoded_proto, encoded.size()), 0);
+}
+
+TEST(Encoder, PackedVarintInsufficientSpace) {
+ uint8_t encode_buffer[8];
+ NestedEncoder encoder(encode_buffer);
+
+ constexpr uint32_t values[] = {0, 50, 100, 150, 200};
+ encoder.WritePackedUint32(1, values);
+
+ span<const uint8_t> encoded;
+ EXPECT_EQ(encoder.Encode(&encoded), Status::RESOURCE_EXHAUSTED);
+ EXPECT_EQ(encoded.size(), 0u);
+}
+
+TEST(Encoder, PackedFixed) {
+ uint8_t encode_buffer[32];
+ NestedEncoder encoder(encode_buffer);
+
+ // repeated fixed32 values = 1;
+ constexpr uint32_t values[] = {0, 50, 100, 150, 200};
+ encoder.WritePackedFixed32(1, values);
+
+ constexpr uint8_t encoded_proto[] = {
+ 0x0a, 0x14, 0x00, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, 0x64,
+ 0x00, 0x00, 0x00, 0x96, 0x00, 0x00, 0x00, 0xc8, 0x00, 0x00, 0x00};
+
+ span<const uint8_t> encoded;
+ EXPECT_EQ(encoder.Encode(&encoded), Status::OK);
+ EXPECT_EQ(encoded.size(), sizeof(encoded_proto));
+ EXPECT_EQ(std::memcmp(encoded.data(), encoded_proto, encoded.size()), 0);
+}
+
+TEST(Encoder, PackedZigzag) {
+ uint8_t encode_buffer[32];
+ NestedEncoder encoder(encode_buffer);
+
+ // repeated sint32 values = 1;
+ constexpr int32_t values[] = {-100, -25, -1, 0, 1, 25, 100};
+ encoder.WritePackedSint32(1, values);
+
+ constexpr uint8_t encoded_proto[] = {
+ 0x0a, 0x09, 0xc7, 0x01, 0x31, 0x01, 0x00, 0x02, 0x32, 0xc8, 0x01};
+
+ span<const uint8_t> encoded;
+ EXPECT_EQ(encoder.Encode(&encoded), Status::OK);
+ EXPECT_EQ(encoded.size(), sizeof(encoded_proto));
+ EXPECT_EQ(std::memcmp(encoded.data(), encoded_proto, encoded.size()), 0);
+}
+
+} // namespace
+} // namespace pw::protobuf
diff --git a/pw_protobuf/public/pw_protobuf/codegen.h b/pw_protobuf/public/pw_protobuf/codegen.h
new file mode 100644
index 0000000..103612f
--- /dev/null
+++ b/pw_protobuf/public/pw_protobuf/codegen.h
@@ -0,0 +1,44 @@
+// Copyright 2019 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include "pw_protobuf/encoder.h"
+
+namespace pw::protobuf {
+
+// Base class for generated encoders. Stores a reference to a low-level proto
+// encoder. If representing a nested message, knows the field number of the
+// message in its parent and and automatically calls Push and Pop when on the
+// encoder when created/destroyed.
+class ProtoMessageEncoder {
+ public:
+ ProtoMessageEncoder(Encoder* encoder, uint32_t parent_field = 0)
+ : encoder_(encoder), parent_field_(parent_field) {
+ if (parent_field_ != 0) {
+ encoder_->Push(parent_field_);
+ }
+ }
+
+ ~ProtoMessageEncoder() {
+ if (parent_field_ != 0) {
+ encoder_->Pop();
+ }
+ }
+
+ protected:
+ Encoder* encoder_;
+ uint32_t parent_field_;
+};
+
+} // namespace pw::protobuf
diff --git a/pw_protobuf/public/pw_protobuf/encoder.h b/pw_protobuf/public/pw_protobuf/encoder.h
new file mode 100644
index 0000000..89c4e78
--- /dev/null
+++ b/pw_protobuf/public/pw_protobuf/encoder.h
@@ -0,0 +1,367 @@
+// Copyright 2019 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <cstring>
+
+#include "pw_span/span.h"
+#include "pw_status/status.h"
+#include "pw_varint/varint.h"
+
+namespace pw::protobuf {
+
+// A streaming protobuf encoder which encodes to a user-specified buffer.
+class Encoder {
+ public:
+ // TODO(frolv): Right now, only one intermediate size is supported. However,
+ // this can be wasteful, as it requires 4 or 8 bytes of space per nested
+ // message. This can be templated to minimize the overhead.
+ using SizeType = size_t;
+
+ constexpr Encoder(span<uint8_t> buffer,
+ span<SizeType*> locations,
+ span<SizeType*> stack)
+ : buffer_(buffer),
+ cursor_(buffer.data()),
+ blob_locations_(locations),
+ blob_count_(0),
+ blob_stack_(stack),
+ depth_(0),
+ encode_status_(Status::OK) {}
+
+ // Disallow copy/assign to avoid confusion about who owns the buffer.
+ Encoder(const Encoder& other) = delete;
+ Encoder& operator=(const Encoder& other) = delete;
+
+ // Per the protobuf specification, valid field numbers range between 1 and
+ // 2**29 - 1, inclusive. The numbers 19000-19999 are reserved for internal
+ // use.
+ constexpr static uint32_t kMaxFieldNumber = (1u << 29) - 1;
+ constexpr static uint32_t kFirstReservedNumber = 19000;
+ constexpr static uint32_t kLastReservedNumber = 19999;
+
+ // Writes a proto uint32 key-value pair.
+ Status WriteUint32(uint32_t field_number, uint32_t value) {
+ return WriteUint64(field_number, value);
+ }
+
+ // Writes a repeated uint32 using packed encoding.
+ Status WritePackedUint32(uint32_t field_number, span<const uint32_t> values) {
+ return WritePackedVarints(field_number, values, /*zigzag=*/false);
+ }
+
+ // Writes a proto uint64 key-value pair.
+ Status WriteUint64(uint32_t field_number, uint64_t value);
+
+ // Writes a repeated uint64 using packed encoding.
+ Status WritePackedUint64(uint64_t field_number, span<const uint64_t> values) {
+ return WritePackedVarints(field_number, values, /*zigzag=*/false);
+ }
+
+ // Writes a proto int32 key-value pair.
+ Status WriteInt32(uint32_t field_number, int32_t value) {
+ return WriteUint64(field_number, value);
+ }
+
+ // Writes a repeated int32 using packed encoding.
+ Status WritePackedInt32(uint32_t field_number, span<const int32_t> values) {
+ return WritePackedVarints(
+ field_number,
+ span(reinterpret_cast<const uint32_t*>(values.data()), values.size()),
+ /*zigzag=*/false);
+ }
+
+ // Writes a proto int64 key-value pair.
+ Status WriteInt64(uint32_t field_number, int64_t value) {
+ return WriteUint64(field_number, value);
+ }
+
+ // Writes a repeated int64 using packed encoding.
+ Status WritePackedInt64(uint32_t field_number, span<const int64_t> values) {
+ return WritePackedVarints(
+ field_number,
+ span(reinterpret_cast<const uint64_t*>(values.data()), values.size()),
+ /*zigzag=*/false);
+ }
+
+ // Writes a proto sint32 key-value pair.
+ Status WriteSint32(uint32_t field_number, int32_t value) {
+ return WriteUint64(field_number, varint::ZigZagEncode(value));
+ }
+
+ // Writes a repeated sint32 using packed encoding.
+ Status WritePackedSint32(uint32_t field_number, span<const int32_t> values) {
+ return WritePackedVarints(
+ field_number,
+ span(reinterpret_cast<const uint32_t*>(values.data()), values.size()),
+ /*zigzag=*/true);
+ }
+
+ // Writes a proto sint64 key-value pair.
+ Status WriteSint64(uint32_t field_number, int64_t value) {
+ return WriteUint64(field_number, varint::ZigZagEncode(value));
+ }
+
+ // Writes a repeated sint64 using packed encoding.
+ Status WritePackedSint64(uint32_t field_number, span<const int64_t> values) {
+ return WritePackedVarints(
+ field_number,
+ span(reinterpret_cast<const uint64_t*>(values.data()), values.size()),
+ /*zigzag=*/true);
+ }
+
+ // Writes a proto bool key-value pair.
+ Status WriteBool(uint32_t field_number, bool value) {
+ return WriteUint32(field_number, static_cast<uint32_t>(value));
+ }
+
+ // Writes a proto fixed32 key-value pair.
+ Status WriteFixed32(uint32_t field_number, uint32_t value) {
+ uint8_t* original_cursor = cursor_;
+ WriteFieldKey(field_number, WireType::kFixed32);
+ Status status = WriteRawBytes(value);
+ IncreaseParentSize(cursor_ - original_cursor);
+ return status;
+ }
+
+ // Writes a repeated fixed32 field using packed encoding.
+ Status WritePackedFixed32(uint32_t field_number,
+ span<const uint32_t> values) {
+ return WriteBytes(field_number, as_bytes(values));
+ }
+
+ // Writes a proto fixed64 key-value pair.
+ Status WriteFixed64(uint32_t field_number, uint64_t value) {
+ uint8_t* original_cursor = cursor_;
+ WriteFieldKey(field_number, WireType::kFixed64);
+ Status status = WriteRawBytes(value);
+ IncreaseParentSize(cursor_ - original_cursor);
+ return status;
+ }
+
+ // Writes a repeated fixed64 field using packed encoding.
+ Status WritePackedFixed32(uint32_t field_number,
+ span<const uint64_t> values) {
+ return WriteBytes(field_number, as_bytes(values));
+ }
+
+ // Writes a proto sfixed32 key-value pair.
+ Status WriteSfixed32(uint32_t field_number, int32_t value) {
+ return WriteFixed32(field_number, static_cast<uint32_t>(value));
+ }
+
+ // Writes a repeated sfixed32 field using packed encoding.
+ Status WritePackedSfixed32(uint32_t field_number,
+ span<const int32_t> values) {
+ return WriteBytes(field_number, as_bytes(values));
+ }
+
+ // Writes a proto sfixed64 key-value pair.
+ Status WriteSfixed64(uint32_t field_number, int64_t value) {
+ return WriteFixed64(field_number, static_cast<uint64_t>(value));
+ }
+
+ // Writes a repeated sfixed64 field using packed encoding.
+ Status WritePackedSfixed64(uint32_t field_number,
+ span<const int64_t> values) {
+ return WriteBytes(field_number, as_bytes(values));
+ }
+
+ // Writes a proto float key-value pair.
+ Status WriteFloat(uint32_t field_number, float value) {
+ static_assert(sizeof(float) == sizeof(uint32_t),
+ "Float and uint32_t are not the same size");
+ uint8_t* original_cursor = cursor_;
+ WriteFieldKey(field_number, WireType::kFixed32);
+ Status status = WriteRawBytes(value);
+ IncreaseParentSize(cursor_ - original_cursor);
+ return status;
+ }
+
+ // Writes a repeated float field using packed encoding.
+ Status WritePackedFloat(uint32_t field_number, span<const float> values) {
+ return WriteBytes(field_number, as_bytes(values));
+ }
+
+ // Writes a proto double key-value pair.
+ Status WriteDouble(uint32_t field_number, double value) {
+ static_assert(sizeof(double) == sizeof(uint64_t),
+ "Double and uint64_t are not the same size");
+ uint8_t* original_cursor = cursor_;
+ WriteFieldKey(field_number, WireType::kFixed64);
+ Status status = WriteRawBytes(value);
+ IncreaseParentSize(cursor_ - original_cursor);
+ return status;
+ }
+
+ // Writes a repeated double field using packed encoding.
+ Status WritePackedDouble(uint32_t field_number, span<const double> values) {
+ return WriteBytes(field_number, as_bytes(values));
+ }
+
+ // Writes a proto bytes key-value pair.
+ Status WriteBytes(uint32_t field_number, span<const std::byte> value) {
+ uint8_t* original_cursor = cursor_;
+ WriteFieldKey(field_number, WireType::kDelimited);
+ WriteVarint(value.size_bytes());
+ Status status = WriteRawBytes(value.data(), value.size_bytes());
+ IncreaseParentSize(cursor_ - original_cursor);
+ return status;
+ }
+
+ // Writes a proto string key-value pair.
+ Status WriteString(uint32_t field_number, const char* value, size_t size) {
+ return WriteBytes(field_number, as_bytes(span(value, size)));
+ }
+
+ Status WriteString(uint32_t field_number, const char* value) {
+ return WriteString(field_number, value, strlen(value));
+ }
+
+ // Begins writing a sub-message with a specified field number.
+ Status Push(uint32_t field_number);
+
+ // Finishes writing a sub-message.
+ Status Pop();
+
+ // Returns the total encoded size of the proto message.
+ size_t EncodedSize() const { return cursor_ - buffer_.data(); }
+
+ // Returns the number of bytes remaining in the buffer.
+ size_t RemainingSize() const { return buffer_.size() - EncodedSize(); }
+
+ // Resets write index to the start of the buffer. This invalidates any spans
+ // obtained from Encode().
+ void Clear() {
+ cursor_ = buffer_.data();
+ encode_status_ = Status::OK;
+ blob_count_ = 0;
+ depth_ = 0;
+ }
+
+ // Runs a final encoding pass over the intermediary data and returns the
+ // encoded protobuf message.
+ Status Encode(span<const uint8_t>* out);
+
+ private:
+ enum class WireType : uint8_t {
+ kVarint = 0,
+ kFixed64 = 1,
+ kDelimited = 2,
+ // Wire types 3 and 4 are deprecated per the protobuf specification.
+ kFixed32 = 5
+ };
+
+ constexpr bool ValidFieldNumber(uint32_t field_number) const {
+ return field_number != 0 && field_number <= kMaxFieldNumber &&
+ !(field_number >= kFirstReservedNumber &&
+ field_number <= kLastReservedNumber);
+ }
+
+ // Encodes the key for a proto field consisting of its number and wire type.
+ Status WriteFieldKey(uint32_t field_number, WireType wire_type) {
+ if (!ValidFieldNumber(field_number)) {
+ encode_status_ = Status::INVALID_ARGUMENT;
+ return encode_status_;
+ }
+
+ return WriteVarint((field_number << 3) | static_cast<uint8_t>(wire_type));
+ }
+
+ Status WriteVarint(uint64_t value);
+
+ Status WriteZigzagVarint(int64_t value) {
+ return WriteVarint(varint::ZigZagEncode(value));
+ }
+
+ template <typename T>
+ Status WriteRawBytes(const T& value) {
+ return WriteRawBytes(reinterpret_cast<const std::byte*>(&value),
+ sizeof(value));
+ }
+
+ Status WriteRawBytes(const std::byte* ptr, size_t size);
+
+ // Writes a list of varints to the buffer in length-delimited packed encoding.
+ // If zigzag is true, zig-zag encodes each of the varints.
+ template <typename T>
+ Status WritePackedVarints(uint32_t field_number,
+ span<T> values,
+ bool zigzag) {
+ if (Status status = Push(field_number); !status.ok()) {
+ return status;
+ }
+
+ uint8_t* original_cursor = cursor_;
+ for (T value : values) {
+ if (zigzag) {
+ WriteZigzagVarint(static_cast<std::make_signed_t<T>>(value));
+ } else {
+ WriteVarint(value);
+ }
+ }
+ IncreaseParentSize(cursor_ - original_cursor);
+
+ return Pop();
+ }
+
+ // Adds to the parent proto's size field in the buffer.
+ void IncreaseParentSize(size_t bytes) {
+ if (depth_ > 0) {
+ *blob_stack_[depth_ - 1] += bytes;
+ }
+ }
+
+ // Returns the size of `n` encoded as a varint.
+ size_t VarintSizeBytes(uint64_t n) {
+ size_t size_bytes = 1;
+ while (n > 127) {
+ ++size_bytes;
+ n >>= 7;
+ }
+ return size_bytes;
+ }
+
+ // The buffer into which the proto is encoded.
+ span<uint8_t> buffer_;
+ uint8_t* cursor_;
+
+ // List of pointers to sub-messages' delimiting size fields.
+ span<SizeType*> blob_locations_;
+ size_t blob_count_;
+
+ // Stack of current nested message size locations. Push() operations add a new
+ // entry to this stack and Pop() operations remove one.
+ span<SizeType*> blob_stack_;
+ size_t depth_;
+
+ Status encode_status_;
+};
+
+// A proto encoder with its own blob stack.
+template <size_t kMaxNestedDepth = 1, size_t kMaxBlobs = 1>
+class NestedEncoder : public Encoder {
+ public:
+ NestedEncoder(span<uint8_t> buffer) : Encoder(buffer, blobs_, stack_) {}
+
+ // Disallow copy/assign to avoid confusion about who owns the buffer.
+ NestedEncoder(const NestedEncoder& other) = delete;
+ NestedEncoder& operator=(const NestedEncoder& other) = delete;
+
+ private:
+ std::array<size_t*, kMaxBlobs> blobs_;
+ std::array<size_t*, kMaxNestedDepth> stack_;
+};
+
+} // namespace pw::protobuf
diff --git a/pw_protobuf/pw_protobuf_protos/test_protos/full_test.proto b/pw_protobuf/pw_protobuf_protos/test_protos/full_test.proto
new file mode 100644
index 0000000..112161d
--- /dev/null
+++ b/pw_protobuf/pw_protobuf_protos/test_protos/full_test.proto
@@ -0,0 +1,122 @@
+// Copyright 2019 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.
+syntax = "proto3";
+
+// This is a test .proto file for pw_protobuf's codegen implementation.
+
+package pw.protobuf.test;
+
+// Top-level enum definition.
+enum Bool {
+ TRUE = 0;
+ FALSE = 1;
+ FILE_NOT_FOUND = 2;
+}
+
+// A message!
+message Pigweed {
+
+ // Nested messages and enums.
+ message Pigweed {
+ enum Binary {
+ ZERO = 0;
+ ONE = 1;
+ };
+
+ Bool status = 1;
+ }
+
+ message Protobuf {
+ enum Binary {
+ ONE = 0;
+ ZERO = 1;
+ };
+
+ // We must go deeper.
+ message Compiler {
+ enum Status {
+ OK = 0;
+ ERROR = 1;
+ FUBAR = 2;
+ }
+
+ string file_name = 1;
+ Status status = 2;
+ Binary protobuf_bin = 3;
+ Pigweed.Binary pigweed_bin = 4;
+ }
+
+ Binary binary_value = 1;
+ }
+
+ // Regular types.
+ uint32 magic_number = 1;
+ sint32 ziggy = 2;
+ fixed64 cycles = 3;
+ float ratio = 4;
+ string error_message = 5;
+
+ DeviceInfo device_info = 6;
+
+ // Nested messages and enums as fields.
+ Pigweed pigweed = 7;
+ Protobuf.Binary bin = 8;
+
+ Proto proto = 9;
+ repeated Proto.ID id = 10;
+}
+
+// Another message.
+message Proto {
+ enum Binary {
+ OFF = 0;
+ ON = 1;
+ };
+
+ message ID {
+ uint32 id = 1;
+ }
+
+ // Circular dependency with Pigweed.
+ Pigweed pigweed = 1;
+
+ // Same name, different namespace.
+ Binary bin = 2;
+ Pigweed.Pigweed.Binary pigweed_pigweed_bin = 3;
+ Pigweed.Protobuf.Binary pigweed_protobuf_bin = 4;
+
+ Pigweed.Protobuf.Compiler meta = 5;
+}
+
+// Yet another message.
+message DeviceInfo {
+ enum DeviceStatus {
+ OK = 0;
+ ASSERT = 1;
+ FAULT = 2;
+ PANIC = 3;
+ }
+
+ string device_name = 1;
+ fixed32 device_id = 2;
+ DeviceStatus status = 3;
+
+ repeated KeyValuePair attributes = 4;
+}
+
+// This might be useful.
+message KeyValuePair {
+ string key = 1;
+ string value = 2;
+}
diff --git a/pw_protobuf/pw_protobuf_protos/test_protos/proto2.proto b/pw_protobuf/pw_protobuf_protos/test_protos/proto2.proto
new file mode 100644
index 0000000..c8d9e8e
--- /dev/null
+++ b/pw_protobuf/pw_protobuf_protos/test_protos/proto2.proto
@@ -0,0 +1,34 @@
+// Copyright 2019 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.
+syntax = "proto2";
+
+package pw.protobuf.test;
+
+message Foo {
+ required uint32 int = 1;
+ optional string str = 2;
+ repeated Bar bar = 3;
+ optional pb pb = 4;
+};
+
+message Bar {
+ optional bytes data = 1;
+};
+
+// This message's name starts with a character which is in the package path,
+// which exposes a bug in the original implementation of the pw_protobuf
+// compiler plugin.
+message pb {
+ optional Foo foo = 1;
+};
diff --git a/pw_protobuf/pw_protobuf_protos/test_protos/repeated.proto b/pw_protobuf/pw_protobuf_protos/test_protos/repeated.proto
new file mode 100644
index 0000000..c281db0
--- /dev/null
+++ b/pw_protobuf/pw_protobuf_protos/test_protos/repeated.proto
@@ -0,0 +1,29 @@
+// Copyright 2019 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.
+syntax = "proto3";
+
+package pw.protobuf.test;
+
+message RepeatedTest {
+ repeated uint32 uint32s = 1;
+ repeated sint32 sint32s = 2;
+ repeated string strings = 3;
+ repeated double doubles = 4;
+ repeated Struct structs = 5;
+};
+
+message Struct {
+ uint32 one = 1;
+ uint32 two = 2;
+}
diff --git a/pw_protobuf/py/pw_protobuf/codegen.py b/pw_protobuf/py/pw_protobuf/codegen.py
new file mode 100755
index 0000000..b56ff56
--- /dev/null
+++ b/pw_protobuf/py/pw_protobuf/codegen.py
@@ -0,0 +1,411 @@
+#!/usr/bin/env python3
+# Copyright 2019 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.
+"""pw_protobuf compiler plugin.
+
+This file implements a protobuf compiler plugin which generates C++ headers for
+protobuf messages in the pw_protobuf format.
+"""
+
+import os
+import sys
+
+from typing import List
+
+import google.protobuf.compiler.plugin_pb2 as plugin_pb2
+import google.protobuf.descriptor_pb2 as descriptor_pb2
+
+from pw_protobuf.methods import PROTO_FIELD_METHODS
+from pw_protobuf.proto_structures import ProtoEnum, ProtoMessage
+from pw_protobuf.proto_structures import ProtoMessageField, ProtoNode
+from pw_protobuf.proto_structures import ProtoPackage
+
+PLUGIN_NAME = 'pw_protobuf'
+PLUGIN_VERSION = '0.1.0'
+
+PROTOBUF_NAMESPACE = 'pw::protobuf'
+BASE_PROTO_CLASS = 'ProtoMessageEncoder'
+
+
+# protoc captures stdout, so we need to printf debug to stderr.
+def debug_print(*args, **kwargs):
+ print(*args, file=sys.stderr, **kwargs)
+
+
+class OutputFile:
+ """A buffer to which data is written.
+
+ Example:
+
+ ```
+ output = Output("hello.c")
+ output.write_line('int main(void) {')
+ with output.indent():
+ output.write_line('printf("Hello, world");')
+ output.write_line('return 0;')
+ output.write_line('}')
+
+ print(output.content())
+ ```
+
+ Produces:
+ ```
+ int main(void) {
+ printf("Hello, world");
+ return 0;
+ }
+ ```
+ """
+
+ INDENT_WIDTH = 2
+
+ def __init__(self, filename: str):
+ self._filename: str = filename
+ self._content: List[str] = []
+ self._indentation: int = 0
+
+ def write_line(self, line: str = '') -> None:
+ if line:
+ self._content.append(' ' * self._indentation)
+ self._content.append(line)
+ self._content.append('\n')
+
+ def indent(self) -> 'OutputFile._IndentationContext':
+ """Increases the indentation level of the output."""
+ return self._IndentationContext(self)
+
+ def name(self) -> str:
+ return self._filename
+
+ def content(self) -> str:
+ return ''.join(self._content)
+
+ class _IndentationContext:
+ """Context that increases the output's indentation when it is active."""
+ def __init__(self, output: 'OutputFile'):
+ self._output = output
+
+ def __enter__(self):
+ self._output._indentation += OutputFile.INDENT_WIDTH
+
+ def __exit__(self, typ, value, traceback):
+ self._output._indentation -= OutputFile.INDENT_WIDTH
+
+
+def generate_code_for_message(
+ message: ProtoNode,
+ root: ProtoNode,
+ output: OutputFile,
+) -> None:
+ """Creates a C++ class for a protobuf message."""
+ assert message.type() == ProtoNode.Type.MESSAGE
+
+ # Message classes inherit from the base proto message class in codegen.h
+ # and use its constructor.
+ base_class = f'{PROTOBUF_NAMESPACE}::{BASE_PROTO_CLASS}'
+ output.write_line(
+ f'class {message.cpp_namespace(root)}::Encoder : public {base_class} {{'
+ )
+ output.write_line(' public:')
+
+ with output.indent():
+ output.write_line(f'using {BASE_PROTO_CLASS}::{BASE_PROTO_CLASS};')
+
+ # Generate methods for each of the message's fields.
+ for field in message.fields():
+ for method_class in PROTO_FIELD_METHODS[field.type()]:
+ method = method_class(field, message, root)
+ if not method.should_appear():
+ continue
+
+ output.write_line()
+ method_signature = (
+ f'{method.return_type()} '
+ f'{method.name()}({method.param_string()})')
+
+ if not method.in_class_definition():
+ # Method will be defined outside of the class at the end of
+ # the file.
+ output.write_line(f'{method_signature};')
+ continue
+
+ output.write_line(f'{method_signature} {{')
+ with output.indent():
+ for line in method.body():
+ output.write_line(line)
+ output.write_line('}')
+
+ output.write_line('};')
+
+
+def define_not_in_class_methods(
+ message: ProtoNode,
+ root: ProtoNode,
+ output: OutputFile,
+) -> None:
+ """Defines methods for a message class that were previously declared."""
+ assert message.type() == ProtoNode.Type.MESSAGE
+
+ for field in message.fields():
+ for method_class in PROTO_FIELD_METHODS[field.type()]:
+ method = method_class(field, message, root)
+ if not method.should_appear() or method.in_class_definition():
+ continue
+
+ output.write_line()
+ class_name = f'{message.cpp_namespace(root)}::Encoder'
+ method_signature = (
+ f'inline {method.return_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():
+ output.write_line(line)
+ output.write_line('}')
+
+
+def generate_code_for_enum(
+ enum: ProtoNode,
+ root: ProtoNode,
+ output: OutputFile,
+) -> None:
+ """Creates a C++ enum for a proto enum."""
+ assert enum.type() == ProtoNode.Type.ENUM
+
+ output.write_line(f'enum class {enum.cpp_namespace(root)} {{')
+ with output.indent():
+ for name, number in enum.values():
+ output.write_line(f'{name} = {number},')
+ output.write_line('};')
+
+
+def forward_declare(
+ node: ProtoNode,
+ root: ProtoNode,
+ output: OutputFile,
+) -> None:
+ """Generates code forward-declaring entities in a message's namespace."""
+ if node.type() != ProtoNode.Type.MESSAGE:
+ return
+
+ namespace = node.cpp_namespace(root)
+ output.write_line()
+ output.write_line(f'namespace {namespace} {{')
+
+ # Define an enum defining each of the message's fields and their numbers.
+ output.write_line('enum class Fields {')
+ with output.indent():
+ for field in node.fields():
+ output.write_line(f'{field.enum_name()} = {field.number()},')
+ output.write_line('};')
+
+ # Declare the message's encoder class and all of its enums.
+ output.write_line()
+ output.write_line('class Encoder;')
+ for child in node.children():
+ if child.type() == ProtoNode.Type.ENUM:
+ output.write_line()
+ generate_code_for_enum(child, node, output)
+
+ output.write_line(f'}} // namespace {namespace}')
+
+
+# TODO(frolv): Right now, this plugin assumes that package is synonymous with
+# .proto file. This will need to be updated to handle compiling multiple files.
+def generate_code_for_package(package: ProtoNode, output: OutputFile) -> None:
+ """Generates code for a single .pb.h 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('#pragma once\n')
+ output.write_line('#include <cstddef>')
+ output.write_line('#include <cstdint>\n')
+ output.write_line('#include "pw_protobuf/codegen.h"')
+
+ if package.cpp_namespace():
+ output.write_line(f'\nnamespace {package.cpp_namespace()} {{')
+
+ for node in package:
+ forward_declare(node, package, output)
+
+ # Define all top-level enums.
+ for node in package.children():
+ if node.type() == ProtoNode.Type.ENUM:
+ output.write_line()
+ generate_code_for_enum(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(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(node, package, output)
+
+ if package.cpp_namespace():
+ output.write_line(f'\n}} // namespace {package.cpp_namespace()}')
+
+
+def add_enum_fields(enum: ProtoNode, proto_enum) -> None:
+ """Adds fields from a protobuf enum descriptor to an enum node."""
+ assert enum.type() == ProtoNode.Type.ENUM
+ for value in proto_enum.value:
+ enum.add_value(value.name, value.number)
+
+
+def add_message_fields(
+ root: ProtoNode,
+ message: ProtoNode,
+ proto_message,
+) -> None:
+ """Adds fields from a protobuf message descriptor to a message node."""
+ assert message.type() == ProtoNode.Type.MESSAGE
+
+ for field in proto_message.field:
+ if field.type_name:
+ # The "type_name" member contains the global .proto path of the
+ # field's type object, for example ".pw.protobuf.test.KeyValuePair".
+ # Since only a single proto file is currently supported, the root
+ # node has the value of the file's package ("pw.protobuf.test").
+ # This must be stripped from the path to find the desired node
+ # within the tree.
+ #
+ # TODO(frolv): Once multiple files are supported, the root node
+ # should refer to the global namespace, and this should no longer
+ # be needed.
+ path = field.type_name
+ if path[0] == '.':
+ path = path[1:]
+
+ if path.startswith(root.name()):
+ relative_path = path[len(root.name()):].lstrip('.')
+ else:
+ relative_path = path
+
+ type_node = root.find(relative_path)
+ else:
+ type_node = None
+
+ repeated = \
+ field.label == descriptor_pb2.FieldDescriptorProto.LABEL_REPEATED
+ message.add_field(
+ ProtoMessageField(
+ field.name,
+ field.number,
+ field.type,
+ type_node,
+ repeated,
+ ))
+
+
+def populate_fields(proto_file, root: ProtoNode) -> None:
+ """Traverses a proto file, adding all message and enum fields to a tree."""
+ def populate_message(node, message):
+ """Recursively populates nested messages and enums."""
+ add_message_fields(root, node, message)
+
+ for enum in message.enum_type:
+ add_enum_fields(node.find(enum.name), enum)
+ for msg in message.nested_type:
+ populate_message(node.find(msg.name), msg)
+
+ # Iterate through the proto file, populating top-level enums and messages.
+ for enum in proto_file.enum_type:
+ add_enum_fields(root.find(enum.name), enum)
+ for message in proto_file.message_type:
+ populate_message(root.find(message.name), message)
+
+
+def build_hierarchy(proto_file):
+ """Creates a ProtoNode hierarchy from a proto file descriptor."""
+
+ root = ProtoPackage(proto_file.package)
+
+ def build_message_subtree(proto_message):
+ node = ProtoMessage(proto_message.name)
+ for enum in proto_message.enum_type:
+ node.add_child(ProtoEnum(enum.name))
+ for submessage in proto_message.nested_type:
+ node.add_child(build_message_subtree(submessage))
+
+ return node
+
+ for enum in proto_file.enum_type:
+ root.add_child(ProtoEnum(enum.name))
+
+ for message in proto_file.message_type:
+ root.add_child(build_message_subtree(message))
+
+ return root
+
+
+def process_proto_file(proto_file):
+ """Generates code for a single .proto file."""
+
+ # Two passes are made through the file. The first builds the tree of all
+ # message/enum nodes, then the second creates the fields in each. This is
+ # done as non-primitive fields need pointers to their types, which requires
+ # the entire tree to have been parsed into memory.
+ root = build_hierarchy(proto_file)
+ populate_fields(proto_file, root)
+
+ output_filename = os.path.splitext(proto_file.name)[0] + '.pb.h'
+ output_file = OutputFile(output_filename)
+ generate_code_for_package(root, output_file)
+
+ return output_file
+
+
+def process_proto_request(req: plugin_pb2.CodeGeneratorRequest,
+ res: plugin_pb2.CodeGeneratorResponse) -> None:
+ """Handles a protoc CodeGeneratorRequest message.
+
+ Generates code for the files in the request and writes the output to the
+ specified CodeGeneratorResponse message.
+
+ Args:
+ req: A CodeGeneratorRequest for a proto compilation.
+ res: A CodeGeneratorResponse to populate with the plugin's output.
+ """
+ for proto_file in req.proto_file:
+ # TODO(frolv): Proto files are currently processed individually. Support
+ # for multiple files with cross-dependencies should be added.
+ output_file = process_proto_file(proto_file)
+ fd = res.file.add()
+ fd.name = output_file.name()
+ fd.content = output_file.content()
+
+
+def main() -> int:
+ """Protobuf compiler plugin entrypoint.
+
+ Reads a CodeGeneratorRequest proto from stdin and writes a
+ CodeGeneratorResponse to stdout.
+ """
+ data = sys.stdin.buffer.read()
+ request = plugin_pb2.CodeGeneratorRequest.FromString(data)
+ response = plugin_pb2.CodeGeneratorResponse()
+ process_proto_request(request, response)
+ sys.stdout.buffer.write(response.SerializeToString())
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/pw_protobuf/py/pw_protobuf/methods.py b/pw_protobuf/py/pw_protobuf/methods.py
new file mode 100644
index 0000000..293448b
--- /dev/null
+++ b/pw_protobuf/py/pw_protobuf/methods.py
@@ -0,0 +1,482 @@
+# Copyright 2019 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 module defines methods for protobuf message C++ encoder classes."""
+
+import abc
+from typing import List, Tuple
+
+import google.protobuf.descriptor_pb2 as descriptor_pb2
+
+from pw_protobuf.proto_structures import ProtoMessageField, ProtoNode
+
+
+class ProtoMethod(abc.ABC):
+ """Base class for a C++ method for a field in a protobuf message."""
+ def __init__(
+ self,
+ field: ProtoMessageField,
+ scope: ProtoNode,
+ root: ProtoNode,
+ ):
+ """Creates an instance of a method.
+
+ Args:
+ field: the ProtoMessageField to which the method belongs.
+ scope: the ProtoNode namespace in which the method is being defined.
+ """
+ self._field: ProtoMessageField = field
+ self._scope: ProtoNode = scope
+ self._root: ProtoNode = root
+
+ @abc.abstractmethod
+ def name(self) -> str:
+ """Returns the name of the method, e.g. DoSomething."""
+
+ @abc.abstractmethod
+ def params(self) -> List[Tuple[str, str]]:
+ """Returns the parameters of the method as a list of (type, name) pairs.
+
+ e.g.
+ [('int', 'foo'), ('const char*', 'bar')]
+ """
+
+ @abc.abstractmethod
+ def body(self) -> List[str]:
+ """Returns the method body as a list of source code lines.
+
+ e.g.
+ [
+ 'int baz = bar[foo];',
+ 'return (baz ^ foo) >> 3;'
+ ]
+ """
+
+ @abc.abstractmethod
+ def return_type(self, 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
+ whether the namespace should be relative to the message's scope
+ (default) or the root scope.
+ """
+
+ @abc.abstractmethod
+ def in_class_definition(self) -> bool:
+ """Determines where the method should be defined.
+
+ Returns True if the method definition should be inlined in its class
+ definition, or False if it should be declared in the class and defined
+ later.
+ """
+
+ def should_appear(self) -> bool: # pylint: disable=no-self-use
+ """Whether the method should be generated."""
+ return True
+
+ def param_string(self) -> str:
+ return ', '.join([f'{type} {name}' for type, name in self.params()])
+
+ def field_cast(self) -> str:
+ return 'static_cast<uint32_t>(Fields::{})'.format(
+ self._field.enum_name())
+
+ def _relative_type_namespace(self, from_root: bool = False) -> str:
+ """Returns relative namespace between method's scope and field type."""
+ scope = self._root if from_root else self._scope
+ ancestor = scope.common_ancestor(self._field.type_node())
+ return self._field.type_node().cpp_namespace(ancestor)
+
+
+class SubMessageMethod(ProtoMethod):
+ """Method which returns a sub-message encoder."""
+ 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 params(self) -> List[Tuple[str, str]]:
+ return []
+
+ def body(self) -> List[str]:
+ line = 'return {}::Encoder(encoder_, {});'.format(
+ self._relative_type_namespace(), self.field_cast())
+ return [line]
+
+ # Submessage methods are not defined within the class itself because the
+ # submessage class may not yet have been defined.
+ def in_class_definition(self) -> bool:
+ return False
+
+
+class WriteMethod(ProtoMethod):
+ """Base class representing an encoder write method.
+
+ Write methods have following format (for the proto field foo):
+
+ Status WriteFoo({params...}) {
+ return encoder_->Write{type}(kFoo, {params...});
+ }
+
+ """
+ def name(self) -> str:
+ return 'Write{}'.format(self._field.name())
+
+ def return_type(self, from_root: bool = False) -> str:
+ return '::pw::Status'
+
+ def body(self) -> List[str]:
+ params = ', '.join([pair[1] for pair in self.params()])
+ line = 'return encoder_->{}({}, {});'.format(self._encoder_fn(),
+ self.field_cast(), params)
+ return [line]
+
+ def params(self) -> List[Tuple[str, str]]:
+ """Method parameters, defined in subclasses."""
+ raise NotImplementedError()
+
+ def in_class_definition(self) -> bool:
+ return True
+
+ def _encoder_fn(self) -> str:
+ """The encoder function to call.
+
+ Defined in subclasses.
+
+ e.g. 'WriteUint32', 'WriteBytes', etc.
+ """
+ raise NotImplementedError()
+
+
+class PackedMethod(WriteMethod):
+ """A method for a packed repeated field.
+
+ Same as a WriteMethod, but is only generated for repeated fields.
+ """
+ def should_appear(self) -> bool:
+ return self._field.is_repeated()
+
+ def _encoder_fn(self) -> str:
+ raise NotImplementedError()
+
+
+#
+# The following code defines write methods for each of the
+# primitive protobuf types.
+#
+
+
+class DoubleMethod(WriteMethod):
+ """Method which writes a proto double value."""
+ def params(self) -> List[Tuple[str, str]]:
+ return [('double', 'value')]
+
+ def _encoder_fn(self) -> str:
+ return 'WriteDouble'
+
+
+class PackedDoubleMethod(PackedMethod):
+ """Method which writes a packed list of doubles."""
+ def params(self) -> List[Tuple[str, str]]:
+ return [('span<const double>', 'values')]
+
+ def _encoder_fn(self) -> str:
+ return 'WritePackedDouble'
+
+
+class FloatMethod(WriteMethod):
+ """Method which writes a proto float value."""
+ def params(self) -> List[Tuple[str, str]]:
+ return [('float', 'value')]
+
+ def _encoder_fn(self) -> str:
+ return 'WriteFloat'
+
+
+class PackedFloatMethod(PackedMethod):
+ """Method which writes a packed list of floats."""
+ def params(self) -> List[Tuple[str, str]]:
+ return [('span<const float>', 'values')]
+
+ def _encoder_fn(self) -> str:
+ return 'WritePackedFloat'
+
+
+class Int32Method(WriteMethod):
+ """Method which writes a proto int32 value."""
+ def params(self) -> List[Tuple[str, str]]:
+ return [('int32_t', 'value')]
+
+ def _encoder_fn(self) -> str:
+ return 'WriteInt32'
+
+
+class PackedInt32Method(PackedMethod):
+ """Method which writes a packed list of int32."""
+ def params(self) -> List[Tuple[str, str]]:
+ return [('span<const int32_t>', 'values')]
+
+ def _encoder_fn(self) -> str:
+ return 'WritePackedInt32'
+
+
+class Sint32Method(WriteMethod):
+ """Method which writes a proto sint32 value."""
+ def params(self) -> List[Tuple[str, str]]:
+ return [('int32_t', 'value')]
+
+ def _encoder_fn(self) -> str:
+ return 'WriteSint32'
+
+
+class PackedSint32Method(PackedMethod):
+ """Method which writes a packed list of sint32."""
+ def params(self) -> List[Tuple[str, str]]:
+ return [('span<const int32_t>', 'values')]
+
+ def _encoder_fn(self) -> str:
+ return 'WritePackedSint32'
+
+
+class Sfixed32Method(WriteMethod):
+ """Method which writes a proto sfixed32 value."""
+ def params(self) -> List[Tuple[str, str]]:
+ return [('int32_t', 'value')]
+
+ def _encoder_fn(self) -> str:
+ return 'WriteSfixed32'
+
+
+class PackedSfixed32Method(PackedMethod):
+ """Method which writes a packed list of sfixed32."""
+ def params(self) -> List[Tuple[str, str]]:
+ return [('span<const int32_t>', 'values')]
+
+ def _encoder_fn(self) -> str:
+ return 'WritePackedSfixed32'
+
+
+class Int64Method(WriteMethod):
+ """Method which writes a proto int64 value."""
+ def params(self) -> List[Tuple[str, str]]:
+ return [('int64_t', 'value')]
+
+ def _encoder_fn(self) -> str:
+ return 'WriteInt64'
+
+
+class PackedInt64Method(PackedMethod):
+ """Method which writes a proto int64 value."""
+ def params(self) -> List[Tuple[str, str]]:
+ return [('span<const int64_t>', 'values')]
+
+ def _encoder_fn(self) -> str:
+ return 'WritePackedInt64'
+
+
+class Sint64Method(WriteMethod):
+ """Method which writes a proto sint64 value."""
+ def params(self) -> List[Tuple[str, str]]:
+ return [('int64_t', 'value')]
+
+ def _encoder_fn(self) -> str:
+ return 'WriteSint64'
+
+
+class PackedSint64Method(PackedMethod):
+ """Method which writes a proto sint64 value."""
+ def params(self) -> List[Tuple[str, str]]:
+ return [('span<const int64_t>', 'values')]
+
+ def _encoder_fn(self) -> str:
+ return 'WritePackedSint64'
+
+
+class Sfixed64Method(WriteMethod):
+ """Method which writes a proto sfixed64 value."""
+ def params(self) -> List[Tuple[str, str]]:
+ return [('int64_t', 'value')]
+
+ def _encoder_fn(self) -> str:
+ return 'WriteSfixed64'
+
+
+class PackedSfixed64Method(PackedMethod):
+ """Method which writes a proto sfixed64 value."""
+ def params(self) -> List[Tuple[str, str]]:
+ return [('span<const int64_t>', 'values')]
+
+ def _encoder_fn(self) -> str:
+ return 'WritePackedSfixed4'
+
+
+class Uint32Method(WriteMethod):
+ """Method which writes a proto uint32 value."""
+ def params(self) -> List[Tuple[str, str]]:
+ return [('uint32_t', 'value')]
+
+ def _encoder_fn(self) -> str:
+ return 'WriteUint32'
+
+
+class PackedUint32Method(PackedMethod):
+ """Method which writes a proto uint32 value."""
+ def params(self) -> List[Tuple[str, str]]:
+ return [('span<const uint32_t>', 'values')]
+
+ def _encoder_fn(self) -> str:
+ return 'WritePackedUint32'
+
+
+class Fixed32Method(WriteMethod):
+ """Method which writes a proto fixed32 value."""
+ def params(self) -> List[Tuple[str, str]]:
+ return [('uint32_t', 'value')]
+
+ def _encoder_fn(self) -> str:
+ return 'WriteFixed32'
+
+
+class PackedFixed32Method(PackedMethod):
+ """Method which writes a proto fixed32 value."""
+ def params(self) -> List[Tuple[str, str]]:
+ return [('span<const uint32_t>', 'values')]
+
+ def _encoder_fn(self) -> str:
+ return 'WritePackedFixed32'
+
+
+class Uint64Method(WriteMethod):
+ """Method which writes a proto uint64 value."""
+ def params(self) -> List[Tuple[str, str]]:
+ return [('uint64_t', 'value')]
+
+ def _encoder_fn(self) -> str:
+ return 'WriteUint64'
+
+
+class PackedUint64Method(PackedMethod):
+ """Method which writes a proto uint64 value."""
+ def params(self) -> List[Tuple[str, str]]:
+ return [('span<const uint64_t>', 'values')]
+
+ def _encoder_fn(self) -> str:
+ return 'WritePackedUint64'
+
+
+class Fixed64Method(WriteMethod):
+ """Method which writes a proto fixed64 value."""
+ def params(self) -> List[Tuple[str, str]]:
+ return [('uint64_t', 'value')]
+
+ def _encoder_fn(self) -> str:
+ return 'WriteFixed64'
+
+
+class PackedFixed64Method(PackedMethod):
+ """Method which writes a proto fixed64 value."""
+ def params(self) -> List[Tuple[str, str]]:
+ return [('span<const uint64_t>', 'values')]
+
+ def _encoder_fn(self) -> str:
+ return 'WritePackedFixed64'
+
+
+class BoolMethod(WriteMethod):
+ """Method which writes a proto bool value."""
+ def params(self) -> List[Tuple[str, str]]:
+ return [('bool', 'value')]
+
+ def _encoder_fn(self) -> str:
+ return 'WriteBool'
+
+
+class BytesMethod(WriteMethod):
+ """Method which writes a proto bytes value."""
+ def params(self) -> List[Tuple[str, str]]:
+ return [('span<const std::byte>', 'value')]
+
+ def _encoder_fn(self) -> str:
+ return 'WriteBytes'
+
+
+class StringLenMethod(WriteMethod):
+ """Method which writes a proto string value with length."""
+ def params(self) -> List[Tuple[str, str]]:
+ return [('const char*', 'value'), ('size_t', 'len')]
+
+ def _encoder_fn(self) -> str:
+ return 'WriteString'
+
+
+class StringMethod(WriteMethod):
+ """Method which writes a proto string value."""
+ def params(self) -> List[Tuple[str, str]]:
+ return [('const char*', 'value')]
+
+ def _encoder_fn(self) -> str:
+ return 'WriteString'
+
+
+class EnumMethod(WriteMethod):
+ """Method which writes a proto enum value."""
+ 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())
+ return [line]
+
+ def in_class_definition(self) -> bool:
+ return True
+
+ def _encoder_fn(self) -> str:
+ raise NotImplementedError()
+
+
+# Mapping of protobuf field types to their method definitions.
+PROTO_FIELD_METHODS = {
+ descriptor_pb2.FieldDescriptorProto.TYPE_DOUBLE:
+ [DoubleMethod, PackedDoubleMethod],
+ descriptor_pb2.FieldDescriptorProto.TYPE_FLOAT:
+ [FloatMethod, PackedFloatMethod],
+ descriptor_pb2.FieldDescriptorProto.TYPE_INT32:
+ [Int32Method, PackedInt32Method],
+ descriptor_pb2.FieldDescriptorProto.TYPE_SINT32:
+ [Sint32Method, PackedSint32Method],
+ descriptor_pb2.FieldDescriptorProto.TYPE_SFIXED32:
+ [Sfixed32Method, PackedSfixed32Method],
+ descriptor_pb2.FieldDescriptorProto.TYPE_INT64:
+ [Int64Method, PackedInt64Method],
+ descriptor_pb2.FieldDescriptorProto.TYPE_SINT64:
+ [Sint64Method, PackedSint64Method],
+ descriptor_pb2.FieldDescriptorProto.TYPE_SFIXED64:
+ [Sfixed64Method, PackedSfixed64Method],
+ descriptor_pb2.FieldDescriptorProto.TYPE_UINT32:
+ [Uint32Method, PackedUint32Method],
+ descriptor_pb2.FieldDescriptorProto.TYPE_FIXED32:
+ [Fixed32Method, PackedFixed32Method],
+ descriptor_pb2.FieldDescriptorProto.TYPE_UINT64:
+ [Uint64Method, PackedUint64Method],
+ descriptor_pb2.FieldDescriptorProto.TYPE_FIXED64:
+ [Fixed64Method, PackedFixed64Method],
+ descriptor_pb2.FieldDescriptorProto.TYPE_BOOL: [BoolMethod],
+ descriptor_pb2.FieldDescriptorProto.TYPE_BYTES: [BytesMethod],
+ descriptor_pb2.FieldDescriptorProto.TYPE_STRING: [
+ StringLenMethod, StringMethod
+ ],
+ descriptor_pb2.FieldDescriptorProto.TYPE_MESSAGE: [SubMessageMethod],
+ descriptor_pb2.FieldDescriptorProto.TYPE_ENUM: [EnumMethod],
+}
diff --git a/pw_protobuf/py/pw_protobuf/proto_structures.py b/pw_protobuf/py/pw_protobuf/proto_structures.py
new file mode 100644
index 0000000..b0bcaf6
--- /dev/null
+++ b/pw_protobuf/py/pw_protobuf/proto_structures.py
@@ -0,0 +1,271 @@
+# Copyright 2019 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 module defines data structures for protobuf entities."""
+
+import abc
+import collections
+import enum
+
+from typing import Callable, Dict, Iterator, List, Optional, Tuple, TypeVar
+
+T = TypeVar('T') # pylint: disable=invalid-name
+
+
+class ProtoNode(abc.ABC):
+ """A ProtoNode represents a C++ scope mapping of an entity in a .proto file.
+
+ Nodes form a tree beginning at a top-level (global) scope, descending into a
+ hierarchy of .proto packages and the messages and enums defined within them.
+ """
+ class Type(enum.Enum):
+ """The type of a ProtoNode.
+
+ PACKAGE maps to a C++ namespace.
+ MESSAGE maps to a C++ "Encoder" class within its own namespace.
+ ENUM maps to a C++ enum within its parent's namespace.
+ """
+ PACKAGE = 1
+ MESSAGE = 2
+ ENUM = 3
+
+ def __init__(self, name: str):
+ self._name: str = name
+ self._children: Dict[str, 'ProtoNode'] = collections.OrderedDict()
+ self._parent: Optional['ProtoNode'] = None
+
+ @abc.abstractmethod
+ def type(self) -> 'ProtoNode.Type':
+ """The type of the node."""
+
+ def children(self) -> List['ProtoNode']:
+ return self._children.values()
+
+ def name(self) -> str:
+ return self._name
+
+ def cpp_name(self) -> str:
+ """The name of this node in generated C++ code."""
+ return self._name.replace('.', '::')
+
+ def cpp_namespace(self, root: Optional['ProtoNode'] = None) -> str:
+ """C++ namespace of the node, up to the specified root."""
+ return '::'.join(
+ self._attr_hierarchy(lambda node: node.cpp_name(), root))
+
+ def common_ancestor(self, other: 'ProtoNode') -> Optional['ProtoNode']:
+ """Finds the earliest common ancestor of this node and other."""
+
+ if other is None:
+ return None
+
+ own_depth = self.depth()
+ other_depth = other.depth()
+ diff = abs(own_depth - other_depth)
+
+ if own_depth < other_depth:
+ first: Optional['ProtoNode'] = self
+ second: Optional['ProtoNode'] = other
+ else:
+ first = other
+ second = self
+
+ while diff > 0:
+ second = second.parent()
+ diff -= 1
+
+ while first != second:
+ if first is None or second is None:
+ return None
+
+ first = first.parent()
+ second = second.parent()
+
+ return first
+
+ def depth(self) -> int:
+ """Returns the depth of this node from the root."""
+ depth = 0
+ node = self._parent
+ while node:
+ depth += 1
+ node = node.parent()
+ return depth
+
+ def add_child(self, child: 'ProtoNode') -> None:
+ """Inserts a new node into the tree as a child of this node.
+
+ Args:
+ child: The node to insert.
+
+ Raises:
+ ValueError: This node does not allow nesting the given type of child.
+ """
+ if not self._supports_child(child):
+ raise ValueError('Invalid child %s for node of type %s' %
+ (child.type(), self.type()))
+
+ # pylint: disable=protected-access
+ if child.parent() is not None:
+ del child._parent._children[child.name()]
+
+ child._parent = self
+ self._children[child.name()] = child
+ # pylint: enable=protected-access
+
+ def find(self, path: str) -> Optional['ProtoNode']:
+ """Finds a node within this node's subtree."""
+ node = self
+
+ # pylint: disable=protected-access
+ for section in path.split('.'):
+ node = node._children.get(section)
+ if node is None:
+ return None
+ # pylint: enable=protected-access
+
+ return node
+
+ def parent(self) -> Optional['ProtoNode']:
+ return self._parent
+
+ def __iter__(self) -> Iterator['ProtoNode']:
+ """Iterates depth-first through all nodes in this node's subtree."""
+ yield self
+ for child_iterator in self._children.values():
+ for child in child_iterator:
+ yield child
+
+ def _attr_hierarchy(
+ self,
+ attr_accessor: Callable[['ProtoNode'], T],
+ root: Optional['ProtoNode'],
+ ) -> Iterator[T]:
+ """Fetches node attributes at each level of the tree from the root.
+
+ Args:
+ attr_accessor: Function which extracts attributes from a ProtoNode.
+ root: The node at which to terminate.
+
+ Returns:
+ An iterator to a list of the selected attributes from the root to the
+ current node.
+ """
+ hierarchy = []
+ node: Optional['ProtoNode'] = self
+ while node is not None and node != root:
+ hierarchy.append(attr_accessor(node))
+ node = node.parent()
+ return reversed(hierarchy)
+
+ @abc.abstractmethod
+ def _supports_child(self, child: 'ProtoNode') -> bool:
+ """Returns True if child is a valid child type for the current node."""
+
+
+class ProtoPackage(ProtoNode):
+ """A protobuf package."""
+ def type(self) -> ProtoNode.Type:
+ return ProtoNode.Type.PACKAGE
+
+ def _supports_child(self, child: ProtoNode) -> bool:
+ return True
+
+
+class ProtoEnum(ProtoNode):
+ """Representation of an enum in a .proto file."""
+
+ # Prefix for names of values in C++ enums.
+ ENUM_PREFIX: str = 'k'
+
+ def __init__(self, name: str):
+ super().__init__(name)
+ self._values: List[Tuple[str, int]] = []
+
+ def type(self) -> ProtoNode.Type:
+ return ProtoNode.Type.ENUM
+
+ def values(self) -> List[Tuple[str, int]]:
+ return list(self._values)
+
+ def add_value(self, name: str, value: int) -> None:
+ name = '{}{}'.format(ProtoEnum.ENUM_PREFIX,
+ ProtoMessageField.canonicalize_name(name))
+ self._values.append((name, value))
+
+ def _supports_child(self, child: ProtoNode) -> bool:
+ # Enums cannot have nested children.
+ return False
+
+
+class ProtoMessage(ProtoNode):
+ """Representation of a message in a .proto file."""
+ def __init__(self, name: str):
+ super().__init__(name)
+ self._fields: List['ProtoMessageField'] = []
+
+ def type(self) -> ProtoNode.Type:
+ return ProtoNode.Type.MESSAGE
+
+ def fields(self) -> List['ProtoMessageField']:
+ return list(self._fields)
+
+ def add_field(self, field: 'ProtoMessageField') -> None:
+ self._fields.append(field)
+
+ def _supports_child(self, child: ProtoNode) -> bool:
+ return (child.type() == self.Type.ENUM
+ or child.type() == self.Type.MESSAGE)
+
+
+# This class is not a node and does not appear in the proto tree.
+# Fields belong to proto messages and are processed separately.
+class ProtoMessageField:
+ """Representation of a field within a protobuf message."""
+ def __init__(self,
+ field_name: str,
+ field_number: int,
+ field_type: int,
+ type_node: Optional[ProtoNode] = None,
+ repeated: bool = False):
+ self._name: str = self.canonicalize_name(field_name)
+ self._number: int = field_number
+ self._type: int = field_type
+ self._type_node: Optional[ProtoNode] = type_node
+ self._repeated: bool = repeated
+
+ def name(self) -> str:
+ return self._name
+
+ def enum_name(self) -> str:
+ return '{}{}'.format(ProtoEnum.ENUM_PREFIX, self._name)
+
+ def number(self) -> int:
+ return self._number
+
+ def type(self) -> int:
+ return self._type
+
+ def type_node(self) -> Optional[ProtoNode]:
+ return self._type_node
+
+ def is_repeated(self) -> bool:
+ return self._repeated
+
+ @staticmethod
+ def canonicalize_name(field_name: str) -> str:
+ """Converts a field name to UpperCamelCase."""
+ name_components = field_name.split('_')
+ for i, _ in enumerate(name_components):
+ name_components[i] = name_components[i].lower().capitalize()
+ return ''.join(name_components)
diff --git a/pw_protobuf/py/setup.py b/pw_protobuf/py/setup.py
new file mode 100644
index 0000000..06192fd
--- /dev/null
+++ b/pw_protobuf/py/setup.py
@@ -0,0 +1,39 @@
+# Copyright 2019 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.
+"""pw_protobuf"""
+
+import unittest
+import setuptools
+
+
+def test_suite():
+ """Test suite for pw_protobuf module."""
+ return unittest.TestLoader().discover('./', pattern='*_test.py')
+
+
+setuptools.setup(
+ name='pw_protobuf',
+ version='0.0.1',
+ author='Pigweed Authors',
+ author_email='pigweed-developers@googlegroups.com',
+ description='Lightweight streaming protobuf implementation',
+ packages=setuptools.find_packages(),
+ test_suite='setup.test_suite',
+ entry_points={
+ 'console_scripts': ['pw_protobuf_codegen = pw_protobuf.codegen:main']
+ },
+ install_requires=[
+ 'protobuf',
+ ],
+)
diff --git a/pw_protobuf_compiler/proto.gni b/pw_protobuf_compiler/proto.gni
index d20679a..208d816 100644
--- a/pw_protobuf_compiler/proto.gni
+++ b/pw_protobuf_compiler/proto.gni
@@ -43,6 +43,10 @@
] + get_path_info(invoker.protos, "abspath")
inputs = invoker.protos
outputs = _outputs
+
+ if (defined(invoker.protoc_deps)) {
+ deps = invoker.protoc_deps
+ }
}
# For C++ proto files, the generated proto directory is added as an include
@@ -105,6 +109,16 @@
_pw_cc_proto_library(_lang_target) {
protos = invoker.sources
deps = _lang_deps
+
+ # List the pw_protobuf plugin's files as a dependency to recompile
+ # generated code if they are modified.
+ #
+ # TODO(frolv): This check is currently true as pw_protobuf is the
+ # only supported plugin. It should be updated to support other proto
+ # libraries such as nanopb.
+ if (true) {
+ protoc_deps = [ "$dir_pw_protobuf:codegen_protoc_plugin" ]
+ }
}
} else {
assert(false,
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 26003e3..2bbecb2 100644
--- a/pw_protobuf_compiler/py/pw_protobuf_compiler/generate_protos.py
+++ b/pw_protobuf_compiler/py/pw_protobuf_compiler/generate_protos.py
@@ -15,6 +15,7 @@
import argparse
import logging
+import shutil
import sys
from typing import Optional
@@ -27,8 +28,7 @@
# Default protoc codegen plugins for each supported language.
# TODO(frolv): Make these overridable with a command-line argument.
DEFAULT_PROTOC_PLUGINS = {
- # TODO(frolv): Enable this when porting the pw_protobuf module.
- # 'cc': 'protoc-gen-custom=pw_protobuf_codegen',
+ 'cc': f'protoc-gen-custom={shutil.which("pw_protobuf_codegen")}',
}