| # Copyright 2012 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| """Entry point for "from-source" and "from-jar" commands.""" |
| |
| import collections |
| import os |
| import pickle |
| import shutil |
| import subprocess |
| import sys |
| import tempfile |
| import zipfile |
| |
| from codegen import called_by_native_header |
| from codegen import convert_type |
| from codegen import header_common |
| from codegen import natives_header |
| from codegen import placeholder_gen_jni_java |
| from codegen import placeholder_java_type |
| from codegen import proxy_impl_java |
| import common |
| import java_types |
| import parse |
| import proxy |
| |
| |
| class NativeMethod: |
| """Describes a C/C++ method that is called by Java.""" |
| def __init__(self, parsed_method, *, java_class, is_proxy): |
| # The Java class the containing the natives. Never a nested class. |
| self.java_class = java_class |
| self.is_proxy = is_proxy |
| # The method name. For non-proxy natives, this omits the "native" prefix. |
| self.name = parsed_method.name |
| self.capitalized_name = common.capitalize(self.name) |
| self.is_test_only = NameIsTestOnly(parsed_method.name) |
| self.signature = parsed_method.signature |
| self.static = self.is_proxy or parsed_method.static |
| # Value of @NativeClassQualifiedName. |
| self.native_class_name = parsed_method.native_class_name |
| |
| # True when an extra jclass parameter should be added. |
| self.needs_implicit_array_element_class_param = ( |
| self.is_proxy |
| and proxy.needs_implicit_array_element_class_param(self.return_type)) |
| |
| if self.is_proxy: |
| class_without_prefix = java_class.class_without_prefix |
| # Signature with all reference types changed to "Object". |
| self.proxy_signature = self.signature.to_proxy() |
| if self.needs_implicit_array_element_class_param: |
| self.proxy_signature = proxy.add_implicit_array_element_class_param( |
| self.proxy_signature) |
| # proxy_signature with params reordered. Does not include switch_num. |
| self.muxed_signature = proxy.muxed_signature(self.proxy_signature) |
| |
| # Name to use when using per-file natives. |
| # "native" prefix to not conflict with interface method names. |
| self.per_file_name = f'native{self.capitalized_name}' |
| # Method name within the GEN_JNI class. |
| self.proxy_name = f'{class_without_prefix.to_cpp()}_{self.name}' |
| # Method name within the J class (when is_hashing=True). |
| # TODO(agrieve): No need to mangle before hashing. |
| self.hashed_name = proxy.hashed_name( |
| common.jni_mangle( |
| f'{class_without_prefix.full_name_with_slashes}/{self.name}'), |
| self.is_test_only) |
| # Method name within the J class (when is_muxing=True). |
| self.muxed_name = proxy.muxed_name(self.muxed_signature) |
| # Name of C++ function that will be called from switch tables. |
| self.muxed_entry_point_name = f'Muxed_{self.proxy_name}' |
| # Switch statement index when multiplexing. |
| self.muxed_switch_num = None |
| |
| # Set when the first param dictates this is implemented as a member |
| # function of the native class given as the first parameter. |
| first_param = self.params and self.params[0] |
| if (first_param and first_param.java_type.is_primitive() |
| and first_param.java_type.primitive_name == 'long' |
| and first_param.name.startswith('native')): |
| if parsed_method.native_class_name: |
| self.first_param_cpp_type = parsed_method.native_class_name |
| else: |
| self.first_param_cpp_type = first_param.name[len('native'):] |
| else: |
| self.first_param_cpp_type = None |
| |
| @property |
| def params(self): |
| return self.signature.param_list |
| |
| @property |
| def return_type(self): |
| return self.signature.return_type |
| |
| @property |
| def proxy_params(self): |
| return self.proxy_signature.param_list |
| |
| @property |
| def proxy_return_type(self): |
| return self.proxy_signature.return_type |
| |
| @property |
| def muxed_params(self): |
| return self.muxed_signature.param_list |
| |
| @property |
| def entry_point_return_type(self): |
| return self.proxy_return_type if self.is_proxy else self.return_type |
| |
| def entry_point_params(self, jni_mode): |
| """Params to use for entry point functions.""" |
| if not self.is_proxy: |
| return self.params |
| if jni_mode.is_muxing: |
| return self.muxed_params |
| return self.proxy_params |
| |
| def boundary_name(self, jni_mode): |
| """Java name of the JNI native method.""" |
| if not self.is_proxy: |
| return f'native{self.name}' |
| if jni_mode.is_per_file: |
| return f'native{self.capitalized_name}' |
| if jni_mode.is_muxing: |
| return self.muxed_name |
| if jni_mode.is_hashing: |
| return self.hashed_name |
| return self.proxy_name |
| |
| def boundary_name_cpp(self, jni_mode, gen_jni_class=None): |
| """C++ name of the JNI native method.""" |
| if not self.is_proxy: |
| mangled_class_name = self.java_class.to_cpp() |
| elif jni_mode.is_per_file: |
| mangled_class_name = self.java_class.to_cpp() + 'Jni' |
| else: |
| mangled_class_name = gen_jni_class.to_cpp() |
| |
| method_name = self.boundary_name(jni_mode=jni_mode) |
| mangled_method_name = common.jni_mangle(method_name) |
| return f'Java_{mangled_class_name}_{mangled_method_name}' |
| |
| |
| class CalledByNative: |
| """Describes a Java method that is called from C++""" |
| def __init__(self, |
| parsed_called_by_native, |
| *, |
| is_system_class, |
| unchecked=False): |
| self.name = parsed_called_by_native.name |
| self.signature = parsed_called_by_native.signature |
| self.static = parsed_called_by_native.static |
| self.unchecked = parsed_called_by_native.unchecked or unchecked |
| self.java_class = parsed_called_by_native.java_class |
| self.is_system_class = is_system_class |
| |
| # Computed once we know if overloads exist. |
| self.method_id_function_name = None |
| |
| @property |
| def is_constructor(self): |
| return self.name == '<init>' |
| |
| @property |
| def return_type(self): |
| return self.signature.return_type |
| |
| @property |
| def params(self): |
| return self.signature.param_list |
| |
| |
| def NameIsTestOnly(name): |
| return name.endswith(('ForTest', 'ForTests', 'ForTesting')) |
| |
| |
| def _MangleMethodName(type_resolver, name, param_types): |
| # E.g. java.util.List.reversed() has overloads that return different types. |
| if not param_types: |
| return name |
| mangled_types = [] |
| for java_type in param_types: |
| if java_type.primitive_name: |
| part = java_type.primitive_name |
| else: |
| part = type_resolver.contextualize(java_type.java_class).replace('.', '_') |
| mangled_types.append(part + ('Array' * java_type.array_dimensions)) |
| |
| return f'{name}__' + '__'.join(mangled_types) |
| |
| |
| def _AssignMethodIdFunctionNames(type_resolver, called_by_natives): |
| # Mangle names for overloads with different number of parameters. |
| def key(called_by_native): |
| return (called_by_native.java_class.full_name_with_slashes, |
| called_by_native.name, len(called_by_native.params)) |
| |
| method_counts = collections.Counter(key(x) for x in called_by_natives) |
| cbn_by_name = collections.defaultdict(list) |
| |
| for called_by_native in called_by_natives: |
| if called_by_native.is_constructor: |
| method_id_function_name = 'Constructor' |
| else: |
| method_id_function_name = called_by_native.name |
| |
| if method_counts[key(called_by_native)] > 1: |
| method_id_function_name = _MangleMethodName( |
| type_resolver, method_id_function_name, |
| called_by_native.signature.param_types) |
| cbn_by_name[method_id_function_name].append(called_by_native) |
| |
| called_by_native.method_id_function_name = method_id_function_name |
| |
| # E.g. java.util.List.reversed() has overloads that return different types. |
| for duplicates in cbn_by_name.values(): |
| for i, cbn in enumerate(duplicates[1:], 1): |
| cbn.method_id_function_name += str(i) |
| |
| |
| class JniObject: |
| """Uses the given java source file to generate the JNI header file.""" |
| |
| def __init__(self, |
| parsed_file, |
| *, |
| from_javap, |
| default_namespace=None, |
| javap_unchecked_exceptions=False): |
| self.filename = parsed_file.filename |
| self.type_resolver = parsed_file.type_resolver |
| self.module_name = parsed_file.module_name |
| self.proxy_interface = parsed_file.proxy_interface |
| self.proxy_visibility = parsed_file.proxy_visibility |
| self.constant_fields = parsed_file.constant_fields |
| |
| # These are different only for legacy reasons. |
| if from_javap: |
| self.jni_namespace = default_namespace or 'JNI_' + self.java_class.name.replace( |
| '$', '__') |
| else: |
| self.jni_namespace = parsed_file.jni_namespace or default_namespace |
| |
| natives = [ |
| NativeMethod(m, java_class=self.java_class, is_proxy=True) |
| for m in parsed_file.proxy_methods |
| ] |
| # Natives are already sorted by name, but we want ForTesting methods to |
| # come at the end so that they do not contribute to switch number ordering. |
| natives.sort(key=lambda n: n.is_test_only) |
| |
| natives.extend( |
| NativeMethod(m, java_class=self.java_class, is_proxy=False) |
| for m in parsed_file.non_proxy_methods) |
| |
| self.natives = natives |
| |
| called_by_natives = [] |
| for parsed_called_by_native in parsed_file.called_by_natives: |
| called_by_natives.append( |
| CalledByNative(parsed_called_by_native, |
| unchecked=from_javap and javap_unchecked_exceptions, |
| is_system_class=from_javap)) |
| |
| _AssignMethodIdFunctionNames(parsed_file.type_resolver, called_by_natives) |
| self.called_by_natives = called_by_natives |
| |
| @property |
| def java_class(self): |
| return self.type_resolver.java_class |
| |
| @property |
| def proxy_natives(self): |
| return [n for n in self.natives if n.is_proxy] |
| |
| @property |
| def non_proxy_natives(self): |
| return [n for n in self.natives if not n.is_proxy] |
| |
| def GetClassesToBeImported(self): |
| classes = set() |
| for n in self.proxy_natives: |
| for t in list(n.signature.param_types) + [n.return_type]: |
| class_obj = t.java_class |
| if class_obj is None: |
| # Primitive types will be None. |
| continue |
| if class_obj.full_name_with_slashes.startswith('java/lang/'): |
| # java.lang** are never imported. |
| continue |
| classes.add(class_obj) |
| |
| return sorted(classes) |
| |
| def RemoveTestOnlyNatives(self): |
| self.natives = [n for n in self.natives if not n.is_test_only] |
| |
| |
| def _CollectReferencedClasses(jni_obj): |
| ret = set() |
| # @CalledByNatives can appear on nested classes, so check each one. |
| for called_by_native in jni_obj.called_by_natives: |
| ret.add(called_by_native.java_class) |
| for param in called_by_native.params: |
| java_type = param.java_type |
| if java_type.is_object_array() and java_type.converted_type: |
| ret.add(java_type.java_class) |
| |
| |
| # Find any classes needed for @JniType conversions. |
| for native in jni_obj.proxy_natives: |
| return_type = native.return_type |
| if return_type.is_object_array() and return_type.converted_type: |
| ret.add(return_type.java_class) |
| return sorted(ret) |
| |
| |
| def _generate_header(jni_mode, |
| jni_obj, |
| gen_jni_class, |
| *, |
| enable_definition_macros, |
| include_path_prefix, |
| extra_includes=None, |
| add_natives_macro_definition=True): |
| user_includes = [f'{include_path_prefix}jni_zero_internal.h'] |
| if extra_includes: |
| user_includes += extra_includes |
| preamble, epilogue = header_common.header_preamble( |
| GetScriptName(), |
| jni_obj.java_class, |
| system_includes=['jni.h'], |
| user_includes=user_includes) |
| sb = common.StringBuilder() |
| sb(preamble) |
| |
| if add_natives_macro_definition: |
| natives_header.natives_macro_definition( |
| sb, |
| jni_mode, |
| jni_obj, |
| gen_jni_class, |
| enable_definition_macros=enable_definition_macros) |
| |
| java_classes = _CollectReferencedClasses(jni_obj) |
| if java_classes: |
| with sb.section('Class Accessors'): |
| header_common.class_accessors(sb, java_classes, jni_obj.module_name) |
| |
| with sb.namespace(jni_obj.jni_namespace): |
| if jni_obj.constant_fields: |
| with sb.section('Constants'): |
| called_by_native_header.constants_enums(sb, jni_obj.java_class, |
| jni_obj.constant_fields) |
| |
| if jni_obj.natives and not enable_definition_macros: |
| with sb.section('Java to native functions'): |
| for native in jni_obj.natives: |
| natives_header.entry_point_method(sb, |
| jni_mode, |
| jni_obj, |
| native, |
| gen_jni_class, |
| include_forward_declaration=True) |
| |
| if jni_obj.called_by_natives: |
| with sb.section('Native to Java functions'): |
| for called_by_native in jni_obj.called_by_natives: |
| called_by_native_header.method_definition(sb, called_by_native) |
| |
| sb(epilogue) |
| return sb.to_string() |
| |
| |
| def GetScriptName(): |
| return '//third_party/jni_zero/jni_zero.py' |
| |
| |
| def _RemoveStaleHeaders(path, output_names): |
| if not os.path.isdir(path): |
| return |
| # Do not remove output files so that timestamps on declared outputs are not |
| # modified unless their contents are changed (avoids reverse deps needing to |
| # be rebuilt). |
| preserve = set(output_names) |
| for root, _, files in os.walk(path): |
| for f in files: |
| if f not in preserve: |
| file_path = os.path.join(root, f) |
| if os.path.isfile(file_path) and file_path.endswith('.h'): |
| os.remove(file_path) |
| |
| |
| def _CheckSameModule(jni_objs): |
| files_by_module = collections.defaultdict(list) |
| for jni_obj in jni_objs: |
| if jni_obj.proxy_natives: |
| files_by_module[jni_obj.module_name].append(jni_obj.filename) |
| if len(files_by_module) > 1: |
| sys.stderr.write( |
| 'Multiple values for @NativeMethods(moduleName) is not supported.\n') |
| for module_name, filenames in files_by_module.items(): |
| sys.stderr.write(f'module_name={module_name}\n') |
| for filename in filenames: |
| sys.stderr.write(f' {filename}\n') |
| sys.exit(1) |
| return next(iter(files_by_module)) if files_by_module else None |
| |
| |
| def _CheckNotEmpty(jni_objs): |
| has_empty = False |
| for jni_obj in jni_objs: |
| if not (jni_obj.natives or jni_obj.called_by_natives): |
| has_empty = True |
| sys.stderr.write(f'No native methods found in {jni_obj.filename}.\n') |
| if has_empty: |
| sys.exit(1) |
| |
| |
| def _RunJavap(javap_path, class_file): |
| p = subprocess.run([javap_path, '-s', '-constants', class_file], |
| text=True, |
| capture_output=True, |
| check=True) |
| return p.stdout |
| |
| |
| def _ParseClassFiles(jar_file, class_files, args): |
| # Parse javap output. |
| ret = [] |
| with tempfile.TemporaryDirectory() as temp_dir: |
| with zipfile.ZipFile(jar_file) as z: |
| z.extractall(temp_dir, class_files) |
| for class_file in class_files: |
| class_file = os.path.join(temp_dir, class_file) |
| contents = _RunJavap(args.javap, class_file) |
| parsed_file = parse.parse_javap(class_file, contents) |
| ret.append( |
| JniObject(parsed_file, |
| from_javap=True, |
| default_namespace=args.namespace, |
| javap_unchecked_exceptions=args.unchecked_exceptions)) |
| return ret |
| |
| |
| def _CreateSrcJar(srcjar_path, jni_mode, gen_jni_class, jni_objs, *, |
| script_name): |
| with common.atomic_output(srcjar_path) as f: |
| with zipfile.ZipFile(f, 'w') as srcjar: |
| for jni_obj in jni_objs: |
| if not jni_obj.proxy_natives: |
| continue |
| content = proxy_impl_java.Generate(jni_mode, |
| jni_obj, |
| gen_jni_class=gen_jni_class, |
| script_name=script_name) |
| zip_path = f'{jni_obj.java_class.class_without_prefix.full_name_with_slashes}Jni.java' |
| common.add_to_zip_hermetic(srcjar, zip_path, data=content) |
| |
| if not jni_mode.is_per_file: |
| content = placeholder_gen_jni_java.Generate(jni_objs, |
| gen_jni_class=gen_jni_class, |
| script_name=script_name) |
| zip_path = f'{gen_jni_class.full_name_with_slashes}.java' |
| common.add_to_zip_hermetic(srcjar, zip_path, data=content) |
| |
| |
| def _CreatePlaceholderSrcJar(srcjar_path, jni_objs, *, script_name): |
| already_added = set() |
| with common.atomic_output(srcjar_path) as f: |
| with zipfile.ZipFile(f, 'w') as srcjar: |
| for jni_obj in jni_objs: |
| if not jni_obj.proxy_natives: |
| continue |
| main_class = jni_obj.type_resolver.java_class |
| zip_path = main_class.class_without_prefix.full_name_with_slashes + '.java' |
| content = placeholder_java_type.Generate( |
| main_class, |
| jni_obj.type_resolver.nested_classes, |
| script_name=script_name, |
| proxy_interface=jni_obj.proxy_interface, |
| proxy_natives=jni_obj.proxy_natives) |
| common.add_to_zip_hermetic(srcjar, zip_path, data=content) |
| already_added.add(zip_path) |
| # In rare circumstances, another file in our generate_jni list will |
| # import the FooJni from another class within the same generate_jni |
| # target. We want to make sure we don't make placeholders for these, but |
| # we do want placeholders for all BarJni classes that aren't a part of |
| # this generate_jni. |
| fake_zip_path = main_class.class_without_prefix.full_name_with_slashes + 'Jni.java' |
| already_added.add(fake_zip_path) |
| |
| placeholders = collections.defaultdict(list) |
| # Doing this in 2 phases to ensure that the Jni classes (the ones that |
| # can have @NativeMethods) all get added first, so we don't accidentally |
| # write a stubbed version of the class if it's imported by another class. |
| for jni_obj in jni_objs: |
| for java_class in jni_obj.GetClassesToBeImported(): |
| if java_class.full_name_with_slashes.startswith('java/'): |
| continue |
| # TODO(mheikal): handle more than 1 nesting layer. |
| if java_class.is_nested(): |
| placeholders[java_class.get_outer_class()].append(java_class) |
| elif java_class not in placeholders: |
| placeholders[java_class] = [] |
| for java_class, nested_classes in placeholders.items(): |
| zip_path = java_class.class_without_prefix.full_name_with_slashes + '.java' |
| if zip_path not in already_added: |
| content = placeholder_java_type.Generate(java_class, |
| nested_classes, |
| script_name=script_name) |
| common.add_to_zip_hermetic(srcjar, zip_path, data=content) |
| already_added.add(zip_path) |
| |
| |
| def _WriteHeaders(jni_mode, |
| jni_objs, |
| output_names, |
| output_dir, |
| *, |
| include_path_prefix, |
| gen_jni_class=None, |
| enable_definition_macros=False, |
| extra_includes=None, |
| add_natives_macro_definition=True): |
| for jni_obj, header_name in zip(jni_objs, output_names): |
| output_file = os.path.join(output_dir, header_name) |
| content = _generate_header( |
| jni_mode, |
| jni_obj, |
| gen_jni_class, |
| enable_definition_macros=enable_definition_macros, |
| include_path_prefix=include_path_prefix, |
| extra_includes=extra_includes, |
| add_natives_macro_definition=add_natives_macro_definition) |
| |
| with common.atomic_output(output_file, 'w') as f: |
| f.write(content) |
| |
| |
| def GenerateFromSource(parser, args, jni_mode): |
| # Remove existing headers so that moving .java source files but not updating |
| # the corresponding C++ include will be a compile failure (otherwise |
| # incremental builds will usually not catch this). |
| _RemoveStaleHeaders(args.output_dir, args.output_names) |
| |
| try: |
| parsed_files = [ |
| parse.parse_java_file(f, |
| package_prefix=args.package_prefix, |
| package_prefix_filter=args.package_prefix_filter, |
| enable_legacy_natives=args.enable_legacy_natives, |
| allow_private_called_by_natives=args. |
| allow_private_called_by_natives) |
| for f in args.input_files |
| ] |
| jni_objs = [ |
| JniObject(x, from_javap=False, default_namespace=args.namespace) |
| for x in parsed_files |
| ] |
| _CheckNotEmpty(jni_objs) |
| module_name = _CheckSameModule(jni_objs) |
| except parse.ParseError as e: |
| sys.stderr.write(f'{e}\n') |
| sys.exit(1) |
| |
| gen_jni_class = proxy.get_gen_jni_class( |
| short=jni_mode.is_hashing or jni_mode.is_muxing, |
| name_prefix=args.module_name or module_name, |
| package_prefix=args.package_prefix, |
| package_prefix_filter=args.package_prefix_filter) |
| |
| _WriteHeaders(jni_mode, |
| jni_objs, |
| args.output_names, |
| args.output_dir, |
| include_path_prefix=args.include_path_prefix, |
| gen_jni_class=gen_jni_class, |
| enable_definition_macros=args.enable_definition_macros, |
| extra_includes=args.extra_includes) |
| |
| jni_objs_with_proxy_natives = [x for x in jni_objs if x.proxy_natives] |
| # Write .srcjar |
| if args.srcjar_path: |
| if jni_objs_with_proxy_natives: |
| gen_jni_class = proxy.get_gen_jni_class( |
| short=False, |
| name_prefix=jni_objs_with_proxy_natives[0].module_name, |
| package_prefix=args.package_prefix, |
| package_prefix_filter=args.package_prefix_filter) |
| _CreateSrcJar(args.srcjar_path, |
| jni_mode, |
| gen_jni_class, |
| jni_objs_with_proxy_natives, |
| script_name=GetScriptName()) |
| else: |
| # Only @CalledByNatives. |
| zipfile.ZipFile(args.srcjar_path, 'w').close() |
| if args.jni_pickle: |
| with common.atomic_output(args.jni_pickle, 'wb') as f: |
| pickle.dump(parsed_files, f) |
| |
| if args.placeholder_srcjar_path: |
| if jni_objs_with_proxy_natives: |
| _CreatePlaceholderSrcJar(args.placeholder_srcjar_path, |
| jni_objs_with_proxy_natives, |
| script_name=GetScriptName()) |
| else: |
| zipfile.ZipFile(args.placeholder_srcjar_path, 'w').close() |
| |
| |
| def GenerateFromJar(parser, args, jni_mode): |
| if not args.javap: |
| args.javap = shutil.which('javap') |
| if not args.javap: |
| parser.error('Could not find "javap" on your PATH. Use --javap to ' |
| 'specify its location.') |
| |
| # Remove existing headers so that moving .java source files but not updating |
| # the corresponding C++ include will be a compile failure (otherwise |
| # incremental builds will usually not catch this). |
| _RemoveStaleHeaders(args.output_dir, args.output_names) |
| |
| try: |
| jni_objs = _ParseClassFiles(args.jar_file, args.input_files, args) |
| except parse.ParseError as e: |
| sys.stderr.write(f'{e}\n') |
| sys.exit(1) |
| |
| _WriteHeaders(jni_mode, |
| jni_objs, |
| args.output_names, |
| args.output_dir, |
| include_path_prefix=args.include_path_prefix, |
| extra_includes=args.extra_includes, |
| add_natives_macro_definition=False) |