| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221 |
- #!/usr/bin/env python
- # Copyright 2022 Google LLC
- #
- # 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
- #
- # http://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.
- """Generates and massages protocol buffer outputs.
- """
- from __future__ import print_function
- import sys
- import io
- import nanopb_generator as nanopb
- import os
- import os.path
- import shlex
- from google.protobuf.descriptor_pb2 import FieldDescriptorProto
- # The plugin_pb2 package loads descriptors on import, but doesn't defend
- # against multiple imports. Reuse the plugin package as loaded by the
- # nanopb_generator.
- plugin_pb2 = nanopb.plugin_pb2
- nanopb_pb2 = nanopb.nanopb_pb2
- def main():
- # Parse request
- data = io.open(sys.stdin.fileno(), 'rb').read()
- request = plugin_pb2.CodeGeneratorRequest.FromString(data)
- # Preprocess inputs, changing types and nanopb defaults
- options = nanopb_parse_options(request)
- use_anonymous_oneof(request)
- use_bytes_for_strings(request)
- # Generate code
- parsed_files = nanopb_parse_files(request, options)
- results = nanopb_generate(request, options, parsed_files)
- response = nanopb_write(results)
- # Write to stdout
- io.open(sys.stdout.fileno(), 'wb').write(response.SerializeToString())
- def use_anonymous_oneof(request):
- """Use anonymous unions for oneofs if they're the only one in a message.
- Equivalent to setting this option on messages where it applies:
- option (nanopb).anonymous_oneof = true;
- Args:
- request: A CodeGeneratorRequest from protoc. The descriptors are modified
- in place.
- """
- for _, message_type in iterate_messages(request):
- if len(message_type.oneof_decl) == 1:
- ext = message_type.options.Extensions[nanopb_pb2.nanopb_msgopt]
- ext.anonymous_oneof = True
- def use_bytes_for_strings(request):
- """Always use the bytes type instead of string.
- By default, nanopb renders proto strings as having the C type char* and does
- not include a separate size field, getting the length of the string via
- strlen(). Unfortunately this prevents using strings with embedded nulls,
- which is something the wire format supports.
- Fortunately, string and bytes proto fields are identical on the wire and
- nanopb's bytes representation does have an explicit length, so this function
- changes the types of all string fields to bytes. The generated code will now
- contain pb_bytes_array_t.
- There's no nanopb or proto option to control this behavior. The equivalent
- would be to hand edit all the .proto files :-(.
- Args:
- request: A CodeGeneratorRequest from protoc. The descriptors are modified
- in place.
- """
- for names, message_type in iterate_messages(request):
- for field in message_type.field:
- if field.type == FieldDescriptorProto.TYPE_STRING:
- field.type = FieldDescriptorProto.TYPE_BYTES
- def iterate_messages(request):
- """Iterates over all messages in all files in the request.
- Args:
- request: A CodeGeneratorRequest passed by protoc.
- Yields:
- names: a nanopb.Names object giving a qualified name for the message
- message_type: a DescriptorProto for the message.
- """
- for fdesc in request.proto_file:
- for names, message_type in nanopb.iterate_messages(fdesc):
- yield names, message_type
- def nanopb_parse_options(request):
- """Parses nanopb_generator command-line options from the given request.
- Args:
- request: A CodeGeneratorRequest passed by protoc.
- Returns:
- Nanopb's options object, obtained via optparser.
- """
- # Parse options the same as nanopb_generator.main_plugin() does.
- args = shlex.split(request.parameter)
- options, _ = nanopb.optparser.parse_args(args)
- # Force certain options
- options.extension = '.nanopb'
- options.verbose = True
- # Replicate options setup from nanopb_generator.main_plugin.
- nanopb.Globals.verbose_options = options.verbose
- # Google's protoc does not currently indicate the full path of proto files.
- # Instead always add the main file path to the search dirs, that works for
- # the common case.
- options.options_path.append(os.path.dirname(request.file_to_generate[0]))
- return options
- def nanopb_parse_files(request, options):
- """Parses the files in the given request into nanopb ProtoFile objects.
- Args:
- request: A CodeGeneratorRequest, as passed by protoc.
- options: The command-line options from nanopb_parse_options.
- Returns:
- A dictionary of filename to nanopb.ProtoFile objects, each one representing
- the parsed form of a FileDescriptor in the request.
- """
- # Process any include files first, in order to have them
- # available as dependencies
- parsed_files = {}
- for fdesc in request.proto_file:
- parsed_files[fdesc.name] = nanopb.parse_file(fdesc.name, fdesc, options)
- return parsed_files
- def nanopb_generate(request, options, parsed_files):
- """Generates C sources from the given parsed files.
- Args:
- request: A CodeGeneratorRequest, as passed by protoc.
- options: The command-line options from nanopb_parse_options.
- parsed_files: A dictionary of filename to nanopb.ProtoFile, as returned by
- nanopb_parse_files().
- Returns:
- A list of nanopb output dictionaries, each one representing the code
- generation result for each file to generate. The output dictionaries have
- the following form:
- {
- 'headername': Name of header file, ending in .h,
- 'headerdata': Contents of the header file,
- 'sourcename': Name of the source code file, ending in .c,
- 'sourcedata': Contents of the source code file
- }
- """
- output = []
- for filename in request.file_to_generate:
- for fdesc in request.proto_file:
- if fdesc.name == filename:
- results = nanopb.process_file(filename, fdesc, options, parsed_files)
- output.append(results)
- return output
- def nanopb_write(results):
- """Translates nanopb output dictionaries to a CodeGeneratorResponse.
- Args:
- results: A list of generated source dictionaries, as returned by
- nanopb_generate().
- Returns:
- A CodeGeneratorResponse describing the result of the code generation
- process to protoc.
- """
- response = plugin_pb2.CodeGeneratorResponse()
- for result in results:
- f = response.file.add()
- f.name = result['headername']
- f.content = result['headerdata']
- f = response.file.add()
- f.name = result['sourcename']
- f.content = result['sourcedata']
- return response
- if __name__ == '__main__':
- main()
|