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