X-Git-Url: https://gerrit.automotivelinux.org/gerrit/gitweb?a=blobdiff_plain;f=generator%2Fnanopb_generator.py;h=e463b6ca8c8e99ddd9a2f7077e83e62d09ea7f5a;hb=840e213b9fa244bce4c67789971c02ce16e66295;hp=663745a151ea1e03c824a17705477a409c3c1e20;hpb=5522e02133be7d9dfd2f350f2774dfc0abc15686;p=apps%2Fagl-service-can-low-level.git diff --git a/generator/nanopb_generator.py b/generator/nanopb_generator.py old mode 100644 new mode 100755 index 663745a1..e463b6ca --- a/generator/nanopb_generator.py +++ b/generator/nanopb_generator.py @@ -1,8 +1,11 @@ +#!/usr/bin/python + '''Generate header file for nanopb from a ProtoBuf FileDescriptorSet.''' -nanopb_version = "nanopb-0.2.1-dev" +nanopb_version = "nanopb-0.2.3-dev" try: import google.protobuf.descriptor_pb2 as descriptor + import google.protobuf.text_format as text_format except: print print "*************************************************************" @@ -54,9 +57,7 @@ datatypes = { } class Names: - '''Keeps a set of nested names and formats them to C identifier. - You can subclass this with your own implementation. - ''' + '''Keeps a set of nested names and formats them to C identifier.''' def __init__(self, parts = ()): if isinstance(parts, Names): parts = parts.parts @@ -187,7 +188,7 @@ class Field: return cmp(self.tag, other.tag) def __str__(self): - if self.rules == 'OPTIONAL': + if self.rules == 'OPTIONAL' and self.allocation == 'STATIC': result = ' bool has_' + self.name + ';\n' elif self.rules == 'REPEATED' and self.allocation == 'STATIC': result = ' size_t ' + self.name + '_count;\n' @@ -236,17 +237,23 @@ class Field: else: return 'const %s %s_default%s = %s;' % (ctype, self.struct_name + self.name, array_decl, default) + def tags(self): + '''Return the #define for the tag number of this field.''' + identifier = '%s_%s_tag' % (self.struct_name, self.name) + return '#define %-40s %d\n' % (identifier, self.tag) + def pb_field_t(self, prev_field_name): '''Return the pb_field_t initializer to use in the constant array. prev_field_name is the name of the previous field or None. ''' - result = ' PB_FIELD(%3d, ' % self.tag + result = ' PB_FIELD2(%3d, ' % self.tag result += '%-8s, ' % self.pbtype result += '%s, ' % self.rules result += '%s, ' % self.allocation result += '%s, ' % self.struct_name result += '%s, ' % self.name result += '%s, ' % (prev_field_name or self.name) + result += '%s, ' % ("first" if not prev_field_name else "other") if self.pbtype == 'MESSAGE': result += '&%s_fields)' % self.submsgname @@ -271,8 +278,72 @@ class Field: return max(self.tag, self.max_size, self.max_count) +class ExtensionRange(Field): + def __init__(self, struct_name, range_start, field_options): + '''Implements a special pb_extension_t* field in an extensible message + structure. The range_start signifies the index at which the extensions + start. Not necessarily all tags above this are extensions, it is merely + a speed optimization. + ''' + self.tag = range_start + self.struct_name = struct_name + self.name = 'extensions' + self.pbtype = 'EXTENSION' + self.rules = 'OPTIONAL' + self.allocation = 'CALLBACK' + self.ctype = 'pb_extension_t' + self.array_decl = '' + self.default = None + self.max_size = 0 + self.max_count = 0 + + def __str__(self): + return ' pb_extension_t *extensions;' + + def types(self): + return None + + def tags(self): + return '' - +class ExtensionField(Field): + def __init__(self, struct_name, desc, field_options): + self.fullname = struct_name + desc.name + self.extendee_name = names_from_type_name(desc.extendee) + Field.__init__(self, self.fullname + 'struct', desc, field_options) + + if self.rules != 'OPTIONAL': + self.skip = True + else: + self.skip = False + self.rules = 'OPTEXT' + + def extension_decl(self): + '''Declaration of the extension type in the .pb.h file''' + if self.skip: + msg = '/* Extension field %s was skipped because only "optional"\n' % self.fullname + msg +=' type of extension fields is currently supported. */\n' + return msg + + return 'extern const pb_extension_type_t %s;\n' % self.fullname + + def extension_def(self): + '''Definition of the extension type in the .pb.c file''' + + if self.skip: + return '' + + result = 'typedef struct {\n' + result += str(self) + result += '\n} %s;\n\n' % self.struct_name + result += ('static const pb_field_t %s_field = \n %s;\n\n' % + (self.fullname, self.pb_field_t(None))) + result += 'const pb_extension_type_t %s = {\n' % self.fullname + result += ' NULL,\n' + result += ' NULL,\n' + result += ' &%s_field\n' % self.fullname + result += '};\n' + return result # --------------------------------------------------------------------------- @@ -286,10 +357,16 @@ class Message: self.fields = [] for f in desc.field: - field_options = get_nanopb_suboptions(f, message_options) + field_options = get_nanopb_suboptions(f, message_options, self.name + f.name) if field_options.type != nanopb_pb2.FT_IGNORE: self.fields.append(Field(self.name, f, field_options)) + if len(desc.extension_range) > 0: + field_options = get_nanopb_suboptions(desc, message_options, self.name + 'extensions') + range_start = min([r.start for r in desc.extension_range]) + if field_options.type != nanopb_pb2.FT_IGNORE: + self.fields.append(ExtensionRange(self.name, range_start, field_options)) + self.packed = message_options.packed_struct self.ordered_fields = self.fields[:] self.ordered_fields.sort() @@ -313,6 +390,11 @@ class Message: result += ' pb_packed' result += ' %s;' % self.name + + if self.packed: + result = 'PB_PACKED_STRUCT_START\n' + result + result += '\nPB_PACKED_STRUCT_END' + return result def types(self): @@ -349,9 +431,6 @@ class Message: - - - # --------------------------------------------------------------------------- # Processing of entire .proto files # --------------------------------------------------------------------------- @@ -371,11 +450,23 @@ def iterate_messages(desc, names = Names()): for x in iterate_messages(submsg, sub_names): yield x +def iterate_extensions(desc, names = Names()): + '''Recursively find all extensions. + For each, yield name, FieldDescriptorProto. + ''' + for extension in desc.extension: + yield names, extension + + for subname, subdesc in iterate_messages(desc, names): + for extension in subdesc.extension: + yield subname, extension + def parse_file(fdesc, file_options): - '''Takes a FileDescriptorProto and returns tuple (enum, messages).''' + '''Takes a FileDescriptorProto and returns tuple (enums, messages, extensions).''' enums = [] messages = [] + extensions = [] if fdesc.package: base_name = Names(fdesc.package.split('.')) @@ -383,16 +474,21 @@ def parse_file(fdesc, file_options): base_name = Names() for enum in fdesc.enum_type: - enum_options = get_nanopb_suboptions(enum, file_options) + enum_options = get_nanopb_suboptions(enum, file_options, base_name + enum.name) enums.append(Enum(base_name, enum, enum_options)) for names, message in iterate_messages(fdesc, base_name): - message_options = get_nanopb_suboptions(message, file_options) + message_options = get_nanopb_suboptions(message, file_options, names) messages.append(Message(names, message, message_options)) for enum in message.enum_type: - enum_options = get_nanopb_suboptions(enum, message_options) + enum_options = get_nanopb_suboptions(enum, message_options, names + enum.name) enums.append(Enum(names, enum, enum_options)) + for names, extension in iterate_extensions(fdesc, base_name): + field_options = get_nanopb_suboptions(extension, file_options, names) + if field_options.type != nanopb_pb2.FT_IGNORE: + extensions.append(ExtensionField(names, extension, field_options)) + # Fix field default values where enum short names are used. for enum in enums: if not enum.options.long_names: @@ -402,7 +498,7 @@ def parse_file(fdesc, file_options): idx = enum.value_longnames.index(field.default) field.default = enum.values[idx][0] - return enums, messages + return enums, messages, extensions def toposort2(data): '''Topological sort. @@ -445,7 +541,7 @@ def make_identifier(headername): result += '_' return result -def generate_header(dependencies, headername, enums, messages, options): +def generate_header(dependencies, headername, enums, messages, extensions, options): '''Generate content for a header file. Generates strings, which should be concatenated and stored to file. ''' @@ -456,7 +552,11 @@ def generate_header(dependencies, headername, enums, messages, options): symbol = make_identifier(headername) yield '#ifndef _PB_%s_\n' % symbol yield '#define _PB_%s_\n' % symbol - yield options.libformat % ('pb.h') + try: + yield options.libformat % ('pb.h') + except TypeError: + # no %s specified - use whatever was passed in as options.libformat + yield options.libformat yield '\n' for dependency in dependencies: @@ -476,12 +576,24 @@ def generate_header(dependencies, headername, enums, messages, options): for msg in sort_dependencies(messages): yield msg.types() yield str(msg) + '\n\n' + + if extensions: + yield '/* Extensions */\n' + for extension in extensions: + yield extension.extension_decl() + yield '\n' yield '/* Default values for struct fields */\n' for msg in messages: yield msg.default_decl(True) yield '\n' + yield '/* Field tags (for use in manual encoding/decoding) */\n' + for msg in sort_dependencies(messages): + for field in msg.fields: + yield field.tags() + yield '\n' + yield '/* Struct field encoding specification for nanopb */\n' for msg in messages: yield msg.fields_declaration() + '\n' @@ -493,7 +605,7 @@ def generate_header(dependencies, headername, enums, messages, options): # End of header yield '\n#endif\n' -def generate_source(headername, enums, messages): +def generate_source(headername, enums, messages, extensions, options): '''Generate content for a source file.''' yield '/* Automatically generated nanopb constant definitions */\n' @@ -508,7 +620,11 @@ def generate_source(headername, enums, messages): for msg in messages: yield msg.fields_definition() + '\n\n' + + for ext in extensions: + yield ext.extension_def() + '\n' + # Add checks for numeric limits if messages: count_required_fields = lambda m: len([f for f in msg.fields if f.rules == 'REQUIRED']) largest_msg = max(messages, key = count_required_fields) @@ -520,7 +636,6 @@ def generate_source(headername, enums, messages): yield ' setting PB_MAX_REQUIRED_FIELDS to %d or more.\n' % largest_count yield '#endif\n' - # Add checks for numeric limits worst = 0 worst_field = '' checks = [] @@ -575,6 +690,67 @@ def generate_source(headername, enums, messages): yield '\n' +# --------------------------------------------------------------------------- +# Options parsing for the .proto files +# --------------------------------------------------------------------------- + +from fnmatch import fnmatch + +def read_options_file(infile): + '''Parse a separate options file to list: + [(namemask, options), ...] + ''' + results = [] + for line in infile: + line = line.strip() + if not line or line.startswith('//') or line.startswith('#'): + continue + + parts = line.split(None, 1) + opts = nanopb_pb2.NanoPBOptions() + text_format.Merge(parts[1], opts) + results.append((parts[0], opts)) + + return results + +class Globals: + '''Ugly global variables, should find a good way to pass these.''' + verbose_options = False + separate_options = [] + +def get_nanopb_suboptions(subdesc, options, name): + '''Get copy of options, and merge information from subdesc.''' + new_options = nanopb_pb2.NanoPBOptions() + new_options.CopyFrom(options) + + # Handle options defined in a separate file + dotname = '.'.join(name.parts) + for namemask, options in Globals.separate_options: + if fnmatch(dotname, namemask): + new_options.MergeFrom(options) + + # Handle options defined in .proto + if isinstance(subdesc.options, descriptor.FieldOptions): + ext_type = nanopb_pb2.nanopb + elif isinstance(subdesc.options, descriptor.FileOptions): + ext_type = nanopb_pb2.nanopb_fileopt + elif isinstance(subdesc.options, descriptor.MessageOptions): + ext_type = nanopb_pb2.nanopb_msgopt + elif isinstance(subdesc.options, descriptor.EnumOptions): + ext_type = nanopb_pb2.nanopb_enumopt + else: + raise Exception("Unknown options type") + + if subdesc.options.HasExtension(ext_type): + ext = subdesc.options.Extensions[ext_type] + new_options.MergeFrom(ext) + + if Globals.verbose_options: + print "Options for " + dotname + ":" + print text_format.MessageToString(new_options) + + return new_options + # --------------------------------------------------------------------------- # Command line interface @@ -583,7 +759,6 @@ def generate_source(headername, enums, messages): import sys import os.path from optparse import OptionParser -import google.protobuf.text_format as text_format optparser = OptionParser( usage = "Usage: nanopb_generator.py [options] file.pb ...", @@ -593,6 +768,8 @@ optparser.add_option("-x", dest="exclude", metavar="FILE", action="append", defa help="Exclude file from generated #include list.") optparser.add_option("-e", "--extension", dest="extension", metavar="EXTENSION", default="pb", help="Set extension to use instead of 'pb' for generated files. [default: %default]") +optparser.add_option("-f", "--options-file", dest="options_file", metavar="FILE", default="%s.options", + help="Set name of a separate generator options file.") optparser.add_option("-Q", "--generated-include-format", dest="genformat", metavar="FORMAT", default='#include "%s"\n', help="Set format string to use for including other .pb.h files. [default: %default]") @@ -606,81 +783,126 @@ optparser.add_option("-v", "--verbose", dest="verbose", action="store_true", def optparser.add_option("-s", dest="settings", metavar="OPTION:VALUE", action="append", default=[], help="Set generator option (max_size, max_count etc.).") -def get_nanopb_suboptions(subdesc, options): - '''Get copy of options, and merge information from subdesc.''' - new_options = nanopb_pb2.NanoPBOptions() - new_options.CopyFrom(options) +def process_file(filename, fdesc, options): + '''Process a single file. + filename: The full path to the .proto or .pb source file, as string. + fdesc: The loaded FileDescriptorSet, or None to read from the input file. + options: Command line options as they come from OptionsParser. + + Returns a dict: + {'headername': Name of header file, + 'headerdata': Data for the .h header file, + 'sourcename': Name of the source code file, + 'sourcedata': Data for the .c source code file + } + ''' + toplevel_options = nanopb_pb2.NanoPBOptions() + for s in options.settings: + text_format.Merge(s, toplevel_options) - if isinstance(subdesc.options, descriptor.FieldOptions): - ext_type = nanopb_pb2.nanopb - elif isinstance(subdesc.options, descriptor.FileOptions): - ext_type = nanopb_pb2.nanopb_fileopt - elif isinstance(subdesc.options, descriptor.MessageOptions): - ext_type = nanopb_pb2.nanopb_msgopt - elif isinstance(subdesc.options, descriptor.EnumOptions): - ext_type = nanopb_pb2.nanopb_enumopt + if not fdesc: + data = open(filename, 'rb').read() + fdesc = descriptor.FileDescriptorSet.FromString(data).file[0] + + # Check if there is a separate .options file + try: + optfilename = options.options_file % os.path.splitext(filename)[0] + except TypeError: + # No %s specified, use the filename as-is + optfilename = options.options_file + + if options.verbose: + print 'Reading options from ' + optfilename + + if os.path.isfile(optfilename): + Globals.separate_options = read_options_file(open(optfilename, "rU")) else: - raise Exception("Unknown options type") + Globals.separate_options = [] - if subdesc.options.HasExtension(ext_type): - ext = subdesc.options.Extensions[ext_type] - new_options.MergeFrom(ext) + # Parse the file + file_options = get_nanopb_suboptions(fdesc, toplevel_options, Names([filename])) + enums, messages, extensions = parse_file(fdesc, file_options) - return new_options + # Decide the file names + noext = os.path.splitext(filename)[0] + headername = noext + '.' + options.extension + '.h' + sourcename = noext + '.' + options.extension + '.c' + headerbasename = os.path.basename(headername) + + # List of .proto files that should not be included in the C header file + # even if they are mentioned in the source .proto. + excludes = ['nanopb.proto', 'google/protobuf/descriptor.proto'] + options.exclude + dependencies = [d for d in fdesc.dependency if d not in excludes] + + headerdata = ''.join(generate_header(dependencies, headerbasename, enums, + messages, extensions, options)) + + sourcedata = ''.join(generate_source(headerbasename, enums, + messages, extensions, options)) -def process(filenames, options): - '''Process the files given on the command line.''' + return {'headername': headername, 'headerdata': headerdata, + 'sourcename': sourcename, 'sourcedata': sourcedata} + +def main_cli(): + '''Main function when invoked directly from the command line.''' + + options, filenames = optparser.parse_args() if not filenames: optparser.print_help() - return False + sys.exit(1) if options.quiet: options.verbose = False - - toplevel_options = nanopb_pb2.NanoPBOptions() - for s in options.settings: - text_format.Merge(s, toplevel_options) + + Globals.verbose_options = options.verbose for filename in filenames: - data = open(filename, 'rb').read() - fdesc = descriptor.FileDescriptorSet.FromString(data) - - file_options = get_nanopb_suboptions(fdesc.file[0], toplevel_options) - - if options.verbose: - print "Options for " + filename + ":" - print text_format.MessageToString(file_options) - - enums, messages = parse_file(fdesc.file[0], file_options) - - noext = os.path.splitext(filename)[0] - headername = noext + '.' + options.extension + '.h' - sourcename = noext + '.' + options.extension + '.c' - headerbasename = os.path.basename(headername) + results = process_file(filename, None, options) if not options.quiet: - print "Writing to " + headername + " and " + sourcename - - # List of .proto files that should not be included in the C header file - # even if they are mentioned in the source .proto. - excludes = ['nanopb.proto', 'google/protobuf/descriptor.proto'] + options.exclude - dependencies = [d for d in fdesc.file[0].dependency if d not in excludes] - - header = open(headername, 'w') - for part in generate_header(dependencies, headerbasename, enums, messages, options): - header.write(part) - - source = open(sourcename, 'w') - for part in generate_source(headerbasename, enums, messages): - source.write(part) + print "Writing to " + results['headername'] + " and " + results['sourcename'] + + open(results['headername'], 'w').write(results['headerdata']) + open(results['sourcename'], 'w').write(results['sourcedata']) - return True +def main_plugin(): + '''Main function when invoked as a protoc plugin.''' -if __name__ == '__main__': - options, filenames = optparser.parse_args() - status = process(filenames, options) + import plugin_pb2 + data = sys.stdin.read() + request = plugin_pb2.CodeGeneratorRequest.FromString(data) - if not status: - sys.exit(1) + import shlex + args = shlex.split(request.parameter) + options, dummy = optparser.parse_args(args) + + # We can't go printing stuff to stdout + Globals.verbose_options = False + options.verbose = False + options.quiet = True + response = plugin_pb2.CodeGeneratorResponse() + + for filename in request.file_to_generate: + for fdesc in request.proto_file: + if fdesc.name == filename: + results = process_file(filename, fdesc, options) + + f = response.file.add() + f.name = results['headername'] + f.content = results['headerdata'] + + f = response.file.add() + f.name = results['sourcename'] + f.content = results['sourcedata'] + + sys.stdout.write(response.SerializeToString()) + +if __name__ == '__main__': + # Check if we are running as a plugin under protoc + if 'protoc-gen-' in sys.argv[0]: + main_plugin() + else: + main_cli() +