Home | History | Annotate | Download | only in chrome_ipc
      1 #! /usr/bin/env python
      2 # Copyright 2016 The Chromium Authors. All rights reserved.
      3 # Use of this source code is governed by a BSD-style license that can be
      4 # found in the LICENSE file.
      5 """A generator of mojom interfaces and typemaps from Chrome IPC messages.
      6 
      7 For example,
      8 generate_mojom.py content/common/file_utilities_messages.h
      9     --output_mojom=content/common/file_utilities.mojom
     10     --output_typemap=content/common/file_utilities.typemap
     11 """
     12 
     13 import argparse
     14 import logging
     15 import os
     16 import re
     17 import subprocess
     18 import sys
     19 
     20 _MESSAGE_PATTERN = re.compile(
     21     r'(?:\n|^)IPC_(SYNC_)?MESSAGE_(ROUTED|CONTROL)(\d_)?(\d)')
     22 _VECTOR_PATTERN = re.compile(r'std::(vector|set)<(.*)>')
     23 _MAP_PATTERN = re.compile(r'std::map<(.*), *(.*)>')
     24 _NAMESPACE_PATTERN = re.compile(r'([a-z_]*?)::([A-Z].*)')
     25 
     26 _unused_arg_count = 0
     27 
     28 
     29 def _git_grep(pattern, paths_pattern):
     30   try:
     31     args = ['git', 'grep', '-l', '-e', pattern, '--'] + paths_pattern
     32     result = subprocess.check_output(args).strip().splitlines()
     33     logging.debug('%s => %s', ' '.join(args), result)
     34     return result
     35   except subprocess.CalledProcessError:
     36     logging.debug('%s => []', ' '.join(args))
     37     return []
     38 
     39 
     40 def _git_multigrep(patterns, paths):
     41   """Find a list of files that match all of the provided patterns."""
     42   if isinstance(paths, str):
     43     paths = [paths]
     44   if isinstance(patterns, str):
     45     patterns = [patterns]
     46   for pattern in patterns:
     47     # Search only the files that matched previous patterns.
     48     paths = _git_grep(pattern, paths)
     49     if not paths:
     50       return []
     51   return paths
     52 
     53 
     54 class Typemap(object):
     55 
     56   def __init__(self, typemap_files):
     57     self._typemap_files = typemap_files
     58     self._custom_mappings = {}
     59     self._new_custom_mappings = {}
     60     self._imports = set()
     61     self._public_includes = set()
     62     self._traits_includes = set()
     63     self._enums = set()
     64 
     65   def load_typemaps(self):
     66     for typemap in self._typemap_files:
     67       self.load_typemap(typemap)
     68 
     69   def load_typemap(self, path):
     70     typemap = {}
     71     with open(path) as f:
     72       content = f.read().replace('=\n', '=')
     73     exec content in typemap
     74     for mapping in typemap['type_mappings']:
     75       mojom, native = mapping.split('=')
     76       self._custom_mappings[native] = {'name': mojom,
     77                                        'mojom': typemap['mojom'].strip('/')}
     78 
     79   def generate_typemap(self, output_mojom, input_filename, namespace):
     80     new_mappings = sorted(self._format_new_mappings(namespace))
     81     if not new_mappings:
     82       return
     83     yield """# Copyright 2016 The Chromium Authors. All rights reserved.
     84 # Use of this source code is governed by a BSD-style license that can be
     85 # found in the LICENSE file.
     86 """
     87     yield 'mojom = "//%s"' % output_mojom
     88     yield 'public_headers = [%s\n]' % ''.join(
     89         '\n  "//%s",' % include for include in sorted(self._public_includes))
     90     yield 'traits_headers = [%s\n]' % ''.join(
     91         '\n  "//%s",' % include
     92         for include in sorted(self._traits_includes.union([os.path.normpath(
     93             input_filename)])))
     94     yield 'deps = [ "//ipc" ]'
     95     yield 'type_mappings = [\n  %s\n]' % '\n  '.join(new_mappings)
     96 
     97   def _format_new_mappings(self, namespace):
     98     for native, mojom in self._new_custom_mappings.iteritems():
     99       yield '"%s.%s=::%s",' % (namespace, mojom, native)
    100 
    101   def format_new_types(self):
    102     for native_type, typename in self._new_custom_mappings.iteritems():
    103       if native_type in self._enums:
    104         yield '[Native]\nenum %s;\n' % typename
    105       else:
    106         yield '[Native]\nstruct %s;\n' % typename
    107 
    108   _BUILTINS = {
    109       'bool': 'bool',
    110       'int': 'int32',
    111       'unsigned': 'uint32',
    112       'char': 'uint8',
    113       'unsigned char': 'uint8',
    114       'short': 'int16',
    115       'unsigned short': 'uint16',
    116       'int8_t': 'int8',
    117       'int16_t': 'int16',
    118       'int32_t': 'int32',
    119       'int64_t': 'int64',
    120       'uint8_t': 'uint8',
    121       'uint16_t': 'uint16',
    122       'uint32_t': 'uint32',
    123       'uint64_t': 'uint64',
    124       'float': 'float',
    125       'double': 'double',
    126       'std::string': 'string',
    127       'base::string16': 'string',
    128       'base::FilePath::StringType': 'string',
    129       'base::SharedMemoryHandle': 'handle<shared_memory>',
    130       'IPC::PlatformFileForTransit': 'handle',
    131       'base::FileDescriptor': 'handle',
    132   }
    133 
    134   def lookup_type(self, typename):
    135     try:
    136       return self._BUILTINS[typename]
    137     except KeyError:
    138       pass
    139 
    140     vector_match = _VECTOR_PATTERN.search(typename)
    141     if vector_match:
    142       return 'array<%s>' % self.lookup_type(vector_match.groups()[1].strip())
    143     map_match = _MAP_PATTERN.search(typename)
    144     if map_match:
    145       return 'map<%s, %s>' % tuple(self.lookup_type(t.strip())
    146                                    for t in map_match.groups())
    147     try:
    148       result = self._custom_mappings[typename]['name']
    149       mojom = self._custom_mappings[typename].get('mojom', None)
    150       if mojom:
    151         self._imports.add(mojom)
    152       return result
    153     except KeyError:
    154       pass
    155 
    156     match = _NAMESPACE_PATTERN.match(typename)
    157     if match:
    158       namespace, name = match.groups()
    159     else:
    160       namespace = ''
    161       name = typename
    162     namespace = namespace.replace('::', '.')
    163     cpp_name = name
    164     name = name.replace('::', '')
    165 
    166     if name.endswith('Params'):
    167       try:
    168         _, name = name.rsplit('Msg_')
    169       except ValueError:
    170         try:
    171           _, name = name.split('_', 1)
    172         except ValueError:
    173           pass
    174 
    175     if namespace.endswith('.mojom'):
    176       generated_mojom_name = '%s.%s' % (namespace, name)
    177     elif not namespace:
    178       generated_mojom_name = 'mojom.%s' % name
    179     else:
    180       generated_mojom_name = '%s.mojom.%s' % (namespace, name)
    181 
    182     self._new_custom_mappings[typename] = name
    183     self._add_includes(namespace, cpp_name, typename)
    184     generated_mojom_name = name
    185     self._custom_mappings[typename] = {'name': generated_mojom_name}
    186     return generated_mojom_name
    187 
    188   def _add_includes(self, namespace, name, fullname):
    189     name_components = name.split('::')
    190     is_enum = False
    191     for i in xrange(len(name_components)):
    192       subname = '::'.join(name_components[i:])
    193       extra_names = name_components[:i] + [subname]
    194       patterns = [r'\(struct\|class\|enum\)[A-Z_ ]* %s {' % s
    195                   for s in extra_names]
    196       if namespace:
    197         patterns.extend(r'namespace %s' % namespace_component
    198                         for namespace_component in namespace.split('.'))
    199       includes = _git_multigrep(patterns, '*.h')
    200       if includes:
    201         if _git_grep(r'enum[A-Z_ ]* %s {' % subname, includes):
    202           self._enums.add(fullname)
    203           is_enum = True
    204         logging.info('%s => public_headers = %s', fullname, includes)
    205         self._public_includes.update(includes)
    206         break
    207 
    208     if is_enum:
    209       patterns = ['IPC_ENUM_TRAITS[A-Z_]*(%s' % fullname]
    210     else:
    211       patterns = [r'\(IPC_STRUCT_TRAITS_BEGIN(\|ParamTraits<\)%s' % fullname]
    212     includes = _git_multigrep(
    213         patterns,
    214         ['*messages.h', '*struct_traits.h', 'ipc/ipc_message_utils.h'])
    215     if includes:
    216       logging.info('%s => traits_headers = %s', fullname, includes)
    217       self._traits_includes.update(includes)
    218 
    219   def format_imports(self):
    220     for import_name in sorted(self._imports):
    221       yield 'import "%s";' % import_name
    222     if self._imports:
    223       yield ''
    224 
    225 
    226 class Argument(object):
    227 
    228   def __init__(self, typename, name):
    229     self.typename = typename.strip()
    230     self.name = name.strip().replace('\n', '').replace(' ', '_').lower()
    231     if not self.name:
    232       global _unused_arg_count
    233       self.name = 'unnamed_arg%d' % _unused_arg_count
    234       _unused_arg_count += 1
    235 
    236   def format(self, typemaps):
    237     return '%s %s' % (typemaps.lookup_type(self.typename), self.name)
    238 
    239 
    240 class Message(object):
    241 
    242   def __init__(self, match, content):
    243     self.sync = bool(match[0])
    244     self.routed = match[1] == 'ROUTED'
    245     self.args = []
    246     self.response_args = []
    247     if self.sync:
    248       num_expected_args = int(match[2][:-1])
    249       num_expected_response_args = int(match[3])
    250     else:
    251       num_expected_args = int(match[3])
    252       num_expected_response_args = 0
    253     body = content.split(',')
    254     name = body[0].strip()
    255     try:
    256       self.group, self.name = name.split('Msg_')
    257     except ValueError:
    258       try:
    259         self.group, self.name = name.split('_')
    260       except ValueError:
    261         self.group = 'UnnamedInterface'
    262         self.name = name
    263     self.group = '%s%s' % (self.group, match[1].title())
    264     args = list(self.parse_args(','.join(body[1:])))
    265     if len(args) != num_expected_args + num_expected_response_args:
    266       raise Exception('Incorrect number of args parsed for %s' % (name))
    267     self.args = args[:num_expected_args]
    268     self.response_args = args[num_expected_args:]
    269 
    270   def parse_args(self, args_str):
    271     args_str = args_str.strip()
    272     if not args_str:
    273       return
    274     looking_for_type = False
    275     type_start = 0
    276     comment_start = None
    277     comment_end = None
    278     type_end = None
    279     angle_bracket_nesting = 0
    280     i = 0
    281     while i < len(args_str):
    282       if args_str[i] == ',' and not angle_bracket_nesting:
    283         looking_for_type = True
    284         if type_end is None:
    285           type_end = i
    286       elif args_str[i:i + 2] == '/*':
    287         if type_end is None:
    288           type_end = i
    289         comment_start = i + 2
    290         comment_end = args_str.index('*/', i + 2)
    291         i = comment_end + 1
    292       elif args_str[i:i + 2] == '//':
    293         if type_end is None:
    294           type_end = i
    295         comment_start = i + 2
    296         comment_end = args_str.index('\n', i + 2)
    297         i = comment_end
    298       elif args_str[i] == '<':
    299         angle_bracket_nesting += 1
    300       elif args_str[i] == '>':
    301         angle_bracket_nesting -= 1
    302       elif looking_for_type and args_str[i].isalpha():
    303         if comment_start is not None and comment_end is not None:
    304           yield Argument(args_str[type_start:type_end],
    305                          args_str[comment_start:comment_end])
    306         else:
    307           yield Argument(args_str[type_start:type_end], '')
    308         type_start = i
    309         type_end = None
    310         comment_start = None
    311         comment_end = None
    312         looking_for_type = False
    313       i += 1
    314     if comment_start is not None and comment_end is not None:
    315       yield Argument(args_str[type_start:type_end],
    316                      args_str[comment_start:comment_end])
    317     else:
    318       yield Argument(args_str[type_start:type_end], '')
    319 
    320   def format(self, typemaps):
    321     result = '%s(%s)' % (self.name, ','.join('\n      %s' % arg.format(typemaps)
    322                                              for arg in self.args))
    323     if self.sync:
    324       result += ' => (%s)' % (',\n'.join('\n      %s' % arg.format(typemaps)
    325                                          for arg in self.response_args))
    326       result = '[Sync]\n  %s' % result
    327     return '%s;' % result
    328 
    329 
    330 class Generator(object):
    331 
    332   def __init__(self, input_name, output_namespace):
    333     self._input_name = input_name
    334     with open(input_name) as f:
    335       self._content = f.read()
    336     self._namespace = output_namespace
    337     self._typemaps = Typemap(self._find_typemaps())
    338     self._interface_definitions = []
    339 
    340   def _get_messages(self):
    341     for m in _MESSAGE_PATTERN.finditer(self._content):
    342       i = m.end() + 1
    343       while i < len(self._content):
    344         if self._content[i:i + 2] == '/*':
    345           i = self._content.index('*/', i + 2) + 1
    346         elif self._content[i] == ')':
    347           yield Message(m.groups(), self._content[m.end() + 1:i])
    348           break
    349         i += 1
    350 
    351   def _extract_messages(self):
    352     grouped_messages = {}
    353     for m in self._get_messages():
    354       grouped_messages.setdefault(m.group, []).append(m)
    355     self._typemaps.load_typemaps()
    356     for interface, messages in grouped_messages.iteritems():
    357       self._interface_definitions.append(self._format_interface(interface,
    358                                                                 messages))
    359 
    360   def count(self):
    361     grouped_messages = {}
    362     for m in self._get_messages():
    363       grouped_messages.setdefault(m.group, []).append(m)
    364     return sum(len(messages) for messages in grouped_messages.values())
    365 
    366   def generate_mojom(self):
    367     self._extract_messages()
    368     if not self._interface_definitions:
    369       return
    370     yield """// Copyright 2016 The Chromium Authors. All rights reserved.
    371 // Use of this source code is governed by a BSD-style license that can be
    372 // found in the LICENSE file.
    373 """
    374     yield 'module %s;\n' % self._namespace
    375     for import_statement in self._typemaps.format_imports():
    376       yield import_statement
    377     for typemap in self._typemaps.format_new_types():
    378       yield typemap
    379     for interface in self._interface_definitions:
    380       yield interface
    381       yield ''
    382 
    383   def generate_typemap(self, output_mojom, input_filename):
    384     return '\n'.join(self._typemaps.generate_typemap(
    385         output_mojom, input_filename, self._namespace)).strip()
    386 
    387   @staticmethod
    388   def _find_typemaps():
    389     return subprocess.check_output(
    390         ['git', 'ls-files', '*.typemap']).strip().split('\n')
    391 
    392   def _format_interface(self, name, messages):
    393     return 'interface %s {\n  %s\n};' % (name,
    394                                          '\n  '.join(m.format(self._typemaps)
    395                                                      for m in messages))
    396 
    397 
    398 def parse_args():
    399   parser = argparse.ArgumentParser(description=__doc__)
    400   parser.add_argument('input', help='input messages.h file')
    401   parser.add_argument(
    402       '--output_namespace',
    403       default='mojom',
    404       help='the mojom module name to use in the generated mojom file '
    405       '(default: %(default)s)')
    406   parser.add_argument('--output_mojom', help='output mojom path')
    407   parser.add_argument('--output_typemap', help='output typemap path')
    408   parser.add_argument(
    409       '--count',
    410       action='store_true',
    411       default=False,
    412       help='count the number of messages in the input instead of generating '
    413       'a mojom file')
    414   parser.add_argument('-v',
    415                       '--verbose',
    416                       action='store_true',
    417                       help='enable logging')
    418   parser.add_argument('-vv', action='store_true', help='enable debug logging')
    419   return parser.parse_args()
    420 
    421 
    422 def main():
    423   args = parse_args()
    424   if args.vv:
    425     logging.basicConfig(level=logging.DEBUG)
    426   elif args.verbose:
    427     logging.basicConfig(level=logging.INFO)
    428   generator = Generator(args.input, args.output_namespace)
    429   if args.count:
    430     count = generator.count()
    431     if count:
    432       print '%d %s' % (generator.count(), args.input)
    433     return
    434   mojom = '\n'.join(generator.generate_mojom()).strip()
    435   if not mojom:
    436     return
    437   typemap = generator.generate_typemap(args.output_mojom, args.input)
    438 
    439   if args.output_mojom:
    440     with open(args.output_mojom, 'w') as f:
    441       f.write(mojom)
    442   else:
    443     print mojom
    444   if typemap:
    445     if args.output_typemap:
    446       with open(args.output_typemap, 'w') as f:
    447         f.write(typemap)
    448     else:
    449       print typemap
    450 
    451 
    452 if __name__ == '__main__':
    453   sys.exit(main())
    454