| # Copyright 2021 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. |
| """Common RPC codegen utilities.""" |
| |
| import abc |
| from datetime import datetime |
| import os |
| from typing import cast, Any, Iterable, Union |
| |
| from pw_protobuf.output_file import OutputFile |
| from pw_protobuf.proto_tree import ProtoNode, ProtoService, ProtoServiceMethod |
| from pw_rpc import ids |
| |
| PLUGIN_NAME = 'pw_rpc_codegen' |
| PLUGIN_VERSION = '0.3.0' |
| |
| RPC_NAMESPACE = '::pw::rpc' |
| |
| STUB_REQUEST_TODO = ( |
| '// TODO: Read the request as appropriate for your application') |
| STUB_RESPONSE_TODO = ( |
| '// TODO: Fill in the response as appropriate for your application') |
| STUB_WRITER_TODO = ( |
| '// TODO: Send responses with the writer as appropriate for your ' |
| 'application') |
| STUB_READER_TODO = ( |
| '// TODO: Set the client stream callback and send a response as ' |
| 'appropriate for your application') |
| STUB_READER_WRITER_TODO = ( |
| '// TODO: Set the client stream callback and send responses as ' |
| 'appropriate for your application') |
| |
| |
| def get_id(item: Union[ProtoService, ProtoServiceMethod]) -> str: |
| name = item.proto_path() if isinstance(item, ProtoService) else item.name() |
| return f'0x{ids.calculate(name):08x}' |
| |
| |
| def client_call_type(method: ProtoServiceMethod, prefix: str) -> str: |
| """Returns Client ReaderWriter/Reader/Writer/Recevier for the call.""" |
| if method.type() is ProtoServiceMethod.Type.UNARY: |
| call_class = 'UnaryReceiver' |
| elif method.type() is ProtoServiceMethod.Type.SERVER_STREAMING: |
| call_class = 'ClientReader' |
| elif method.type() is ProtoServiceMethod.Type.CLIENT_STREAMING: |
| call_class = 'ClientWriter' |
| elif method.type() is ProtoServiceMethod.Type.BIDIRECTIONAL_STREAMING: |
| call_class = 'ClientReaderWriter' |
| else: |
| raise NotImplementedError(f'Unknown {method.type()}') |
| |
| return f'{RPC_NAMESPACE}::{prefix}{call_class}' |
| |
| |
| class CodeGenerator(abc.ABC): |
| """Generates RPC code for services and clients.""" |
| def __init__(self, output_filename: str) -> None: |
| self.output = OutputFile(output_filename) |
| |
| def indent(self, amount: int = OutputFile.INDENT_WIDTH) -> Any: |
| """Indents the output. Use in a with block.""" |
| return self.output.indent(amount) |
| |
| def line(self, value: str = '') -> None: |
| """Writes a line to the output.""" |
| self.output.write_line(value) |
| |
| def indented_list(self, *args: str, end: str = ',') -> None: |
| """Outputs each arg one per line; adds end to teh last arg.""" |
| with self.indent(4): |
| for arg in args[:-1]: |
| self.line(arg + ',') |
| |
| self.line(args[-1] + end) |
| |
| @abc.abstractmethod |
| def name(self) -> str: |
| """Name of the pw_rpc implementation.""" |
| |
| @abc.abstractmethod |
| def method_union_name(self) -> str: |
| """Name of the MethodUnion class to use.""" |
| |
| @abc.abstractmethod |
| def includes(self, proto_file_name: str) -> Iterable[str]: |
| """Yields #include lines.""" |
| |
| @abc.abstractmethod |
| def service_aliases(self) -> None: |
| """Generates reader/writer aliases.""" |
| |
| @abc.abstractmethod |
| def method_descriptor(self, method: ProtoServiceMethod) -> None: |
| """Generates code for a service method.""" |
| |
| @abc.abstractmethod |
| def client_member_function(self, method: ProtoServiceMethod) -> None: |
| """Generates the client code for the Client member functions.""" |
| |
| @abc.abstractmethod |
| def client_static_function(self, method: ProtoServiceMethod) -> None: |
| """Generates method static functions that instantiate a Client.""" |
| |
| def method_info_specialization(self, method: ProtoServiceMethod) -> None: |
| """Generates impl-specific additions to the MethodInfo specialization. |
| |
| May be empty if the generator has nothing to add to the MethodInfo. |
| """ |
| |
| def private_additions(self, service: ProtoService) -> None: |
| """Additions to the private section of the outer generated class.""" |
| |
| |
| def generate_package(file_descriptor_proto, proto_package: ProtoNode, |
| gen: CodeGenerator) -> None: |
| """Generates service and client code for a package.""" |
| assert proto_package.type() == ProtoNode.Type.PACKAGE |
| |
| gen.line(f'// {os.path.basename(gen.output.name())} automatically ' |
| f'generated by {PLUGIN_NAME} {PLUGIN_VERSION}') |
| gen.line(f'// on {datetime.now().isoformat()}') |
| gen.line('// clang-format off') |
| gen.line('#pragma once\n') |
| |
| gen.line('#include <array>') |
| gen.line('#include <cstdint>') |
| gen.line('#include <type_traits>\n') |
| |
| include_lines = [ |
| '#include "pw_rpc/internal/method_info.h"', |
| '#include "pw_rpc/internal/method_lookup.h"', |
| '#include "pw_rpc/internal/service_client.h"', |
| '#include "pw_rpc/method_type.h"', |
| '#include "pw_rpc/service.h"', |
| '#include "pw_rpc/service_id.h"', |
| ] |
| include_lines += gen.includes(file_descriptor_proto.name) |
| |
| for include_line in sorted(include_lines): |
| gen.line(include_line) |
| |
| gen.line() |
| |
| if proto_package.cpp_namespace(): |
| file_namespace = proto_package.cpp_namespace() |
| if file_namespace.startswith('::'): |
| file_namespace = file_namespace[2:] |
| |
| gen.line(f'namespace {file_namespace} {{') |
| else: |
| file_namespace = '' |
| |
| gen.line(f'namespace pw_rpc::{gen.name()} {{') |
| gen.line() |
| |
| services = [ |
| cast(ProtoService, node) for node in proto_package |
| if node.type() == ProtoNode.Type.SERVICE |
| ] |
| |
| for service in services: |
| _generate_service_and_client(gen, service) |
| |
| gen.line() |
| gen.line(f'}} // namespace pw_rpc::{gen.name()}\n') |
| |
| if file_namespace: |
| gen.line('} // namespace ' + file_namespace) |
| |
| gen.line() |
| gen.line('// Specialize MethodInfo for each RPC to provide metadata at ' |
| 'compile time.') |
| for service in services: |
| _generate_info(gen, file_namespace, service) |
| |
| |
| def _generate_service_and_client(gen: CodeGenerator, |
| service: ProtoService) -> None: |
| gen.line('// Wrapper class that namespaces server and client code for ' |
| 'this RPC service.') |
| gen.line(f'class {service.name()} final {{') |
| gen.line(' public:') |
| |
| with gen.indent(): |
| gen.line(f'{service.name()}() = delete;') |
| gen.line() |
| |
| gen.line('static constexpr ::pw::rpc::ServiceId service_id() {') |
| with gen.indent(): |
| gen.line('return ::pw::rpc::internal::WrapServiceId(kServiceId);') |
| gen.line('}') |
| gen.line() |
| |
| _generate_service(gen, service) |
| |
| gen.line() |
| |
| _generate_client(gen, service) |
| |
| gen.line(' private:') |
| |
| with gen.indent(): |
| gen.line(f'// Hash of "{service.proto_path()}".') |
| gen.line(f'static constexpr uint32_t kServiceId = {get_id(service)};') |
| |
| gen.line('};') |
| |
| |
| def _check_method_name(method: ProtoServiceMethod) -> None: |
| # Methods with the same name as their enclosing service will fail |
| # to compile because the generated method will be indistinguishable |
| # from a constructor. |
| if method.name() == method.service().name(): |
| raise ValueError( |
| f'Attempted to compile `pw_rpc` for proto with method ' |
| f'`{method.name()}` inside a service of the same name. ' |
| '`pw_rpc` does not yet support methods with the same name as their ' |
| 'enclosing service.') |
| if method.name() in ('Service', 'ServiceInfo', 'Client'): |
| raise ValueError( |
| f'"{method.service().proto_path()}.{method.name()}" is not a ' |
| f'valid method name! The name "{method.name()}" is reserved ' |
| 'for internal use by pw_rpc.') |
| |
| |
| def _generate_client(gen: CodeGenerator, service: ProtoService) -> None: |
| gen.line('// The Client is used to invoke RPCs for this service.') |
| gen.line(f'class Client final : public {RPC_NAMESPACE}::internal::' |
| 'ServiceClient {') |
| gen.line(' public:') |
| |
| with gen.indent(): |
| gen.line(f'constexpr Client({RPC_NAMESPACE}::Client& client,' |
| ' uint32_t channel_id)') |
| gen.line(' : ServiceClient(client, channel_id) {}') |
| gen.line() |
| gen.line(f'using ServiceInfo = {service.name()};') |
| |
| for method in service.methods(): |
| gen.line() |
| gen.client_member_function(method) |
| |
| gen.line('};') |
| gen.line() |
| |
| gen.line('// Static functions for invoking RPCs on a pw_rpc server. ' |
| 'These functions are ') |
| gen.line('// equivalent to instantiating a Client and calling the ' |
| 'corresponding RPC.') |
| for method in service.methods(): |
| _check_method_name(method) |
| gen.client_static_function(method) |
| gen.line() |
| |
| |
| def _generate_info(gen: CodeGenerator, namespace: str, |
| service: ProtoService) -> None: |
| """Generates MethodInfo for each method.""" |
| service_id = get_id(service) |
| info = f'struct {RPC_NAMESPACE.lstrip(":")}::internal::MethodInfo' |
| |
| for method in service.methods(): |
| gen.line('template <>') |
| gen.line(f'{info}<{namespace}::pw_rpc::{gen.name()}::' |
| f'{service.name()}::{method.name()}> {{') |
| |
| with gen.indent(): |
| gen.line(f'static constexpr uint32_t kServiceId = {service_id};') |
| gen.line(f'static constexpr uint32_t kMethodId = ' |
| f'{get_id(method)};') |
| gen.line(f'static constexpr {RPC_NAMESPACE}::MethodType kType = ' |
| f'{method.type().cc_enum()};') |
| gen.line() |
| |
| gen.line('template <typename ServiceImpl>') |
| gen.line('static constexpr auto Function() {') |
| |
| with gen.indent(): |
| gen.line(f'return &ServiceImpl::{method.name()};') |
| |
| gen.line('}') |
| |
| gen.line('using GeneratedClient = ' |
| f'{"::" + namespace if namespace else ""}' |
| f'::pw_rpc::{gen.name()}::{service.name()}::Client;') |
| |
| gen.method_info_specialization(method) |
| |
| gen.line('};') |
| gen.line() |
| |
| |
| def _generate_service(gen: CodeGenerator, service: ProtoService) -> None: |
| """Generates a C++ class for an RPC service.""" |
| |
| base_class = f'{RPC_NAMESPACE}::Service' |
| gen.line('// The RPC service base class.') |
| gen.line( |
| '// Inherit from this to implement an RPC service for a pw_rpc server.' |
| ) |
| gen.line('template <typename Implementation>') |
| gen.line(f'class Service : public {base_class} {{') |
| gen.line(' public:') |
| |
| with gen.indent(): |
| gen.service_aliases() |
| |
| gen.line() |
| gen.line(f'static constexpr const char* name() ' |
| f'{{ return "{service.name()}"; }}') |
| gen.line() |
| gen.line(f'using ServiceInfo = {service.name()};') |
| gen.line() |
| |
| gen.line(' protected:') |
| |
| with gen.indent(): |
| gen.line('constexpr Service() : ' |
| f'{base_class}(kServiceId, kPwRpcMethods) {{}}') |
| |
| gen.line() |
| gen.line(' private:') |
| |
| with gen.indent(): |
| gen.line('friend class ::pw::rpc::internal::MethodLookup;') |
| gen.line() |
| |
| # Generate the method table |
| gen.line('static constexpr std::array<' |
| f'{RPC_NAMESPACE}::internal::{gen.method_union_name()},' |
| f' {len(service.methods())}> kPwRpcMethods = {{') |
| |
| with gen.indent(4): |
| for method in service.methods(): |
| gen.method_descriptor(method) |
| |
| gen.line('};\n') |
| |
| # Generate the method lookup table |
| _method_lookup_table(gen, service) |
| |
| gen.line('};') |
| |
| |
| def _method_lookup_table(gen: CodeGenerator, service: ProtoService) -> None: |
| """Generates array of method IDs for looking up methods at compile time.""" |
| gen.line('static constexpr std::array<uint32_t, ' |
| f'{len(service.methods())}> kPwRpcMethodIds = {{') |
| |
| with gen.indent(4): |
| for method in service.methods(): |
| gen.line(f'{get_id(method)}, // Hash of "{method.name()}"') |
| |
| gen.line('};') |
| |
| |
| class StubGenerator(abc.ABC): |
| """Generates stub method implementations that can be copied-and-pasted.""" |
| @abc.abstractmethod |
| def unary_signature(self, method: ProtoServiceMethod, prefix: str) -> str: |
| """Returns the signature of this unary method.""" |
| |
| @abc.abstractmethod |
| def unary_stub(self, method: ProtoServiceMethod, |
| output: OutputFile) -> None: |
| """Returns the stub for this unary method.""" |
| |
| @abc.abstractmethod |
| def server_streaming_signature(self, method: ProtoServiceMethod, |
| prefix: str) -> str: |
| """Returns the signature of this server streaming method.""" |
| |
| def server_streaming_stub( # pylint: disable=no-self-use |
| self, unused_method: ProtoServiceMethod, |
| output: OutputFile) -> None: |
| """Returns the stub for this server streaming method.""" |
| output.write_line(STUB_REQUEST_TODO) |
| output.write_line('static_cast<void>(request);') |
| output.write_line(STUB_WRITER_TODO) |
| output.write_line('static_cast<void>(writer);') |
| |
| @abc.abstractmethod |
| def client_streaming_signature(self, method: ProtoServiceMethod, |
| prefix: str) -> str: |
| """Returns the signature of this client streaming method.""" |
| |
| def client_streaming_stub( # pylint: disable=no-self-use |
| self, unused_method: ProtoServiceMethod, |
| output: OutputFile) -> None: |
| """Returns the stub for this client streaming method.""" |
| output.write_line(STUB_READER_TODO) |
| output.write_line('static_cast<void>(reader);') |
| |
| @abc.abstractmethod |
| def bidirectional_streaming_signature(self, method: ProtoServiceMethod, |
| prefix: str) -> str: |
| """Returns the signature of this bidirectional streaming method.""" |
| |
| def bidirectional_streaming_stub( # pylint: disable=no-self-use |
| self, unused_method: ProtoServiceMethod, |
| output: OutputFile) -> None: |
| """Returns the stub for this bidirectional streaming method.""" |
| output.write_line(STUB_READER_WRITER_TODO) |
| output.write_line('static_cast<void>(reader_writer);') |
| |
| |
| def _select_stub_methods(gen: StubGenerator, method: ProtoServiceMethod): |
| if method.type() is ProtoServiceMethod.Type.UNARY: |
| return gen.unary_signature, gen.unary_stub |
| |
| if method.type() is ProtoServiceMethod.Type.SERVER_STREAMING: |
| return gen.server_streaming_signature, gen.server_streaming_stub |
| |
| if method.type() is ProtoServiceMethod.Type.CLIENT_STREAMING: |
| return gen.client_streaming_signature, gen.client_streaming_stub |
| |
| if method.type() is ProtoServiceMethod.Type.BIDIRECTIONAL_STREAMING: |
| return (gen.bidirectional_streaming_signature, |
| gen.bidirectional_streaming_stub) |
| |
| raise NotImplementedError(f'Unrecognized method type {method.type()}') |
| |
| |
| _STUBS_COMMENT = r''' |
| /* |
| ____ __ __ __ _ |
| / _/___ ___ ____ / /__ ____ ___ ___ ____ / /_____ _/ /_(_)___ ____ |
| / // __ `__ \/ __ \/ / _ \/ __ `__ \/ _ \/ __ \/ __/ __ `/ __/ / __ \/ __ \ |
| _/ // / / / / / /_/ / / __/ / / / / / __/ / / / /_/ /_/ / /_/ / /_/ / / / / |
| /___/_/ /_/ /_/ .___/_/\___/_/ /_/ /_/\___/_/ /_/\__/\__,_/\__/_/\____/_/ /_/ |
| /_/ |
| _____ __ __ __ |
| / ___// /___ __/ /_ _____/ / |
| \__ \/ __/ / / / __ \/ ___/ / |
| ___/ / /_/ /_/ / /_/ (__ )_/ |
| /____/\__/\__,_/_.___/____(_) |
| |
| */ |
| // This section provides stub implementations of the RPC services in this file. |
| // The code below may be referenced or copied to serve as a starting point for |
| // your RPC service implementations. |
| ''' |
| |
| |
| def package_stubs(proto_package: ProtoNode, gen: CodeGenerator, |
| stub_generator: StubGenerator) -> None: |
| """Generates the RPC stubs for a package.""" |
| if proto_package.cpp_namespace(): |
| file_ns = proto_package.cpp_namespace() |
| if file_ns.startswith('::'): |
| file_ns = file_ns[2:] |
| |
| start_ns = lambda: gen.line(f'namespace {file_ns} {{\n') |
| finish_ns = lambda: gen.line(f'}} // namespace {file_ns}\n') |
| else: |
| start_ns = finish_ns = lambda: None |
| |
| services = [ |
| cast(ProtoService, node) for node in proto_package |
| if node.type() == ProtoNode.Type.SERVICE |
| ] |
| |
| gen.line('#ifdef _PW_RPC_COMPILE_GENERATED_SERVICE_STUBS') |
| gen.line(_STUBS_COMMENT) |
| |
| gen.line(f'#include "{gen.output.name()}"\n') |
| |
| start_ns() |
| |
| for node in services: |
| _service_declaration_stub(node, gen, stub_generator) |
| |
| gen.line() |
| |
| finish_ns() |
| |
| start_ns() |
| |
| for node in services: |
| _service_definition_stub(node, gen, stub_generator) |
| gen.line() |
| |
| finish_ns() |
| |
| gen.line('#endif // _PW_RPC_COMPILE_GENERATED_SERVICE_STUBS') |
| |
| |
| def _service_declaration_stub(service: ProtoService, gen: CodeGenerator, |
| stub_generator: StubGenerator) -> None: |
| gen.line(f'// Implementation class for {service.proto_path()}.') |
| gen.line(f'class {service.name()} : public pw_rpc::{gen.name()}::' |
| f'{service.name()}::Service<{service.name()}> {{') |
| |
| gen.line(' public:') |
| |
| with gen.indent(): |
| blank_line = False |
| |
| for method in service.methods(): |
| if blank_line: |
| gen.line() |
| else: |
| blank_line = True |
| |
| signature, _ = _select_stub_methods(stub_generator, method) |
| |
| gen.line(signature(method, '') + ';') |
| |
| gen.line('};\n') |
| |
| |
| def _service_definition_stub(service: ProtoService, gen: CodeGenerator, |
| stub_generator: StubGenerator) -> None: |
| gen.line(f'// Method definitions for {service.proto_path()}.') |
| |
| blank_line = False |
| |
| for method in service.methods(): |
| if blank_line: |
| gen.line() |
| else: |
| blank_line = True |
| |
| signature, stub = _select_stub_methods(stub_generator, method) |
| |
| gen.line(signature(method, f'{service.name()}::') + ' {') |
| with gen.indent(): |
| stub(method, gen.output) |
| gen.line('}') |