Home | History | Annotate | Download | only in common
      1 #!/usr/bin/python2
      2 
      3 #
      4 # Copyright (C) 2015 The Android Open Source Project
      5 #
      6 # Licensed under the Apache License, Version 2.0 (the "License");
      7 # you may not use this file except in compliance with the License.
      8 # You may obtain a copy of the License at
      9 #
     10 #      http://www.apache.org/licenses/LICENSE-2.0
     11 #
     12 # Unless required by applicable law or agreed to in writing, software
     13 # distributed under the License is distributed on an "AS IS" BASIS,
     14 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     15 # See the License for the specific language governing permissions and
     16 # limitations under the License.
     17 #
     18 
     19 """A C++ code generator for printing protobufs which use the LITE_RUNTIME.
     20 
     21 Normally printing a protobuf would be done with Message::DebugString(). However,
     22 this is not available when using only MessageLite. This script generates code to
     23 emulate Message::DebugString() without using reflection. The input must be a
     24 valid .proto file.
     25 
     26 Usage: proto_print.py [--subdir=foo] <bar.proto>
     27 
     28 Files named print_bar_proto.h and print_bar_proto.cc will be created in the
     29 current working directory.
     30 """
     31 
     32 from __future__ import print_function
     33 
     34 import argparse
     35 import collections
     36 from datetime import date
     37 import os
     38 import re
     39 import subprocess
     40 
     41 
     42 # Holds information about a protobuf message field.
     43 #
     44 # Attributes:
     45 #   repeated: Whether the field is a repeated field.
     46 #   type_: The type of the field. E.g. int32.
     47 #   name: The name of the field.
     48 Field = collections.namedtuple('Field', 'repeated type_ name')
     49 
     50 
     51 class Message(object):
     52   """Holds information about a protobuf message.
     53 
     54   Attributes:
     55     name: The name of the message.
     56     fields: A list of Field tuples.
     57   """
     58 
     59   def __init__(self, name):
     60     """Initializes a Message instance.
     61 
     62     Args:
     63       name: The protobuf message name.
     64     """
     65     self.name = name
     66     self.fields = []
     67 
     68   def AddField(self, attribute, field_type, field_name):
     69     """Adds a new field to the message.
     70 
     71     Args:
     72       attribute: This should be 'optional', 'required', or 'repeated'.
     73       field_type: The type of the field. E.g. int32.
     74       field_name: The name of the field.
     75     """
     76     self.fields.append(Field(repeated=attribute == 'repeated',
     77                              type_=field_type, name=field_name))
     78 
     79 
     80 class Enum(object):
     81   """Holds information about a protobuf enum.
     82 
     83   Attributes:
     84     name: The name of the enum.
     85     values: A list of enum value names.
     86   """
     87 
     88   def __init__(self, name):
     89     """Initializes a Enum instance.
     90 
     91     Args:
     92       name: The protobuf enum name.
     93     """
     94     self.name = name
     95     self.values = []
     96 
     97   def AddValue(self, value_name):
     98     """Adds a new value to the enum.
     99 
    100     Args:
    101       value_name: The name of the value.
    102     """
    103     self.values.append(value_name)
    104 
    105 
    106 def ParseProto(input_file):
    107   """Parses a proto file and returns a tuple of parsed information.
    108 
    109   Args:
    110     input_file: The proto file to parse.
    111 
    112   Returns:
    113     A tuple in the form (package, imports, messages, enums) where
    114       package: A string holding the proto package.
    115       imports: A list of strings holding proto imports.
    116       messages: A list of Message objects; one for each message in the proto.
    117       enums: A list of Enum objects; one for each enum in the proto.
    118   """
    119   package = ''
    120   imports = []
    121   messages = []
    122   enums = []
    123   current_message_stack = []
    124   current_enum = None
    125   package_re = re.compile(r'package\s+(\w+);')
    126   import_re = re.compile(r'import\s+"([\w]*/)*(\w+).proto";')
    127   message_re = re.compile(r'message\s+(\w+)\s*{')
    128   field_re = re.compile(r'(optional|required|repeated)\s+(\w+)\s+(\w+)\s*=')
    129   enum_re = re.compile(r'enum\s+(\w+)\s*{')
    130   enum_value_re = re.compile(r'(\w+)\s*=')
    131   for line in input_file:
    132     line = line.strip()
    133     if not line or line.startswith('//'):
    134       continue
    135     # Close off the current scope. Enums first because they can't be nested.
    136     if line == '}':
    137       if current_enum:
    138         enums.append(current_enum)
    139         current_enum = None
    140       if current_message_stack:
    141         messages.append(current_message_stack.pop())
    142       continue
    143     # Look for a message definition.
    144     match = message_re.search(line)
    145     if match:
    146       prefix = ''
    147       if current_message_stack:
    148         prefix = '::'.join([m.name for m in current_message_stack]) + '::'
    149       current_message_stack.append(Message(prefix + match.group(1)))
    150       continue
    151     # Look for a message field definition.
    152     if current_message_stack:
    153       match = field_re.search(line)
    154       if match:
    155         current_message_stack[-1].AddField(match.group(1),
    156                                            match.group(2),
    157                                            match.group(3))
    158         continue
    159     # Look for an enum definition.
    160     match = enum_re.search(line)
    161     if match:
    162       current_enum = Enum(match.group(1))
    163       continue
    164     # Look for an enum value.
    165     if current_enum:
    166       match = enum_value_re.search(line)
    167       if match:
    168         current_enum.AddValue(match.group(1))
    169         continue
    170     # Look for a package statement.
    171     match = package_re.search(line)
    172     if match:
    173       package = match.group(1)
    174     # Look for an import statement.
    175     match = import_re.search(line)
    176     if match:
    177       imports.append(match.group(2))
    178   return package, imports, messages, enums
    179 
    180 
    181 def GenerateFileHeaders(proto_name, package, imports, subdir, header_file_name,
    182                         header_file, impl_file):
    183   """Generates and prints file headers.
    184 
    185   Args:
    186     proto_name: The name of the proto file.
    187     package: The protobuf package.
    188     imports: A list of imported protos.
    189     subdir: The --subdir arg.
    190     header_file_name: The header file name.
    191     header_file: The header file handle, open for writing.
    192     impl_file: The implementation file handle, open for writing.
    193   """
    194   if subdir:
    195     guard_name = '%s_%s_PRINT_%s_PROTO_H_' % (package.upper(),
    196                                               subdir.upper(),
    197                                               proto_name.upper())
    198     package_with_subdir = '%s/%s' % (package, subdir)
    199   else:
    200     guard_name = '%s_PRINT_%s_PROTO_H_' % (package.upper(), proto_name.upper())
    201     package_with_subdir = package
    202   includes = '\n'.join(
    203       ['#include "%(package_with_subdir)s/print_%(import)s_proto.h"' % {
    204           'package_with_subdir': package_with_subdir,
    205           'import': current_import} for current_import in imports])
    206   header = """\
    207 // Copyright %(year)s The Chromium OS Authors. All rights reserved.
    208 // Use of this source code is governed by a BSD-style license that can be
    209 // found in the LICENSE file.
    210 
    211 // THIS CODE IS GENERATED.
    212 
    213 #ifndef %(guard_name)s
    214 #define %(guard_name)s
    215 
    216 #include <string>
    217 
    218 #include "%(package_with_subdir)s/%(proto)s.pb.h"
    219 
    220 namespace %(package)s {
    221 """ % {'year': date.today().year,
    222        'guard_name': guard_name,
    223        'package': package,
    224        'proto': proto_name,
    225        'package_with_subdir': package_with_subdir}
    226   impl = """\
    227 // Copyright %(year)s The Chromium OS Authors. All rights reserved.
    228 // Use of this source code is governed by a BSD-style license that can be
    229 // found in the LICENSE file.
    230 
    231 // THIS CODE IS GENERATED.
    232 
    233 #include "%(package_with_subdir)s/%(header_file_name)s"
    234 
    235 #include <string>
    236 
    237 #include <base/strings/string_number_conversions.h>
    238 #include <base/strings/stringprintf.h>
    239 
    240 %(includes)s
    241 
    242 namespace %(package)s {
    243 """ % {'year': date.today().year,
    244        'package': package,
    245        'package_with_subdir': package_with_subdir,
    246        'header_file_name': header_file_name,
    247        'includes': includes}
    248 
    249   header_file.write(header)
    250   impl_file.write(impl)
    251 
    252 
    253 def GenerateFileFooters(proto_name, package, subdir, header_file, impl_file):
    254   """Generates and prints file footers.
    255 
    256   Args:
    257     proto_name: The name of the proto file.
    258     package: The protobuf package.
    259     subdir: The --subdir arg.
    260     header_file: The header file handle, open for writing.
    261     impl_file: The implementation file handle, open for writing.
    262   """
    263   if subdir:
    264     guard_name = '%s_%s_PRINT_%s_PROTO_H_' % (package.upper(),
    265                                               subdir.upper(),
    266                                               proto_name.upper())
    267   else:
    268     guard_name = '%s_PRINT_%s_PROTO_H_' % (package.upper(), proto_name.upper())
    269   header = """
    270 
    271 }  // namespace %(package)s
    272 
    273 #endif  // %(guard_name)s
    274 """ % {'guard_name': guard_name, 'package': package}
    275   impl = """
    276 }  // namespace %(package)s
    277 """ % {'package': package}
    278 
    279   header_file.write(header)
    280   impl_file.write(impl)
    281 
    282 
    283 def GenerateEnumPrinter(enum, header_file, impl_file):
    284   """Generates and prints a function to print an enum value.
    285 
    286   Args:
    287     enum: An Enum instance.
    288     header_file: The header file handle, open for writing.
    289     impl_file: The implementation file handle, open for writing.
    290   """
    291   declare = """
    292 std::string GetProtoDebugStringWithIndent(%(name)s value, int indent_size);
    293 std::string GetProtoDebugString(%(name)s value);""" % {'name': enum.name}
    294   define_begin = """
    295 std::string GetProtoDebugString(%(name)s value) {
    296   return GetProtoDebugStringWithIndent(value, 0);
    297 }
    298 
    299 std::string GetProtoDebugStringWithIndent(%(name)s value, int indent_size) {
    300 """ % {'name': enum.name}
    301   define_end = """
    302   return "<unknown>";
    303 }
    304 """
    305   condition = """
    306   if (value == %(value_name)s) {
    307     return "%(value_name)s";
    308   }"""
    309 
    310   header_file.write(declare)
    311   impl_file.write(define_begin)
    312   for value_name in enum.values:
    313     impl_file.write(condition % {'value_name': value_name})
    314   impl_file.write(define_end)
    315 
    316 
    317 def GenerateMessagePrinter(message, header_file, impl_file):
    318   """Generates and prints a function to print a message.
    319 
    320   Args:
    321     message: A Message instance.
    322     header_file: The header file handle, open for writing.
    323     impl_file: The implementation file handle, open for writing.
    324   """
    325   declare = """
    326 std::string GetProtoDebugStringWithIndent(const %(name)s& value,
    327                                           int indent_size);
    328 std::string GetProtoDebugString(const %(name)s& value);""" % {'name':
    329                                                               message.name}
    330   define_begin = """
    331 std::string GetProtoDebugString(const %(name)s& value) {
    332   return GetProtoDebugStringWithIndent(value, 0);
    333 }
    334 
    335 std::string GetProtoDebugStringWithIndent(const %(name)s& value,
    336                                           int indent_size) {
    337   std::string indent(indent_size, ' ');
    338   std::string output = base::StringPrintf("[%%s] {\\n",
    339                                           value.GetTypeName().c_str());
    340 """ % {'name': message.name}
    341   define_end = """
    342   output += indent + "}\\n";
    343   return output;
    344 }
    345 """
    346   singular_field = """
    347   if (value.has_%(name)s()) {
    348     output += indent + "  %(name)s: ";
    349     base::StringAppendF(&output, %(format)s);
    350     output += "\\n";
    351   }"""
    352   repeated_field = """
    353   output += indent + "  %(name)s: {";
    354   for (int i = 0; i < value.%(name)s_size(); ++i) {
    355     if (i > 0) {
    356       base::StringAppendF(&output, ", ");
    357     }
    358     base::StringAppendF(&output, %(format)s);
    359   }
    360   output += "}\\n";"""
    361   singular_field_get = 'value.%(name)s()'
    362   repeated_field_get = 'value.%(name)s(i)'
    363   formats = {'bool': '"%%s", %(value)s ? "true" : "false"',
    364              'int32': '"%%d", %(value)s',
    365              'int64': '"%%ld", %(value)s',
    366              'uint32': '"%%u (0x%%08X)", %(value)s, %(value)s',
    367              'uint64': '"%%lu (0x%%016X)", %(value)s, %(value)s',
    368              'string': '"%%s", %(value)s.c_str()',
    369              'bytes': """"%%s", base::HexEncode(%(value)s.data(),
    370                                                 %(value)s.size()).c_str()"""}
    371   subtype_format = ('"%%s", GetProtoDebugStringWithIndent(%(value)s, '
    372                     'indent_size + 2).c_str()')
    373 
    374   header_file.write(declare)
    375   impl_file.write(define_begin)
    376   for field in message.fields:
    377     if field.repeated:
    378       value_get = repeated_field_get % {'name': field.name}
    379       field_code = repeated_field
    380     else:
    381       value_get = singular_field_get % {'name': field.name}
    382       field_code = singular_field
    383     if field.type_ in formats:
    384       value_format = formats[field.type_] % {'value': value_get}
    385     else:
    386       value_format = subtype_format % {'value': value_get}
    387     impl_file.write(field_code % {'name': field.name,
    388                                   'format': value_format})
    389   impl_file.write(define_end)
    390 
    391 
    392 def FormatFile(filename):
    393   subprocess.call(['clang-format', '-i', '-style=Chromium', filename])
    394 
    395 
    396 def main():
    397   parser = argparse.ArgumentParser(description='print proto code generator')
    398   parser.add_argument('input_file')
    399   parser.add_argument('--subdir', default='')
    400   args = parser.parse_args()
    401   with open(args.input_file) as input_file:
    402     package, imports, messages, enums = ParseProto(input_file)
    403   proto_name = os.path.basename(args.input_file).rsplit('.', 1)[0]
    404   header_file_name = 'print_%s_proto.h' % proto_name
    405   impl_file_name = 'print_%s_proto.cc' % proto_name
    406   with open(header_file_name, 'w') as header_file:
    407     with open(impl_file_name, 'w') as impl_file:
    408       GenerateFileHeaders(proto_name, package, imports, args.subdir,
    409                           header_file_name, header_file, impl_file)
    410       for enum in enums:
    411         GenerateEnumPrinter(enum, header_file, impl_file)
    412       for message in messages:
    413         GenerateMessagePrinter(message, header_file, impl_file)
    414       GenerateFileFooters(proto_name, package, args.subdir, header_file,
    415                           impl_file)
    416   FormatFile(header_file_name)
    417   FormatFile(impl_file_name)
    418 
    419 if __name__ == '__main__':
    420   main()
    421