'''Generate header file for nanopb from a ProtoBuf FileDescriptorSet.'''
-import google.protobuf.descriptor_pb2 as descriptor
-import nanopb_pb2
+try:
+ import google.protobuf.descriptor_pb2 as descriptor
+except:
+ print
+ print "*************************************************************"
+ print "*** Could not import the Google protobuf Python libraries ***"
+ print "*** Try installing package 'python-protobuf' or similar. ***"
+ print "*************************************************************"
+ print
+ raise
+
+try:
+ import nanopb_pb2
+except:
+ print
+ print "***************************************************************"
+ print "*** Could not import the precompiled nanopb_pb2.py. ***"
+ print "*** Run 'make' in the 'generator' folder to update the file.***"
+ print "***************************************************************"
+ print
+ raise
+
import os.path
# Values are tuple (c type, pb ltype)
FieldD = descriptor.FieldDescriptorProto
datatypes = {
FieldD.TYPE_BOOL: ('bool', 'PB_LTYPE_VARINT'),
- FieldD.TYPE_DOUBLE: ('double', 'PB_LTYPE_FIXED'),
- FieldD.TYPE_FIXED32: ('uint32_t', 'PB_LTYPE_FIXED'),
- FieldD.TYPE_FIXED64: ('uint64_t', 'PB_LTYPE_FIXED'),
- FieldD.TYPE_FLOAT: ('float', 'PB_LTYPE_FIXED'),
+ FieldD.TYPE_DOUBLE: ('double', 'PB_LTYPE_FIXED64'),
+ FieldD.TYPE_FIXED32: ('uint32_t', 'PB_LTYPE_FIXED32'),
+ FieldD.TYPE_FIXED64: ('uint64_t', 'PB_LTYPE_FIXED64'),
+ FieldD.TYPE_FLOAT: ('float', 'PB_LTYPE_FIXED32'),
FieldD.TYPE_INT32: ('int32_t', 'PB_LTYPE_VARINT'),
FieldD.TYPE_INT64: ('int64_t', 'PB_LTYPE_VARINT'),
- FieldD.TYPE_SFIXED32: ('int32_t', 'PB_LTYPE_FIXED'),
- FieldD.TYPE_SFIXED64: ('int64_t', 'PB_LTYPE_FIXED'),
+ FieldD.TYPE_SFIXED32: ('int32_t', 'PB_LTYPE_FIXED32'),
+ FieldD.TYPE_SFIXED64: ('int64_t', 'PB_LTYPE_FIXED64'),
FieldD.TYPE_SINT32: ('int32_t', 'PB_LTYPE_SVARINT'),
FieldD.TYPE_SINT64: ('int64_t', 'PB_LTYPE_SVARINT'),
FieldD.TYPE_UINT32: ('uint32_t', 'PB_LTYPE_VARINT'),
elif desc.type == FieldD.TYPE_ENUM:
self.ltype = 'PB_LTYPE_VARINT'
self.ctype = names_from_type_name(desc.type_name)
- self.default = Names(self.ctype) + self.default
+ if self.default is not None:
+ self.default = self.ctype + self.default
elif desc.type == FieldD.TYPE_STRING:
self.ltype = 'PB_LTYPE_STRING'
if self.max_size is None:
if self.max_size is None:
is_callback = True
else:
- self.ctype = 'PB_BYTES_ARRAY(%d)' % self.max_size
+ self.ctype = self.struct_name + self.name + 't'
elif desc.type == FieldD.TYPE_MESSAGE:
self.ltype = 'PB_LTYPE_SUBMESSAGE'
- self.ctype = names_from_type_name(desc.type_name)
+ self.ctype = self.submsgname = names_from_type_name(desc.type_name)
else:
raise NotImplementedError(desc.type)
result += ' %s %s%s;' % (self.ctype, self.name, self.array_decl)
return result
- def default_decl(self):
+ def types(self):
+ '''Return definitions for any special types this field might need.'''
+ if self.ltype == 'PB_LTYPE_BYTES' and self.max_size is not None:
+ result = 'typedef struct {\n'
+ result += ' size_t size;\n'
+ result += ' uint8_t bytes[%d];\n' % self.max_size
+ result += '} %s;\n' % self.ctype
+ else:
+ result = None
+ return result
+
+ def default_decl(self, declaration_only = False):
'''Return definition for this field's default value.'''
if self.default is None:
return None
if self.ltype == 'PB_LTYPE_STRING':
ctype = 'char'
if self.max_size is None:
- array_decl = '[]'
+ return None # Not implemented
else:
- array_decl = '[%d]' % self.max_size
- default = self.default.encode('string_escape')
+ array_decl = '[%d]' % (self.max_size + 1)
+ default = str(self.default).encode('string_escape')
default = default.replace('"', '\\"')
default = '"' + default + '"'
elif self.ltype == 'PB_LTYPE_BYTES':
data = ['0x%02x' % ord(c) for c in data]
if self.max_size is None:
- ctype = 'PB_BYTES_ARRAY(%d)' % len(data)
+ return None # Not implemented
else:
- ctype = 'PB_BYTES_ARRAY(%d)' % self.max_size
+ ctype = self.ctype
default = '{%d, {%s}}' % (len(data), ','.join(data))
array_decl = ''
ctype, default = self.ctype, self.default
array_decl = ''
- return 'const %s %s_default%s = %s;' % (ctype, self.struct_name + self.name, array_decl, default)
+ if declaration_only:
+ return 'extern const %s %s_default%s;' % (ctype, self.struct_name + self.name, array_decl)
+ else:
+ return 'const %s %s_default%s = %s;' % (ctype, self.struct_name + self.name, array_decl, default)
def pb_field_t(self, prev_field_name):
'''Return the pb_field_t initializer to use in the constant array.
if prev_field_name is None:
result += ' offsetof(%s, %s),' % (self.struct_name, self.name)
else:
- result += ' pb_delta(%s, %s, %s),' % (self.struct_name, self.name, prev_field_name)
+ result += ' pb_delta_end(%s, %s, %s),' % (self.struct_name, self.name, prev_field_name)
if self.htype == 'PB_HTYPE_OPTIONAL':
result += '\n pb_delta(%s, has_%s, %s),' % (self.struct_name, self.name, self.name)
result += ' 0,'
if self.ltype == 'PB_LTYPE_SUBMESSAGE':
- result += '\n &%s_fields}' % self.ctype
+ result += '\n &%s_fields}' % self.submsgname
elif self.default is None or self.htype == 'PB_HTYPE_CALLBACK':
result += ' 0}'
else:
self.fields = [Field(self.name, f) for f in desc.field]
self.ordered_fields = self.fields[:]
self.ordered_fields.sort()
-
- def __cmp__(self, other):
- '''Sort messages so that submessages are declared before the message
- that uses them.
- '''
- if self.refers_to(other.name):
- return 1
- elif other.refers_to(self.name):
- return -1
- else:
- return 0
-
- def refers_to(self, name):
- '''Returns True if this message uses the specified type as field type.'''
- for field in self.fields:
- if str(field.ctype) == str(name):
- return True
- return False
+
+ def get_dependencies(self):
+ '''Get list of type names that this structure refers to.'''
+ return [str(field.ctype) for field in self.fields]
def __str__(self):
result = 'typedef struct {\n'
- result += '\n'.join([str(f) for f in self.fields])
+ result += '\n'.join([str(f) for f in self.ordered_fields])
result += '\n} %s;' % self.name
return result
- def default_decl(self):
+ def types(self):
result = ""
for field in self.fields:
- default = field.default_decl()
+ types = field.types()
+ if types is not None:
+ result += types + '\n'
+ return result
+
+ def default_decl(self, declaration_only = False):
+ result = ""
+ for field in self.fields:
+ default = field.default_decl(declaration_only)
if default is not None:
result += default + '\n'
return result
- def pb_field_t(self):
- result = 'const pb_field_t %s_fields[] = {\n' % self.name
+ def fields_declaration(self):
+ result = 'extern const pb_field_t %s_fields[%d];' % (self.name, len(self.fields) + 1)
+ return result
+
+ def fields_definition(self):
+ result = 'const pb_field_t %s_fields[%d] = {\n' % (self.name, len(self.fields) + 1)
prev = None
for field in self.ordered_fields:
result += ',\n\n'
prev = field.name
- result = result[:-3] + '\n};'
+ result += ' PB_LAST_FIELD\n};'
return result
def iterate_messages(desc, names = Names()):
for x in iterate_messages(submsg, sub_names):
yield x
-def process_file(fdesc):
- '''Takes a FileDescriptorProto and generate content for its header file.
- Generates strings, which should be concatenated and stored to file.
- '''
-
- yield '/* Automatically generated nanopb header */\n'
- yield '#include <pb.h>\n\n'
+def parse_file(fdesc):
+ '''Takes a FileDescriptorProto and returns tuple (enum, messages).'''
enums = []
messages = []
+ if fdesc.package:
+ base_name = Names(fdesc.package.split('.'))
+ else:
+ base_name = Names()
+
for enum in fdesc.enum_type:
- enums.append(Enum(Names(), enum))
+ enums.append(Enum(base_name, enum))
- for names, message in iterate_messages(fdesc):
+ for names, message in iterate_messages(fdesc, base_name):
+ messages.append(Message(names, message))
for enum in message.enum_type:
enums.append(Enum(names, enum))
-
- messages.append(Message(names, message))
+
+ return enums, messages
+
+def toposort2(data):
+ '''Topological sort.
+ From http://code.activestate.com/recipes/577413-topological-sort/
+ This function is under the MIT license.
+ '''
+ for k, v in data.items():
+ v.discard(k) # Ignore self dependencies
+ extra_items_in_deps = reduce(set.union, data.values(), set()) - set(data.keys())
+ data.update(dict([(item, set()) for item in extra_items_in_deps]))
+ while True:
+ ordered = set(item for item,dep in data.items() if not dep)
+ if not ordered:
+ break
+ for item in sorted(ordered):
+ yield item
+ data = dict([(item, (dep - ordered)) for item,dep in data.items()
+ if item not in ordered])
+ assert not data, "A cyclic dependency exists amongst %r" % data
+
+def sort_dependencies(messages):
+ '''Sort a list of Messages based on dependencies.'''
+ dependencies = {}
+ message_by_name = {}
+ for message in messages:
+ dependencies[str(message.name)] = set(message.get_dependencies())
+ message_by_name[str(message.name)] = message
+
+ for msgname in toposort2(dependencies):
+ if msgname in message_by_name:
+ yield message_by_name[msgname]
+
+def generate_header(dependencies, headername, enums, messages):
+ '''Generate content for a header file.
+ Generates strings, which should be concatenated and stored to file.
+ '''
+
+ yield '/* Automatically generated nanopb header */\n'
+
+ symbol = headername.replace('.', '_').upper()
+ yield '#ifndef _PB_%s_\n' % symbol
+ yield '#define _PB_%s_\n' % symbol
+ yield '#include <pb.h>\n\n'
+
+ for dependency in dependencies:
+ noext = os.path.splitext(dependency)[0]
+ yield '#include "%s.pb.h"\n' % noext
+ yield '\n'
yield '/* Enum definitions */\n'
for enum in enums:
yield str(enum) + '\n\n'
yield '/* Struct definitions */\n'
- messages.sort()
- for msg in messages:
+ for msg in sort_dependencies(messages):
+ yield msg.types()
yield str(msg) + '\n\n'
yield '/* Default values for struct fields */\n'
for msg in messages:
- yield msg.default_decl()
+ yield msg.default_decl(True)
yield '\n'
yield '/* Struct field encoding specification for nanopb */\n'
for msg in messages:
- yield msg.pb_field_t() + '\n\n'
+ yield msg.fields_declaration() + '\n'
+
+ yield '\n#endif\n'
+
+def generate_source(headername, enums, messages):
+ '''Generate content for a source file.'''
+
+ yield '/* Automatically generated nanopb constant definitions */\n'
+ yield '#include "%s"\n\n' % headername
+
+ for msg in messages:
+ yield msg.default_decl(False)
+
+ yield '\n\n'
+ for msg in messages:
+ yield msg.fields_definition() + '\n\n'
+
if __name__ == '__main__':
import sys
import os.path
print "Usage: " + sys.argv[0] + " file.pb"
print "where file.pb has been compiled from .proto by:"
print "protoc -ofile.pb file.proto"
- print "Output fill be written to file.h"
+ print "Output fill be written to file.pb.h and file.pb.c"
sys.exit(1)
- data = open(sys.argv[1]).read()
+ data = open(sys.argv[1], 'rb').read()
fdesc = descriptor.FileDescriptorSet.FromString(data)
+ enums, messages = parse_file(fdesc.file[0])
- destfile = os.path.splitext(sys.argv[1])[0] + '.h'
+ noext = os.path.splitext(sys.argv[1])[0]
+ headername = noext + '.pb.h'
+ sourcename = noext + '.pb.c'
+ headerbasename = os.path.basename(headername)
- print "Writing to " + destfile
+ print "Writing to " + headername + " and " + sourcename
- destfile = open(destfile, 'w')
+ # 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']
+ 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):
+ header.write(part)
+
+ source = open(sourcename, 'w')
+ for part in generate_source(headerbasename, enums, messages):
+ source.write(part)
+
- for part in process_file(fdesc.file[0]):
- destfile.write(part)