def unary_stub(self, method: ProtoServiceMethod, output: OutputFile) -> None: output.write_line(codegen.STUB_REQUEST_TODO) output.write_line('static_cast<void>(request);') output.write_line(codegen.STUB_RESPONSE_TODO) output.write_line('static_cast<void>(response);') output.write_line('return pw::Status::Unimplemented();')
def _generate_method_descriptor(method: ProtoServiceMethod, method_id: int, output: OutputFile) -> None: """Generates a method descriptor for a raw RPC method.""" impl_method = f'&Implementation::{method.name()}' output.write_line( f'{RPC_NAMESPACE}::internal::GetRawMethodFor<{impl_method}, ' f'{method.type().cc_enum()}>(') output.write_line(f' 0x{method_id:08x}), // Hash of "{method.name()}"')
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) output_file.write_line() codegen.package_stubs(package_root, output_file, StubGenerator()) return [output_file]
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);')
def _generate_method_descriptor(method: ProtoServiceMethod, method_id: int, output: OutputFile) -> None: """Generates a nanopb method descriptor for an RPC method.""" req_fields = f'{method.request_type().nanopb_name()}_fields' res_fields = f'{method.response_type().nanopb_name()}_fields' impl_method = f'&Implementation::{method.name()}' output.write_line( f'{RPC_NAMESPACE}::internal::GetNanopbOrRawMethodFor<{impl_method}, ' f'{method.type().cc_enum()}>(') with output.indent(4): output.write_line(f'0x{method_id:08x}, // Hash of "{method.name()}"') output.write_line(f'{req_fields},') output.write_line(f'{res_fields}),')
def generate_code_for_enum(enum: ProtoEnum, 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 _generate_service_stubs(service: ProtoService, output: OutputFile, stub_generator: StubGenerator) -> None: output.write_line(f'// Method definitions for {service.proto_path()}.') blank_line = False for method in service.methods(): if blank_line: output.write_line() else: blank_line = True signature, stub = _select_stub_methods(stub_generator, method) output.write_line(signature(method, f'{service.name()}::') + ' {') with output.indent(): stub(method, output) output.write_line('}')
def _method_lookup_table(service: ProtoService, output: OutputFile) -> None: """Generates array of method IDs for looking up methods at compile time.""" output.write_line('static constexpr std::array<uint32_t, ' f'{len(service.methods())}> kMethodIds = {{') with output.indent(4): for method in service.methods(): method_id = pw_rpc.ids.calculate(method.name()) output.write_line( f'0x{method_id:08x}, // Hash of "{method.name()}"') output.write_line('};\n')
def process_proto_file(proto_file) -> Iterable[OutputFile]: """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. _, 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]
def define_not_in_class_methods(message: ProtoMessage, 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_message(message: ProtoMessage, 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 _server_streaming_stub(method: ProtoServiceMethod, output: OutputFile) -> None: output.write_line(f'void {method.name()}(ServerContext&, ' 'pw::ConstByteSpan request, RawServerWriter& writer) {') with output.indent(): output.write_line(codegen.STUB_REQUEST_TODO) output.write_line('static_cast<void>(request);') output.write_line(codegen.STUB_WRITER_TODO) output.write_line('static_cast<void>(writer);') output.write_line('}')
def _unary_stub(method: ProtoServiceMethod, output: OutputFile) -> None: output.write_line(f'pw::StatusWithSize {method.name()}(ServerContext&, ' 'pw::ConstByteSpan request, pw::ByteSpan response) {') with output.indent(): output.write_line(codegen.STUB_REQUEST_TODO) output.write_line('static_cast<void>(request);') output.write_line(codegen.STUB_RESPONSE_TODO) output.write_line('static_cast<void>(response);') output.write_line('return pw::StatusWithSize::Unimplemented();') output.write_line('}')
def _server_streaming_stub(method: ProtoServiceMethod, output: OutputFile) -> None: output.write_line( f'void {method.name()}(ServerContext&, ' f'const {method.request_type().nanopb_name()}& request, ' f'ServerWriter<{method.response_type().nanopb_name()}>& writer) {{') with output.indent(): output.write_line(codegen.STUB_REQUEST_TODO) output.write_line('static_cast<void>(request);') output.write_line(codegen.STUB_WRITER_TODO) output.write_line('static_cast<void>(writer);') output.write_line('}')
def package_stubs(proto_package: ProtoNode, output: OutputFile, unary_stub: StubFunction, server_streaming_stub: StubFunction) -> None: output.write_line('#ifdef _PW_RPC_COMPILE_GENERATED_SERVICE_STUBS') output.write_line(_STUBS_COMMENT) output.write_line(f'#include "{output.name()}"\n') if proto_package.cpp_namespace(): file_namespace = proto_package.cpp_namespace() if file_namespace.startswith('::'): file_namespace = file_namespace[2:] output.write_line(f'namespace {file_namespace} {{') for node in proto_package: if node.type() == ProtoNode.Type.SERVICE: _generate_service_stub(cast(ProtoService, node), output, unary_stub, server_streaming_stub) if proto_package.cpp_namespace(): output.write_line(f'}} // namespace {file_namespace}') output.write_line('\n#endif // _PW_RPC_COMPILE_GENERATED_SERVICE_STUBS')
def _generate_code_for_client_method(method: ProtoServiceMethod, output: OutputFile) -> None: """Outputs client code for a single RPC method.""" req = method.request_type().nanopb_name() res = method.response_type().nanopb_name() method_id = pw_rpc.ids.calculate(method.name()) if method.type() == ProtoServiceMethod.Type.UNARY: callback = f'{RPC_NAMESPACE}::UnaryResponseHandler<{res}>' elif method.type() == ProtoServiceMethod.Type.SERVER_STREAMING: callback = f'{RPC_NAMESPACE}::ServerStreamingResponseHandler<{res}>' else: raise NotImplementedError( 'Only unary and server streaming RPCs are currently supported') output.write_line() output.write_line(f'static NanopbClientCall<\n {callback}>') output.write_line(f'{method.name()}({RPC_NAMESPACE}::Channel& channel,') with output.indent(len(method.name()) + 1): output.write_line(f'const {req}& request,') output.write_line(f'{callback}& callback) {{') with output.indent(): output.write_line(f'NanopbClientCall<{callback}>') output.write_line(' call(&channel,') with output.indent(9): output.write_line('kServiceId,') output.write_line( f'0x{method_id:08x}, // Hash of "{method.name()}"') output.write_line('callback,') output.write_line(f'{req}_fields,') output.write_line(f'{res}_fields);') output.write_line('call.SendRequest(&request);') output.write_line('return call;') output.write_line('}')
def _generate_server_writer_alias(output: OutputFile) -> None: output.write_line('template <typename T>') output.write_line('using ServerWriter = ::pw::rpc::ServerWriter<T>;')
def _generate_service_class(service: ProtoService, output: OutputFile, stub_generator: StubGenerator) -> None: output.write_line(f'// Implementation class for {service.proto_path()}.') output.write_line( f'class {service.name()} ' f': public generated::{service.name()}<{service.name()}> {{') output.write_line(' public:') with output.indent(): blank_line = False for method in service.methods(): if blank_line: output.write_line() else: blank_line = True signature, _ = _select_stub_methods(stub_generator, method) output.write_line(signature(method, '') + ';') output.write_line('};\n')
def _generate_code_for_client(service: ProtoService, root: ProtoNode, output: OutputFile) -> None: """Outputs client code for an RPC service.""" output.write_line('namespace nanopb {') class_name = f'{service.cpp_namespace(root)}Client' output.write_line(f'\nclass {class_name} {{') output.write_line(' public:') with output.indent(): output.write_line('template <typename T>') output.write_line( f'using NanopbClientCall = {RPC_NAMESPACE}::NanopbClientCall<T>;') output.write_line('') output.write_line(f'{class_name}() = delete;') for method in service.methods(): _generate_code_for_client_method(method, output) service_name_hash = pw_rpc.ids.calculate(service.proto_path()) output.write_line('\n private:') with output.indent(): output.write_line(f'// Hash of "{service.proto_path()}".') output.write_line( f'static constexpr uint32_t kServiceId = 0x{service_name_hash:08x};' ) output.write_line('};') output.write_line('\n} // namespace nanopb\n')
def forward_declare(node: ProtoMessage, root: ProtoNode, output: OutputFile) -> None: """Generates code forward-declaring entities in a message's namespace.""" 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(cast(ProtoEnum, child), node, output) output.write_line(f'}} // namespace {namespace}')
def generate_code_for_package(file_descriptor_proto, 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(f'// on {datetime.now()}') output.write_line('#pragma once\n') output.write_line('#include <cstddef>') output.write_line('#include <cstdint>') output.write_line('#include <span>\n') output.write_line('#include "pw_protobuf/codegen.h"') for imported_file in file_descriptor_proto.dependency: generated_header = _proto_filename_to_generated_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.MESSAGE: forward_declare(cast(ProtoMessage, 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(cast(ProtoEnum, node), package, output) # Run through all messages in the file, generating a class for each. for node in package: if node.type() == ProtoNode.Type.MESSAGE: output.write_line() generate_code_for_message(cast(ProtoMessage, node), package, output) # Run a second pass through the classes, this time defining all of the # methods which were previously only declared. for node in package: if node.type() == ProtoNode.Type.MESSAGE: define_not_in_class_methods(cast(ProtoMessage, node), package, output) if package.cpp_namespace(): output.write_line(f'\n}} // namespace {package.cpp_namespace()}')
def package(file_descriptor_proto, proto_package: ProtoNode, output: OutputFile, includes: IncludesGenerator, service: ServiceGenerator, client: ServiceGenerator) -> None: """Generates service and client code for a package.""" assert proto_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().isoformat()}') output.write_line('// clang-format off') output.write_line('#pragma once\n') output.write_line('#include <array>') output.write_line('#include <cstdint>') output.write_line('#include <type_traits>\n') include_lines = [ '#include "pw_rpc/internal/method_lookup.h"', '#include "pw_rpc/server_context.h"', '#include "pw_rpc/service.h"', ] include_lines += includes(file_descriptor_proto, proto_package) for include_line in sorted(include_lines): output.write_line(include_line) output.write_line() if proto_package.cpp_namespace(): file_namespace = proto_package.cpp_namespace() if file_namespace.startswith('::'): file_namespace = file_namespace[2:] output.write_line(f'namespace {file_namespace} {{') for node in proto_package: if node.type() == ProtoNode.Type.SERVICE: service(cast(ProtoService, node), proto_package, output) client(cast(ProtoService, node), proto_package, output) if proto_package.cpp_namespace(): output.write_line(f'}} // namespace {file_namespace}')
def package_stubs(proto_package: ProtoNode, output: OutputFile, 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: output.write_line(f'namespace {file_ns} {{\n') finish_ns = lambda: output.write_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 ] output.write_line('#ifdef _PW_RPC_COMPILE_GENERATED_SERVICE_STUBS') output.write_line(_STUBS_COMMENT) output.write_line(f'#include "{output.name()}"\n') start_ns() for node in services: _generate_service_class(node, output, stub_generator) output.write_line() finish_ns() start_ns() for node in services: _generate_service_stubs(node, output, stub_generator) output.write_line() finish_ns() output.write_line('#endif // _PW_RPC_COMPILE_GENERATED_SERVICE_STUBS')
def _unary_stub(method: ProtoServiceMethod, output: OutputFile) -> None: output.write_line(f'pw::Status {method.name()}(ServerContext&, ' f'const {method.request_type().nanopb_name()}& request, ' f'{method.response_type().nanopb_name()}& response) {{') with output.indent(): output.write_line(codegen.STUB_REQUEST_TODO) output.write_line('static_cast<void>(request);') output.write_line(codegen.STUB_RESPONSE_TODO) output.write_line('static_cast<void>(response);') output.write_line('return pw::Status::Unimplemented();') output.write_line('}')
def _generate_service_stub(service: ProtoService, output: OutputFile, unary_stub: StubFunction, server_streaming_stub: StubFunction) -> None: output.write_line() output.write_line( f'class {service.name()} ' f': public generated::{service.name()}<{service.name()}> {{') output.write_line(' public:') with output.indent(): blank_line = False for method in service.methods(): if blank_line: output.write_line() else: blank_line = True if method.type() is ProtoServiceMethod.Type.UNARY: unary_stub(method, output) elif method.type() is ProtoServiceMethod.Type.SERVER_STREAMING: server_streaming_stub(method, output) else: raise NotImplementedError( 'Client and bidirectional streaming not yet implemented') output.write_line('};\n')
def _generate_server_writer_alias(output: OutputFile) -> None: output.write_line( f'using RawServerWriter = {RPC_NAMESPACE}::RawServerWriter;')
def service_class(service: ProtoService, root: ProtoNode, output: OutputFile, server_writer_alias: ServerWriterGenerator, method_union: str, method_descriptor: MethodGenerator) -> None: """Generates a C++ derived class for a nanopb RPC service.""" output.write_line('namespace generated {') base_class = f'{RPC_NAMESPACE}::Service' output.write_line('\ntemplate <typename Implementation>') output.write_line( f'class {service.cpp_namespace(root)} : public {base_class} {{') output.write_line(' public:') with output.indent(): output.write_line( f'using ServerContext = {RPC_NAMESPACE}::ServerContext;') server_writer_alias(output) output.write_line() output.write_line(f'constexpr {service.name()}()') output.write_line(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;') output.write_line() output.write_line(f'static constexpr const char* name() ' f'{{ return "{service.name()}"; }}') output.write_line() output.write_line( '// Used by MethodLookup to identify the generated service base.') output.write_line( 'constexpr void _PwRpcInternalGeneratedBase() const {}') service_name_hash = pw_rpc.ids.calculate(service.proto_path()) output.write_line('\n private:') with output.indent(): output.write_line('friend class ::pw::rpc::internal::MethodLookup;\n') output.write_line(f'// Hash of "{service.proto_path()}".') output.write_line( f'static constexpr uint32_t kServiceId = 0x{service_name_hash:08x};' ) output.write_line() # Generate the method table output.write_line('static constexpr std::array<' f'{RPC_NAMESPACE}::internal::{method_union},' f' {len(service.methods())}> kMethods = {{') with output.indent(4): for method in service.methods(): method_descriptor(method, pw_rpc.ids.calculate(method.name()), output) output.write_line('};\n') # Generate the method lookup table _method_lookup_table(service, output) output.write_line('};') output.write_line('\n} // namespace generated\n')
def _generate_code_for_client(unused_service: ProtoService, unused_root: ProtoNode, output: OutputFile) -> None: """Outputs client code for an RPC service.""" output.write_line('// Raw RPC clients are not yet implemented.\n')