• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
7For example,
8generate_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
13import argparse
14import logging
15import os
16import re
17import subprocess
18import 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
29def _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
40def _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
54class 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
226class 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
240class 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
330class 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
398def 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
422def 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
452if __name__ == '__main__':
453  sys.exit(main())
454