pw_rpc: Nanopb RPC protoc plugin

This change adds a protoc plugin which generates service code for pw_rpc
using nanopb as the protobuf implementation. Only the plugin code is
added here; it is not yet integrated into the build system.

Change-Id: Ia24bb5d130bf54c7afb41af2bca7f346e14bed9b
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/12941
Commit-Queue: Alexei Frolov <frolv@google.com>
Reviewed-by: Wyatt Hepler <hepler@google.com>
diff --git a/pw_protobuf/py/pw_protobuf/codegen_pwpb.py b/pw_protobuf/py/pw_protobuf/codegen_pwpb.py
index b9760fa..4588753 100644
--- a/pw_protobuf/py/pw_protobuf/codegen_pwpb.py
+++ b/pw_protobuf/py/pw_protobuf/codegen_pwpb.py
@@ -21,6 +21,7 @@
 
 import google.protobuf.descriptor_pb2 as descriptor_pb2
 
+from pw_protobuf.output_file import OutputFile
 from pw_protobuf.proto_tree import ProtoMessageField, ProtoNode
 from pw_protobuf.proto_tree import build_node_tree
 
@@ -500,66 +501,6 @@
 }
 
 
-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."""
diff --git a/pw_protobuf/py/pw_protobuf/output_file.py b/pw_protobuf/py/pw_protobuf/output_file.py
new file mode 100644
index 0000000..04bd0ee
--- /dev/null
+++ b/pw_protobuf/py/pw_protobuf/output_file.py
@@ -0,0 +1,80 @@
+# Copyright 2020 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.
+"""Defines a class used to write code to an output buffer."""
+
+from typing import List
+
+
+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,
+        amount: int = INDENT_WIDTH,
+    ) -> 'OutputFile._IndentationContext':
+        """Increases the indentation level of the output."""
+        return self._IndentationContext(self, amount)
+
+    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', amount: int):
+            self._output = output
+            self._amount: int = amount
+
+        def __enter__(self):
+            self._output._indentation += self._amount
+
+        def __exit__(self, typ, value, traceback):
+            self._output._indentation -= self._amount
diff --git a/pw_protobuf/py/pw_protobuf/proto_tree.py b/pw_protobuf/py/pw_protobuf/proto_tree.py
index 7b9e361..9313689 100644
--- a/pw_protobuf/py/pw_protobuf/proto_tree.py
+++ b/pw_protobuf/py/pw_protobuf/proto_tree.py
@@ -70,6 +70,11 @@
         return '::'.join(
             self._attr_hierarchy(lambda node: node.cpp_name(), root))
 
+    def nanopb_name(self) -> str:
+        """Full nanopb-style name of the node."""
+        name = '_'.join(self._attr_hierarchy(lambda node: node.name(), None))
+        return name.lstrip('_')
+
     def common_ancestor(self, other: 'ProtoNode') -> Optional['ProtoNode']:
         """Finds the earliest common ancestor of this node and other."""
 
@@ -329,6 +334,15 @@
     def name(self) -> str:
         return self._name
 
+    def type(self) -> Type:
+        return self._type
+
+    def request_type(self) -> ProtoNode:
+        return self._request_type
+
+    def response_type(self) -> ProtoNode:
+        return self._response_type
+
 
 def _add_enum_fields(enum_node: ProtoNode, proto_enum) -> None:
     """Adds fields from a protobuf enum descriptor to an enum node."""
diff --git a/pw_rpc/BUILD b/pw_rpc/BUILD
index b2f3f2c..36e8c8b 100644
--- a/pw_rpc/BUILD
+++ b/pw_rpc/BUILD
@@ -90,9 +90,13 @@
 filegroup(
     name = "nanopb",
     srcs = [
+        "nanopb/codegen_manual_test.cc",
         "nanopb/method.cc",
         "nanopb/method_test.cc",
         "nanopb/public_overrides/pw_rpc/internal/method.h",
+        "nanopb/test.pb.c",
+        "nanopb/test.pb.h",
+        "nanopb/test_rpc.pb.h",
     ],
 )
 
diff --git a/pw_rpc/pw_rpc_test_protos/test.proto b/pw_rpc/pw_rpc_test_protos/test.proto
index 48e8cc5..3a67f91 100644
--- a/pw_rpc/pw_rpc_test_protos/test.proto
+++ b/pw_rpc/pw_rpc_test_protos/test.proto
@@ -23,4 +23,13 @@
   int32 value = 1;
 }
 
+message TestStreamResponse {
+  bytes chunk = 1;
+}
+
 message Empty {}
+
+service TestService {
+  rpc TestRpc(TestRequest) returns (TestResponse) {}
+  rpc TestStreamRpc(Empty) returns (stream TestStreamResponse) {}
+}
diff --git a/pw_rpc/py/codegen_test.py b/pw_rpc/py/codegen_test.py
new file mode 100644
index 0000000..d72b6e5
--- /dev/null
+++ b/pw_rpc/py/codegen_test.py
@@ -0,0 +1,138 @@
+#!/usr/bin/env python3
+# Copyright 2020 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.
+"""Tests the generated pw_rpc code."""
+
+from pathlib import Path
+import os
+import subprocess
+import tempfile
+import unittest
+
+TEST_PROTO_FILE = b"""\
+syntax = "proto3";
+
+package pw.rpc.test;
+
+message TestRequest {
+  float integer = 1;
+}
+
+message TestResponse {
+  int32 value = 1;
+}
+
+message TestStreamResponse {
+  bytes chunk = 1;
+}
+
+message Empty {}
+
+service TestService {
+  rpc TestRpc(TestRequest) returns (TestResponse) {}
+  rpc TestStreamRpc(Empty) returns (stream TestStreamResponse) {}
+}
+"""
+
+EXPECTED_NANOPB_CODE = """\
+#pragma once
+
+#include <cstddef>
+#include <cstdint>
+
+#include "pw_rpc/internal/service.h"
+#include "test.pb.h"
+
+
+namespace pw::rpc::test {
+
+class TestService : public ::pw::rpc::internal::Service {
+ public:
+  using ServerContext = ::pw::rpc::ServerContext;
+  template <typename T> using ServerWriter = ::pw::rpc::ServerWriter<T>;
+
+  constexpr TestService() : ::pw::rpc::internal::Service(kServiceId, kMethods) {}
+
+  TestService(const TestService&) = delete;
+  TestService& operator=(const TestService&) = delete;
+
+  static ::pw::Status TestRpc(
+      ServerContext& ctx,
+      const pw_rpc_test_TestRequest& request,
+      pw_rpc_test_TestResponse& response);
+
+  static ::pw::Status TestStreamRpc(
+      ServerContext& ctx,
+      const pw_rpc_test_Empty& request,
+      ServerWriter<pw_rpc_test_TestStreamResponse>& writer);
+
+ private:
+  // 65599 hash of "TestService".
+  static constexpr uint32_t kServiceId = 0x105b6ac8;
+  static constexpr char* kServiceName = "TestService";
+
+  static constexpr std::array<::pw::rpc::internal::Method, 2> kMethods = {
+      ::pw::rpc::internal::Method::Unary<TestRpc>(
+          0xbc924054,  // 65599 hash of "TestRpc"
+          pw_rpc_test_TestRequest_fields,
+          pw_rpc_test_TestResponse_fields),
+      ::pw::rpc::internal::Method::ServerStreaming<TestStreamRpc>(
+          0xd97a28fa,  // 65599 hash of "TestStreamRpc"
+          pw_rpc_test_Empty_fields,
+          pw_rpc_test_TestStreamResponse_fields),
+  };
+};
+
+}  // namespace pw::rpc::test
+"""
+
+
+class TestNanopbCodegen(unittest.TestCase):
+    """Test case for nanopb code generation."""
+    def setUp(self):
+        self._output_dir = tempfile.TemporaryDirectory()
+
+    def tearDown(self):
+        self._output_dir.cleanup()
+
+    def test_nanopb_codegen(self):
+        root = Path(os.getenv('PW_ROOT'))
+        proto_dir = root / 'pw_rpc' / 'pw_rpc_test_protos'
+        proto_file = proto_dir / 'test.proto'
+
+        venv_bin = 'Scripts' if os.name == 'nt' else 'bin'
+        plugin = root / '.python3-env' / venv_bin / 'pw_rpc_codegen'
+
+        command = (
+            'protoc',
+            f'-I{proto_dir}',
+            proto_file,
+            '--plugin',
+            f'protoc-gen-custom={plugin}',
+            '--custom_out',
+            self._output_dir.name,
+        )
+
+        subprocess.run(command)
+
+        generated_files = os.listdir(self._output_dir.name)
+        self.assertEqual(len(generated_files), 1)
+        self.assertEqual(generated_files[0], 'test_rpc.pb.h')
+
+        # Read the generated file, ignoring its preamble.
+        generated_code = Path(self._output_dir.name,
+                              generated_files[0]).read_text()
+        generated_code = generated_code[generated_code.index('#pragma'):]
+
+        self.assertEqual(generated_code, EXPECTED_NANOPB_CODE)
diff --git a/pw_rpc/py/pw_rpc/codegen_nanopb.py b/pw_rpc/py/pw_rpc/codegen_nanopb.py
new file mode 100644
index 0000000..27b2573
--- /dev/null
+++ b/pw_rpc/py/pw_rpc/codegen_nanopb.py
@@ -0,0 +1,200 @@
+# Copyright 2020 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 generates the code for nanopb-based pw_rpc services."""
+
+from datetime import datetime
+import os
+from typing import Iterable
+
+from pw_protobuf.output_file import OutputFile
+from pw_protobuf.proto_tree import ProtoNode, ProtoServiceMethod
+from pw_protobuf.proto_tree import build_node_tree
+import pw_rpc.ids
+
+PLUGIN_NAME = 'pw_rpc_codegen'
+PLUGIN_VERSION = '0.1.0'
+
+PROTO_H_EXTENSION = '.pb.h'
+PROTO_CC_EXTENSION = '.pb.cc'
+NANOPB_H_EXTENSION = '.pb.h'
+
+RPC_NAMESPACE = '::pw::rpc'
+
+
+def _proto_filename_to_nanopb_header(proto_file: str) -> str:
+    """Returns the generated nanopb header name for a .proto file."""
+    return os.path.splitext(proto_file)[0] + NANOPB_H_EXTENSION
+
+
+def _proto_filename_to_generated_header(proto_file: str) -> str:
+    """Returns the generated C++ RPC header name for a .proto file."""
+    filename = os.path.splitext(proto_file)[0]
+    return f'{filename}_rpc{PROTO_H_EXTENSION}'
+
+
+def _generate_method_descriptor(method: ProtoServiceMethod,
+                                output: OutputFile) -> None:
+    """Generates a nanopb method descriptor for an RPC method."""
+
+    method_class = f'{RPC_NAMESPACE}::internal::Method'
+
+    if method.type() == ProtoServiceMethod.Type.UNARY:
+        func = f'{method_class}::Unary<{method.name()}>'
+    elif method.type() == ProtoServiceMethod.Type.SERVER_STREAMING:
+        func = f'{method_class}::ServerStreaming<{method.name()}>'
+    else:
+        raise NotImplementedError(
+            'Only unary and server streaming RPCs are currently supported')
+
+    method_id = pw_rpc.ids.calculate(method.name())
+    req_fields = f'{method.request_type().nanopb_name()}_fields'
+    res_fields = f'{method.response_type().nanopb_name()}_fields'
+
+    output.write_line(f'{func}(')
+    with output.indent(4):
+        output.write_line(
+            f'{hex(method_id)},  // 65599 hash of "{method.name()}"')
+        output.write_line(f'{req_fields},')
+        output.write_line(f'{res_fields}),')
+
+
+def _generate_code_for_method(method: ProtoServiceMethod,
+                              output: OutputFile) -> None:
+    """Generates the function singature of a nanopb RPC method."""
+
+    req_type = method.request_type().nanopb_name()
+    res_type = method.response_type().nanopb_name()
+    signature = f'static ::pw::Status {method.name()}'
+
+    output.write_line()
+
+    if method.type() == ProtoServiceMethod.Type.UNARY:
+        output.write_line(f'{signature}(')
+        with output.indent(4):
+            output.write_line('ServerContext& ctx,')
+            output.write_line(f'const {req_type}& request,')
+            output.write_line(f'{res_type}& response);')
+    elif method.type() == ProtoServiceMethod.Type.SERVER_STREAMING:
+        output.write_line(f'{signature}(')
+        with output.indent(4):
+            output.write_line('ServerContext& ctx,')
+            output.write_line(f'const {req_type}& request,')
+            output.write_line(f'ServerWriter<{res_type}>& writer);')
+    else:
+        raise NotImplementedError(
+            'Only unary and server streaming RPCs are currently supported')
+
+
+def _generate_code_for_service(service: ProtoNode, root: ProtoNode,
+                               output: OutputFile) -> None:
+    """Generates a C++ derived class for a nanopb RPC service."""
+
+    base_class = f'{RPC_NAMESPACE}::internal::Service'
+    output.write_line(
+        f'\nclass {service.cpp_namespace(root)} : public {base_class} {{')
+    output.write_line(' public:')
+
+    with output.indent():
+        output.write_line(
+            f'using ServerContext = {RPC_NAMESPACE}::ServerContext;')
+        output.write_line('template <typename T> using ServerWriter = '
+                          f'{RPC_NAMESPACE}::ServerWriter<T>;')
+        output.write_line()
+
+        output.write_line(f'constexpr {service.name()}()'
+                          f' : {base_class}(kServiceId, kMethods) {{}}')
+
+        output.write_line()
+        output.write_line(
+            f'{service.name()}(const {service.name()}&) = delete;')
+        output.write_line(f'{service.name()}& operator='
+                          f'(const {service.name()}&) = delete;')
+
+        for method in service.methods():
+            _generate_code_for_method(method, output)
+
+    service_name_hash = pw_rpc.ids.calculate(service.name())
+    output.write_line('\n private:')
+
+    with output.indent():
+        output.write_line(f'// 65599 hash of "{service.name()}".')
+        output.write_line(
+            f'static constexpr uint32_t kServiceId = {hex(service_name_hash)};'
+        )
+        output.write_line(
+            f'static constexpr char* kServiceName = "{service.name()}";')
+        output.write_line()
+
+        output.write_line(
+            f'static constexpr std::array<{RPC_NAMESPACE}::internal::Method,'
+            f' {len(service.methods())}> kMethods = {{')
+
+        with output.indent(4):
+            for method in service.methods():
+                _generate_method_descriptor(method, output)
+
+        output.write_line('};')
+
+    output.write_line('};')
+
+
+def generate_code_for_package(file_descriptor_proto, package: ProtoNode,
+                              output: OutputFile) -> None:
+    """Generates code for a header 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(f'// on {datetime.now()}')
+    output.write_line('// clang-format off')
+    output.write_line('#pragma once\n')
+    output.write_line('#include <cstddef>')
+    output.write_line('#include <cstdint>\n')
+    output.write_line('#include "pw_rpc/internal/service.h"')
+
+    # Include the corresponding nanopb header file for this proto file, in which
+    # the file's messages and enums are generated.
+    nanopb_header = _proto_filename_to_nanopb_header(
+        file_descriptor_proto.name)
+    output.write_line(f'#include "{nanopb_header}"\n')
+
+    for imported_file in file_descriptor_proto.dependency:
+        generated_header = _proto_filename_to_nanopb_header(imported_file)
+        output.write_line(f'#include "{generated_header}"')
+
+    if package.cpp_namespace():
+        file_namespace = package.cpp_namespace()
+        if file_namespace.startswith('::'):
+            file_namespace = file_namespace[2:]
+
+        output.write_line(f'\nnamespace {file_namespace} {{')
+
+    for node in package:
+        if node.type() == ProtoNode.Type.SERVICE:
+            _generate_code_for_service(node, package, output)
+
+    if package.cpp_namespace():
+        output.write_line(f'\n}}  // namespace {file_namespace}')
+
+
+def process_proto_file(proto_file) -> Iterable[OutputFile]:
+    """Generates code for a single .proto file."""
+
+    _, package_root = build_node_tree(proto_file)
+    output_filename = _proto_filename_to_generated_header(proto_file.name)
+    output_file = OutputFile(output_filename)
+    generate_code_for_package(proto_file, package_root, output_file)
+
+    return [output_file]
diff --git a/pw_rpc/py/pw_rpc/ids.py b/pw_rpc/py/pw_rpc/ids.py
new file mode 100644
index 0000000..fa56f02
--- /dev/null
+++ b/pw_rpc/py/pw_rpc/ids.py
@@ -0,0 +1,34 @@
+# Copyright 2020 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 the string to ID hash used in pw_rpc."""
+
+HASH_CONSTANT = 65599
+
+
+# This is the same hash function that is used in pw_tokenizer, with the maximum
+# length removed. It is chosen due to its simplicity. The tokenizer code is
+# duplicated here to avoid unnecessary dependencies between modules.
+def hash_65599(string: str) -> int:
+    hash_value = len(string)
+    coefficient = HASH_CONSTANT
+
+    for char in string:
+        hash_value = (hash_value + coefficient * ord(char)) % 2**32
+        coefficient = (coefficient * HASH_CONSTANT) % 2**32
+
+    return hash_value
+
+
+# Function that converts a name to an ID.
+calculate = hash_65599
diff --git a/pw_rpc/py/pw_rpc/plugin.py b/pw_rpc/py/pw_rpc/plugin.py
new file mode 100644
index 0000000..a7ea616
--- /dev/null
+++ b/pw_rpc/py/pw_rpc/plugin.py
@@ -0,0 +1,60 @@
+# Copyright 2020 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_rpc compiler plugin.
+
+protoc plugin which generates C++ code for pw_rpc services using nanopb.
+"""
+
+import sys
+
+import google.protobuf.compiler.plugin_pb2 as plugin_pb2
+
+import pw_rpc.codegen_nanopb as codegen_nanopb
+
+
+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:
+        output_files = codegen_nanopb.process_proto_file(proto_file)
+        for output_file in output_files:
+            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_rpc/py/setup.py b/pw_rpc/py/setup.py
new file mode 100644
index 0000000..07c6b2c
--- /dev/null
+++ b/pw_rpc/py/setup.py
@@ -0,0 +1,30 @@
+# Copyright 2020 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 setuptools
+
+setuptools.setup(
+    name='pw_rpc',
+    version='0.0.1',
+    author='Pigweed Authors',
+    author_email='pigweed-developers@googlegroups.com',
+    description='On-device remote procedure calls',
+    packages=setuptools.find_packages(),
+    entry_points={'console_scripts': ['pw_rpc_codegen = pw_rpc.plugin:main']},
+    install_requires=[
+        'protobuf',
+        'pw_protobuf',
+    ],
+)