1# Copyright 2012 The Chromium Authors 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4"""Entry point for "from-source" and "from-jar" commands.""" 5 6import collections 7import os 8import pickle 9import shutil 10import subprocess 11import sys 12import tempfile 13import zipfile 14 15from codegen import called_by_native_header 16from codegen import convert_type 17from codegen import header_common 18from codegen import natives_header 19from codegen import placeholder_gen_jni_java 20from codegen import placeholder_java_type 21from codegen import proxy_impl_java 22import common 23import java_types 24import parse 25import proxy 26 27 28class NativeMethod: 29 """Describes a C/C++ method that is called by Java.""" 30 def __init__(self, parsed_method, *, java_class, is_proxy): 31 # The Java class the containing the natives. Never a nested class. 32 self.java_class = java_class 33 self.is_proxy = is_proxy 34 # The method name. For non-proxy natives, this omits the "native" prefix. 35 self.name = parsed_method.name 36 self.capitalized_name = common.capitalize(self.name) 37 self.is_test_only = NameIsTestOnly(parsed_method.name) 38 self.signature = parsed_method.signature 39 self.static = self.is_proxy or parsed_method.static 40 # Value of @NativeClassQualifiedName. 41 self.native_class_name = parsed_method.native_class_name 42 43 # True when an extra jclass parameter should be added. 44 self.needs_implicit_array_element_class_param = ( 45 self.is_proxy 46 and proxy.needs_implicit_array_element_class_param(self.return_type)) 47 48 if self.is_proxy: 49 # Signature with all reference types changed to "Object". 50 self.proxy_signature = self.signature.to_proxy() 51 if self.needs_implicit_array_element_class_param: 52 self.proxy_signature = proxy.add_implicit_array_element_class_param( 53 self.proxy_signature) 54 # proxy_signature with params reordered. Does not include switch_num. 55 self.muxed_signature = proxy.muxed_signature(self.proxy_signature) 56 57 # Name to use when using per-file natives. 58 # "native" prefix to not conflict with interface method names. 59 self.per_file_name = f'native{self.capitalized_name}' 60 # Method name within the GEN_JNI class. 61 self.proxy_name = f'{java_class.to_cpp()}_{self.name}' 62 # Method name within the J class (when is_hashing=True). 63 # TODO(agrieve): No need to mangle before hashing. 64 self.hashed_name = proxy.hashed_name( 65 common.jni_mangle(f'{java_class.full_name_with_slashes}/{self.name}'), 66 self.is_test_only) 67 # Method name within the J class (when is_muxing=True). 68 self.muxed_name = proxy.muxed_name(self.muxed_signature) 69 # Name of C++ function that will be called from switch tables. 70 self.muxed_entry_point_name = f'Muxed_{self.proxy_name}' 71 # Switch statement index when multiplexing. 72 self.muxed_switch_num = None 73 74 # Set when the first param dictates this is implemented as a member 75 # function of the native class given as the first parameter. 76 first_param = self.params and self.params[0] 77 if (first_param and first_param.java_type.is_primitive() 78 and first_param.java_type.primitive_name == 'long' 79 and first_param.name.startswith('native')): 80 if parsed_method.native_class_name: 81 self.first_param_cpp_type = parsed_method.native_class_name 82 else: 83 self.first_param_cpp_type = first_param.name[len('native'):] 84 else: 85 self.first_param_cpp_type = None 86 87 @property 88 def params(self): 89 return self.signature.param_list 90 91 @property 92 def return_type(self): 93 return self.signature.return_type 94 95 @property 96 def proxy_params(self): 97 return self.proxy_signature.param_list 98 99 @property 100 def proxy_return_type(self): 101 return self.proxy_signature.return_type 102 103 @property 104 def muxed_params(self): 105 return self.muxed_signature.param_list 106 107 @property 108 def entry_point_return_type(self): 109 return self.proxy_return_type if self.is_proxy else self.return_type 110 111 def entry_point_params(self, jni_mode): 112 """Params to use for entry point functions.""" 113 if not self.is_proxy: 114 return self.params 115 if jni_mode.is_muxing: 116 return self.muxed_params 117 return self.proxy_params 118 119 def boundary_name(self, jni_mode): 120 """Java name of the JNI native method.""" 121 if not self.is_proxy: 122 return f'native{self.name}' 123 if jni_mode.is_per_file: 124 return f'native{self.capitalized_name}' 125 if jni_mode.is_muxing: 126 return self.muxed_name 127 if jni_mode.is_hashing: 128 return self.hashed_name 129 return self.proxy_name 130 131 def boundary_name_cpp(self, jni_mode, gen_jni_class=None): 132 """C++ name of the JNI native method.""" 133 if not self.is_proxy: 134 mangled_class_name = self.java_class.to_cpp() 135 elif jni_mode.is_per_file: 136 mangled_class_name = self.java_class.to_cpp() + 'Jni' 137 else: 138 mangled_class_name = gen_jni_class.to_cpp() 139 140 method_name = self.boundary_name(jni_mode=jni_mode) 141 mangled_method_name = common.jni_mangle(method_name) 142 return f'Java_{mangled_class_name}_{mangled_method_name}' 143 144 145class CalledByNative: 146 """Describes a Java method that is called from C++""" 147 def __init__(self, 148 parsed_called_by_native, 149 *, 150 is_system_class, 151 unchecked=False): 152 self.name = parsed_called_by_native.name 153 self.signature = parsed_called_by_native.signature 154 self.static = parsed_called_by_native.static 155 self.unchecked = parsed_called_by_native.unchecked or unchecked 156 self.java_class = parsed_called_by_native.java_class 157 self.is_system_class = is_system_class 158 159 # Computed once we know if overloads exist. 160 self.method_id_function_name = None 161 162 @property 163 def is_constructor(self): 164 return self.name == '<init>' 165 166 @property 167 def return_type(self): 168 return self.signature.return_type 169 170 @property 171 def params(self): 172 return self.signature.param_list 173 174 175def NameIsTestOnly(name): 176 return name.endswith(('ForTest', 'ForTests', 'ForTesting')) 177 178 179def _MangleMethodName(type_resolver, name, param_types): 180 # E.g. java.util.List.reversed() has overloads that return different types. 181 if not param_types: 182 return name 183 mangled_types = [] 184 for java_type in param_types: 185 if java_type.primitive_name: 186 part = java_type.primitive_name 187 else: 188 part = type_resolver.contextualize(java_type.java_class).replace('.', '_') 189 mangled_types.append(part + ('Array' * java_type.array_dimensions)) 190 191 return f'{name}__' + '__'.join(mangled_types) 192 193 194def _AssignMethodIdFunctionNames(type_resolver, called_by_natives): 195 # Mangle names for overloads with different number of parameters. 196 def key(called_by_native): 197 return (called_by_native.java_class.full_name_with_slashes, 198 called_by_native.name, len(called_by_native.params)) 199 200 method_counts = collections.Counter(key(x) for x in called_by_natives) 201 cbn_by_name = collections.defaultdict(list) 202 203 for called_by_native in called_by_natives: 204 if called_by_native.is_constructor: 205 method_id_function_name = 'Constructor' 206 else: 207 method_id_function_name = called_by_native.name 208 209 if method_counts[key(called_by_native)] > 1: 210 method_id_function_name = _MangleMethodName( 211 type_resolver, method_id_function_name, 212 called_by_native.signature.param_types) 213 cbn_by_name[method_id_function_name].append(called_by_native) 214 215 called_by_native.method_id_function_name = method_id_function_name 216 217 # E.g. java.util.List.reversed() has overloads that return different types. 218 for duplicates in cbn_by_name.values(): 219 for i, cbn in enumerate(duplicates[1:], 1): 220 cbn.method_id_function_name += str(i) 221 222 223class JniObject: 224 """Uses the given java source file to generate the JNI header file.""" 225 226 def __init__(self, 227 parsed_file, 228 *, 229 from_javap, 230 default_namespace=None, 231 javap_unchecked_exceptions=False): 232 self.filename = parsed_file.filename 233 self.type_resolver = parsed_file.type_resolver 234 self.module_name = parsed_file.module_name 235 self.proxy_interface = parsed_file.proxy_interface 236 self.proxy_visibility = parsed_file.proxy_visibility 237 self.constant_fields = parsed_file.constant_fields 238 239 # These are different only for legacy reasons. 240 if from_javap: 241 self.jni_namespace = default_namespace or 'JNI_' + self.java_class.name.replace( 242 '$', '__') 243 else: 244 self.jni_namespace = parsed_file.jni_namespace or default_namespace 245 246 natives = [] 247 for parsed_method in parsed_file.proxy_methods: 248 natives.append( 249 NativeMethod(parsed_method, java_class=self.java_class, 250 is_proxy=True)) 251 252 for parsed_method in parsed_file.non_proxy_methods: 253 natives.append( 254 NativeMethod(parsed_method, 255 java_class=self.java_class, 256 is_proxy=False)) 257 258 self.natives = natives 259 260 called_by_natives = [] 261 for parsed_called_by_native in parsed_file.called_by_natives: 262 called_by_natives.append( 263 CalledByNative(parsed_called_by_native, 264 unchecked=from_javap and javap_unchecked_exceptions, 265 is_system_class=from_javap)) 266 267 _AssignMethodIdFunctionNames(parsed_file.type_resolver, called_by_natives) 268 self.called_by_natives = called_by_natives 269 270 @property 271 def java_class(self): 272 return self.type_resolver.java_class 273 274 @property 275 def proxy_natives(self): 276 return [n for n in self.natives if n.is_proxy] 277 278 @property 279 def non_proxy_natives(self): 280 return [n for n in self.natives if not n.is_proxy] 281 282 def GetClassesToBeImported(self): 283 classes = set() 284 for n in self.proxy_natives: 285 for t in list(n.signature.param_types) + [n.return_type]: 286 class_obj = t.java_class 287 if class_obj is None: 288 # Primitive types will be None. 289 continue 290 if class_obj.full_name_with_slashes.startswith('java/lang/'): 291 # java.lang** are never imported. 292 continue 293 classes.add(class_obj) 294 295 return sorted(classes) 296 297 def RemoveTestOnlyNatives(self): 298 self.natives = [n for n in self.natives if not n.is_test_only] 299 300 301def _CollectReferencedClasses(jni_obj): 302 ret = set() 303 # @CalledByNatives can appear on nested classes, so check each one. 304 for called_by_native in jni_obj.called_by_natives: 305 ret.add(called_by_native.java_class) 306 for param in called_by_native.params: 307 java_type = param.java_type 308 if java_type.is_object_array() and java_type.converted_type: 309 ret.add(java_type.java_class) 310 311 312 # Find any classes needed for @JniType conversions. 313 for native in jni_obj.proxy_natives: 314 return_type = native.return_type 315 if return_type.is_object_array() and return_type.converted_type: 316 ret.add(return_type.java_class) 317 return sorted(ret) 318 319 320def _generate_header(jni_mode, jni_obj, extra_includes, gen_jni_class): 321 preamble, epilogue = header_common.header_preamble( 322 GetScriptName(), 323 jni_obj.java_class, 324 system_includes=['jni.h'], 325 user_includes=['third_party/jni_zero/jni_export.h'] + extra_includes) 326 java_classes = _CollectReferencedClasses(jni_obj) 327 sb = common.StringBuilder() 328 sb(preamble) 329 330 if java_classes: 331 with sb.section('Class Accessors'): 332 header_common.class_accessors(sb, java_classes, jni_obj.module_name) 333 334 with sb.namespace(jni_obj.jni_namespace): 335 if jni_obj.constant_fields: 336 with sb.section('Constants'): 337 called_by_native_header.constants_enums(sb, jni_obj.java_class, 338 jni_obj.constant_fields) 339 340 if jni_obj.natives: 341 with sb.section('Java to native functions'): 342 for native in jni_obj.natives: 343 natives_header.entry_point_method(sb, jni_mode, jni_obj, native, 344 gen_jni_class) 345 346 if jni_obj.called_by_natives: 347 with sb.section('Native to Java functions'): 348 for called_by_native in jni_obj.called_by_natives: 349 called_by_native_header.method_definition(sb, called_by_native) 350 351 sb(epilogue) 352 return sb.to_string() 353 354 355def GetScriptName(): 356 return '//third_party/jni_zero/jni_zero.py' 357 358 359def _RemoveStaleHeaders(path, output_names): 360 if not os.path.isdir(path): 361 return 362 # Do not remove output files so that timestamps on declared outputs are not 363 # modified unless their contents are changed (avoids reverse deps needing to 364 # be rebuilt). 365 preserve = set(output_names) 366 for root, _, files in os.walk(path): 367 for f in files: 368 if f not in preserve: 369 file_path = os.path.join(root, f) 370 if os.path.isfile(file_path) and file_path.endswith('.h'): 371 os.remove(file_path) 372 373 374def _CheckSameModule(jni_objs): 375 files_by_module = collections.defaultdict(list) 376 for jni_obj in jni_objs: 377 if jni_obj.proxy_natives: 378 files_by_module[jni_obj.module_name].append(jni_obj.filename) 379 if len(files_by_module) > 1: 380 sys.stderr.write( 381 'Multiple values for @NativeMethods(moduleName) is not supported.\n') 382 for module_name, filenames in files_by_module.items(): 383 sys.stderr.write(f'module_name={module_name}\n') 384 for filename in filenames: 385 sys.stderr.write(f' {filename}\n') 386 sys.exit(1) 387 return next(iter(files_by_module)) if files_by_module else None 388 389 390def _CheckNotEmpty(jni_objs): 391 has_empty = False 392 for jni_obj in jni_objs: 393 if not (jni_obj.natives or jni_obj.called_by_natives): 394 has_empty = True 395 sys.stderr.write(f'No native methods found in {jni_obj.filename}.\n') 396 if has_empty: 397 sys.exit(1) 398 399 400def _RunJavap(javap_path, class_file): 401 p = subprocess.run([javap_path, '-s', '-constants', class_file], 402 text=True, 403 capture_output=True, 404 check=True) 405 return p.stdout 406 407 408def _ParseClassFiles(jar_file, class_files, args): 409 # Parse javap output. 410 ret = [] 411 with tempfile.TemporaryDirectory() as temp_dir: 412 with zipfile.ZipFile(jar_file) as z: 413 z.extractall(temp_dir, class_files) 414 for class_file in class_files: 415 class_file = os.path.join(temp_dir, class_file) 416 contents = _RunJavap(args.javap, class_file) 417 parsed_file = parse.parse_javap(class_file, contents) 418 ret.append( 419 JniObject(parsed_file, 420 from_javap=True, 421 default_namespace=args.namespace, 422 javap_unchecked_exceptions=args.unchecked_exceptions)) 423 return ret 424 425 426def _CreateSrcJar(srcjar_path, jni_mode, gen_jni_class, jni_objs, *, 427 script_name): 428 with common.atomic_output(srcjar_path) as f: 429 with zipfile.ZipFile(f, 'w') as srcjar: 430 for jni_obj in jni_objs: 431 if not jni_obj.proxy_natives: 432 continue 433 content = proxy_impl_java.Generate(jni_mode, 434 jni_obj, 435 gen_jni_class=gen_jni_class, 436 script_name=script_name) 437 zip_path = f'{jni_obj.java_class.class_without_prefix.full_name_with_slashes}Jni.java' 438 common.add_to_zip_hermetic(srcjar, zip_path, data=content) 439 440 if not jni_mode.is_per_file: 441 content = placeholder_gen_jni_java.Generate(jni_objs, 442 gen_jni_class=gen_jni_class, 443 script_name=script_name) 444 zip_path = f'{gen_jni_class.full_name_with_slashes}.java' 445 common.add_to_zip_hermetic(srcjar, zip_path, data=content) 446 447 448def _CreatePlaceholderSrcJar(srcjar_path, jni_objs, *, script_name): 449 already_added = set() 450 with common.atomic_output(srcjar_path) as f: 451 with zipfile.ZipFile(f, 'w') as srcjar: 452 for jni_obj in jni_objs: 453 if not jni_obj.proxy_natives: 454 continue 455 main_class = jni_obj.type_resolver.java_class 456 zip_path = main_class.class_without_prefix.full_name_with_slashes + '.java' 457 content = placeholder_java_type.Generate( 458 main_class, 459 jni_obj.type_resolver.nested_classes, 460 script_name=script_name, 461 proxy_interface=jni_obj.proxy_interface, 462 proxy_natives=jni_obj.proxy_natives) 463 common.add_to_zip_hermetic(srcjar, zip_path, data=content) 464 already_added.add(zip_path) 465 # In rare circumstances, another file in our generate_jni list will 466 # import the FooJni from another class within the same generate_jni 467 # target. We want to make sure we don't make placeholders for these, but 468 # we do want placeholders for all BarJni classes that aren't a part of 469 # this generate_jni. 470 fake_zip_path = main_class.class_without_prefix.full_name_with_slashes + 'Jni.java' 471 already_added.add(fake_zip_path) 472 473 placeholders = collections.defaultdict(list) 474 # Doing this in 2 phases to ensure that the Jni classes (the ones that 475 # can have @NativeMethods) all get added first, so we don't accidentally 476 # write a stubbed version of the class if it's imported by another class. 477 for jni_obj in jni_objs: 478 for java_class in jni_obj.GetClassesToBeImported(): 479 if java_class.full_name_with_slashes.startswith('java/'): 480 continue 481 # TODO(mheikal): handle more than 1 nesting layer. 482 if java_class.is_nested(): 483 placeholders[java_class.get_outer_class()].append(java_class) 484 elif java_class not in placeholders: 485 placeholders[java_class] = [] 486 for java_class, nested_classes in placeholders.items(): 487 zip_path = java_class.class_without_prefix.full_name_with_slashes + '.java' 488 if zip_path not in already_added: 489 content = placeholder_java_type.Generate(java_class, 490 nested_classes, 491 script_name=script_name) 492 common.add_to_zip_hermetic(srcjar, zip_path, data=content) 493 already_added.add(zip_path) 494 495 496def _WriteHeaders(jni_mode, 497 jni_objs, 498 output_names, 499 output_dir, 500 extra_includes, 501 gen_jni_class=None): 502 for jni_obj, header_name in zip(jni_objs, output_names): 503 output_file = os.path.join(output_dir, header_name) 504 content = _generate_header(jni_mode, jni_obj, extra_includes, gen_jni_class) 505 506 with common.atomic_output(output_file, 'w') as f: 507 f.write(content) 508 509 510def GenerateFromSource(parser, args, jni_mode): 511 # Remove existing headers so that moving .java source files but not updating 512 # the corresponding C++ include will be a compile failure (otherwise 513 # incremental builds will usually not catch this). 514 _RemoveStaleHeaders(args.output_dir, args.output_names) 515 516 try: 517 parsed_files = [ 518 parse.parse_java_file(f, 519 package_prefix=args.package_prefix, 520 package_prefix_filter=args.package_prefix_filter) 521 for f in args.input_files 522 ] 523 jni_objs = [ 524 JniObject(x, from_javap=False, default_namespace=args.namespace) 525 for x in parsed_files 526 ] 527 _CheckNotEmpty(jni_objs) 528 module_name = _CheckSameModule(jni_objs) 529 except parse.ParseError as e: 530 sys.stderr.write(f'{e}\n') 531 sys.exit(1) 532 533 gen_jni_class = proxy.get_gen_jni_class( 534 short=jni_mode.is_hashing or jni_mode.is_muxing, 535 name_prefix=args.module_name or module_name, 536 package_prefix=args.package_prefix, 537 package_prefix_filter=args.package_prefix_filter) 538 539 _WriteHeaders(jni_mode, jni_objs, args.output_names, args.output_dir, 540 args.extra_includes, gen_jni_class) 541 542 jni_objs_with_proxy_natives = [x for x in jni_objs if x.proxy_natives] 543 # Write .srcjar 544 if args.srcjar_path: 545 if jni_objs_with_proxy_natives: 546 gen_jni_class = proxy.get_gen_jni_class( 547 short=False, 548 name_prefix=jni_objs_with_proxy_natives[0].module_name, 549 package_prefix=args.package_prefix, 550 package_prefix_filter=args.package_prefix_filter) 551 _CreateSrcJar(args.srcjar_path, 552 jni_mode, 553 gen_jni_class, 554 jni_objs_with_proxy_natives, 555 script_name=GetScriptName()) 556 else: 557 # Only @CalledByNatives. 558 zipfile.ZipFile(args.srcjar_path, 'w').close() 559 if args.jni_pickle: 560 with common.atomic_output(args.jni_pickle, 'wb') as f: 561 pickle.dump(parsed_files, f) 562 563 if args.placeholder_srcjar_path: 564 if jni_objs_with_proxy_natives: 565 _CreatePlaceholderSrcJar(args.placeholder_srcjar_path, 566 jni_objs_with_proxy_natives, 567 script_name=GetScriptName()) 568 else: 569 zipfile.ZipFile(args.placeholder_srcjar_path, 'w').close() 570 571 572def GenerateFromJar(parser, args, jni_mode): 573 if not args.javap: 574 args.javap = shutil.which('javap') 575 if not args.javap: 576 parser.error('Could not find "javap" on your PATH. Use --javap to ' 577 'specify its location.') 578 579 # Remove existing headers so that moving .java source files but not updating 580 # the corresponding C++ include will be a compile failure (otherwise 581 # incremental builds will usually not catch this). 582 _RemoveStaleHeaders(args.output_dir, args.output_names) 583 584 try: 585 jni_objs = _ParseClassFiles(args.jar_file, args.input_files, args) 586 except parse.ParseError as e: 587 sys.stderr.write(f'{e}\n') 588 sys.exit(1) 589 590 _WriteHeaders(jni_mode, jni_objs, args.output_names, args.output_dir, 591 args.extra_includes) 592