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")}',
 }