#! /usr/bin/env python # Copyright 2016 The Chromium Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """A generator of mojom interfaces and typemaps from Chrome IPC messages. For example, generate_mojom.py content/common/file_utilities_messages.h --output_mojom=content/common/file_utilities.mojom --output_typemap=content/common/file_utilities.typemap """ import argparse import logging import os import re import subprocess import sys _MESSAGE_PATTERN = re.compile( r'(?:\n|^)IPC_(SYNC_)?MESSAGE_(ROUTED|CONTROL)(\d_)?(\d)') _VECTOR_PATTERN = re.compile(r'std::(vector|set)<(.*)>') _MAP_PATTERN = re.compile(r'std::map<(.*), *(.*)>') _NAMESPACE_PATTERN = re.compile(r'([a-z_]*?)::([A-Z].*)') _unused_arg_count = 0 def _git_grep(pattern, paths_pattern): try: args = ['git', 'grep', '-l', '-e', pattern, '--'] + paths_pattern result = subprocess.check_output(args).strip().splitlines() logging.debug('%s => %s', ' '.join(args), result) return result except subprocess.CalledProcessError: logging.debug('%s => []', ' '.join(args)) return [] def _git_multigrep(patterns, paths): """Find a list of files that match all of the provided patterns.""" if isinstance(paths, str): paths = [paths] if isinstance(patterns, str): patterns = [patterns] for pattern in patterns: # Search only the files that matched previous patterns. paths = _git_grep(pattern, paths) if not paths: return [] return paths class Typemap(object): def __init__(self, typemap_files): self._typemap_files = typemap_files self._custom_mappings = {} self._new_custom_mappings = {} self._imports = set() self._public_includes = set() self._traits_includes = set() self._enums = set() def load_typemaps(self): for typemap in self._typemap_files: self.load_typemap(typemap) def load_typemap(self, path): typemap = {} with open(path) as f: content = f.read().replace('=\n', '=') exec content in typemap for mapping in typemap['type_mappings']: mojom, native = mapping.split('=') self._custom_mappings[native] = {'name': mojom, 'mojom': typemap['mojom'].strip('/')} def generate_typemap(self, output_mojom, input_filename, namespace): new_mappings = sorted(self._format_new_mappings(namespace)) if not new_mappings: return yield """# Copyright 2016 The Chromium Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """ yield 'mojom = "//%s"' % output_mojom yield 'public_headers = [%s\n]' % ''.join( '\n "//%s",' % include for include in sorted(self._public_includes)) yield 'traits_headers = [%s\n]' % ''.join( '\n "//%s",' % include for include in sorted(self._traits_includes.union([os.path.normpath( input_filename)]))) yield 'deps = [ "//ipc" ]' yield 'type_mappings = [\n %s\n]' % '\n '.join(new_mappings) def _format_new_mappings(self, namespace): for native, mojom in self._new_custom_mappings.iteritems(): yield '"%s.%s=::%s",' % (namespace, mojom, native) def format_new_types(self): for native_type, typename in self._new_custom_mappings.iteritems(): if native_type in self._enums: yield '[Native]\nenum %s;\n' % typename else: yield '[Native]\nstruct %s;\n' % typename _BUILTINS = { 'bool': 'bool', 'int': 'int32', 'unsigned': 'uint32', 'char': 'uint8', 'unsigned char': 'uint8', 'short': 'int16', 'unsigned short': 'uint16', 'int8_t': 'int8', 'int16_t': 'int16', 'int32_t': 'int32', 'int64_t': 'int64', 'uint8_t': 'uint8', 'uint16_t': 'uint16', 'uint32_t': 'uint32', 'uint64_t': 'uint64', 'float': 'float', 'double': 'double', 'std::string': 'string', 'base::string16': 'string', 'base::FilePath::StringType': 'string', 'base::SharedMemoryHandle': 'handle', 'IPC::PlatformFileForTransit': 'handle', 'base::FileDescriptor': 'handle', } def lookup_type(self, typename): try: return self._BUILTINS[typename] except KeyError: pass vector_match = _VECTOR_PATTERN.search(typename) if vector_match: return 'array<%s>' % self.lookup_type(vector_match.groups()[1].strip()) map_match = _MAP_PATTERN.search(typename) if map_match: return 'map<%s, %s>' % tuple(self.lookup_type(t.strip()) for t in map_match.groups()) try: result = self._custom_mappings[typename]['name'] mojom = self._custom_mappings[typename].get('mojom', None) if mojom: self._imports.add(mojom) return result except KeyError: pass match = _NAMESPACE_PATTERN.match(typename) if match: namespace, name = match.groups() else: namespace = '' name = typename namespace = namespace.replace('::', '.') cpp_name = name name = name.replace('::', '') if name.endswith('Params'): try: _, name = name.rsplit('Msg_') except ValueError: try: _, name = name.split('_', 1) except ValueError: pass if namespace.endswith('.mojom'): generated_mojom_name = '%s.%s' % (namespace, name) elif not namespace: generated_mojom_name = 'mojom.%s' % name else: generated_mojom_name = '%s.mojom.%s' % (namespace, name) self._new_custom_mappings[typename] = name self._add_includes(namespace, cpp_name, typename) generated_mojom_name = name self._custom_mappings[typename] = {'name': generated_mojom_name} return generated_mojom_name def _add_includes(self, namespace, name, fullname): name_components = name.split('::') is_enum = False for i in xrange(len(name_components)): subname = '::'.join(name_components[i:]) extra_names = name_components[:i] + [subname] patterns = [r'\(struct\|class\|enum\)[A-Z_ ]* %s {' % s for s in extra_names] if namespace: patterns.extend(r'namespace %s' % namespace_component for namespace_component in namespace.split('.')) includes = _git_multigrep(patterns, '*.h') if includes: if _git_grep(r'enum[A-Z_ ]* %s {' % subname, includes): self._enums.add(fullname) is_enum = True logging.info('%s => public_headers = %s', fullname, includes) self._public_includes.update(includes) break if is_enum: patterns = ['IPC_ENUM_TRAITS[A-Z_]*(%s' % fullname] else: patterns = [r'\(IPC_STRUCT_TRAITS_BEGIN(\|ParamTraits<\)%s' % fullname] includes = _git_multigrep( patterns, ['*messages.h', '*struct_traits.h', 'ipc/ipc_message_utils.h']) if includes: logging.info('%s => traits_headers = %s', fullname, includes) self._traits_includes.update(includes) def format_imports(self): for import_name in sorted(self._imports): yield 'import "%s";' % import_name if self._imports: yield '' class Argument(object): def __init__(self, typename, name): self.typename = typename.strip() self.name = name.strip().replace('\n', '').replace(' ', '_').lower() if not self.name: global _unused_arg_count self.name = 'unnamed_arg%d' % _unused_arg_count _unused_arg_count += 1 def format(self, typemaps): return '%s %s' % (typemaps.lookup_type(self.typename), self.name) class Message(object): def __init__(self, match, content): self.sync = bool(match[0]) self.routed = match[1] == 'ROUTED' self.args = [] self.response_args = [] if self.sync: num_expected_args = int(match[2][:-1]) num_expected_response_args = int(match[3]) else: num_expected_args = int(match[3]) num_expected_response_args = 0 body = content.split(',') name = body[0].strip() try: self.group, self.name = name.split('Msg_') except ValueError: try: self.group, self.name = name.split('_') except ValueError: self.group = 'UnnamedInterface' self.name = name self.group = '%s%s' % (self.group, match[1].title()) args = list(self.parse_args(','.join(body[1:]))) if len(args) != num_expected_args + num_expected_response_args: raise Exception('Incorrect number of args parsed for %s' % (name)) self.args = args[:num_expected_args] self.response_args = args[num_expected_args:] def parse_args(self, args_str): args_str = args_str.strip() if not args_str: return looking_for_type = False type_start = 0 comment_start = None comment_end = None type_end = None angle_bracket_nesting = 0 i = 0 while i < len(args_str): if args_str[i] == ',' and not angle_bracket_nesting: looking_for_type = True if type_end is None: type_end = i elif args_str[i:i + 2] == '/*': if type_end is None: type_end = i comment_start = i + 2 comment_end = args_str.index('*/', i + 2) i = comment_end + 1 elif args_str[i:i + 2] == '//': if type_end is None: type_end = i comment_start = i + 2 comment_end = args_str.index('\n', i + 2) i = comment_end elif args_str[i] == '<': angle_bracket_nesting += 1 elif args_str[i] == '>': angle_bracket_nesting -= 1 elif looking_for_type and args_str[i].isalpha(): if comment_start is not None and comment_end is not None: yield Argument(args_str[type_start:type_end], args_str[comment_start:comment_end]) else: yield Argument(args_str[type_start:type_end], '') type_start = i type_end = None comment_start = None comment_end = None looking_for_type = False i += 1 if comment_start is not None and comment_end is not None: yield Argument(args_str[type_start:type_end], args_str[comment_start:comment_end]) else: yield Argument(args_str[type_start:type_end], '') def format(self, typemaps): result = '%s(%s)' % (self.name, ','.join('\n %s' % arg.format(typemaps) for arg in self.args)) if self.sync: result += ' => (%s)' % (',\n'.join('\n %s' % arg.format(typemaps) for arg in self.response_args)) result = '[Sync]\n %s' % result return '%s;' % result class Generator(object): def __init__(self, input_name, output_namespace): self._input_name = input_name with open(input_name) as f: self._content = f.read() self._namespace = output_namespace self._typemaps = Typemap(self._find_typemaps()) self._interface_definitions = [] def _get_messages(self): for m in _MESSAGE_PATTERN.finditer(self._content): i = m.end() + 1 while i < len(self._content): if self._content[i:i + 2] == '/*': i = self._content.index('*/', i + 2) + 1 elif self._content[i] == ')': yield Message(m.groups(), self._content[m.end() + 1:i]) break i += 1 def _extract_messages(self): grouped_messages = {} for m in self._get_messages(): grouped_messages.setdefault(m.group, []).append(m) self._typemaps.load_typemaps() for interface, messages in grouped_messages.iteritems(): self._interface_definitions.append(self._format_interface(interface, messages)) def count(self): grouped_messages = {} for m in self._get_messages(): grouped_messages.setdefault(m.group, []).append(m) return sum(len(messages) for messages in grouped_messages.values()) def generate_mojom(self): self._extract_messages() if not self._interface_definitions: return yield """// Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. """ yield 'module %s;\n' % self._namespace for import_statement in self._typemaps.format_imports(): yield import_statement for typemap in self._typemaps.format_new_types(): yield typemap for interface in self._interface_definitions: yield interface yield '' def generate_typemap(self, output_mojom, input_filename): return '\n'.join(self._typemaps.generate_typemap( output_mojom, input_filename, self._namespace)).strip() @staticmethod def _find_typemaps(): return subprocess.check_output( ['git', 'ls-files', '*.typemap']).strip().split('\n') def _format_interface(self, name, messages): return 'interface %s {\n %s\n};' % (name, '\n '.join(m.format(self._typemaps) for m in messages)) def parse_args(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('input', help='input messages.h file') parser.add_argument( '--output_namespace', default='mojom', help='the mojom module name to use in the generated mojom file ' '(default: %(default)s)') parser.add_argument('--output_mojom', help='output mojom path') parser.add_argument('--output_typemap', help='output typemap path') parser.add_argument( '--count', action='store_true', default=False, help='count the number of messages in the input instead of generating ' 'a mojom file') parser.add_argument('-v', '--verbose', action='store_true', help='enable logging') parser.add_argument('-vv', action='store_true', help='enable debug logging') return parser.parse_args() def main(): args = parse_args() if args.vv: logging.basicConfig(level=logging.DEBUG) elif args.verbose: logging.basicConfig(level=logging.INFO) generator = Generator(args.input, args.output_namespace) if args.count: count = generator.count() if count: print '%d %s' % (generator.count(), args.input) return mojom = '\n'.join(generator.generate_mojom()).strip() if not mojom: return typemap = generator.generate_typemap(args.output_mojom, args.input) if args.output_mojom: with open(args.output_mojom, 'w') as f: f.write(mojom) else: print mojom if typemap: if args.output_typemap: with open(args.output_typemap, 'w') as f: f.write(typemap) else: print typemap if __name__ == '__main__': sys.exit(main())