• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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