1#!/usr/bin/env python
2# Copyright (c) 2012 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
6"""Extracts native methods from a Java file and generates the JNI bindings.
7If you change this, please run and update the tests."""
8
9import collections
10import errno
11import optparse
12import os
13import re
14import string
15from string import Template
16import subprocess
17import sys
18import textwrap
19import zipfile
20
21
22class ParseError(Exception):
23  """Exception thrown when we can't parse the input file."""
24
25  def __init__(self, description, *context_lines):
26    Exception.__init__(self)
27    self.description = description
28    self.context_lines = context_lines
29
30  def __str__(self):
31    context = '\n'.join(self.context_lines)
32    return '***\nERROR: %s\n\n%s\n***' % (self.description, context)
33
34
35class Param(object):
36  """Describes a param for a method, either java or native."""
37
38  def __init__(self, **kwargs):
39    self.datatype = kwargs['datatype']
40    self.name = kwargs['name']
41
42
43class NativeMethod(object):
44  """Describes a C/C++ method that is called by Java code"""
45
46  def __init__(self, **kwargs):
47    self.static = kwargs['static']
48    self.java_class_name = kwargs['java_class_name']
49    self.return_type = kwargs['return_type']
50    self.name = kwargs['name']
51    self.params = kwargs['params']
52    if self.params:
53      assert type(self.params) is list
54      assert type(self.params[0]) is Param
55    if (self.params and
56        self.params[0].datatype == 'int' and
57        self.params[0].name.startswith('native')):
58      self.type = 'method'
59      self.p0_type = self.params[0].name[len('native'):]
60      if kwargs.get('native_class_name'):
61        self.p0_type = kwargs['native_class_name']
62    else:
63      self.type = 'function'
64    self.method_id_var_name = kwargs.get('method_id_var_name', None)
65
66
67class CalledByNative(object):
68  """Describes a java method exported to c/c++"""
69
70  def __init__(self, **kwargs):
71    self.system_class = kwargs['system_class']
72    self.unchecked = kwargs['unchecked']
73    self.static = kwargs['static']
74    self.java_class_name = kwargs['java_class_name']
75    self.return_type = kwargs['return_type']
76    self.name = kwargs['name']
77    self.params = kwargs['params']
78    self.method_id_var_name = kwargs.get('method_id_var_name', None)
79    self.is_constructor = kwargs.get('is_constructor', False)
80    self.env_call = GetEnvCall(self.is_constructor, self.static,
81                               self.return_type)
82    self.static_cast = GetStaticCastForReturnType(self.return_type)
83
84
85def JavaDataTypeToC(java_type):
86  """Returns a C datatype for the given java type."""
87  java_pod_type_map = {
88      'int': 'jint',
89      'byte': 'jbyte',
90      'char': 'jchar',
91      'short': 'jshort',
92      'boolean': 'jboolean',
93      'long': 'jlong',
94      'double': 'jdouble',
95      'float': 'jfloat',
96  }
97  java_type_map = {
98      'void': 'void',
99      'String': 'jstring',
100      'java/lang/String': 'jstring',
101      'Class': 'jclass',
102      'java/lang/Class': 'jclass',
103  }
104
105  if java_type in java_pod_type_map:
106    return java_pod_type_map[java_type]
107  elif java_type in java_type_map:
108    return java_type_map[java_type]
109  elif java_type.endswith('[]'):
110    if java_type[:-2] in java_pod_type_map:
111      return java_pod_type_map[java_type[:-2]] + 'Array'
112    return 'jobjectArray'
113  else:
114    return 'jobject'
115
116
117class JniParams(object):
118  _imports = []
119  _fully_qualified_class = ''
120  _package = ''
121  _inner_classes = []
122  _remappings = []
123
124  @staticmethod
125  def SetFullyQualifiedClass(fully_qualified_class):
126    JniParams._fully_qualified_class = 'L' + fully_qualified_class
127    JniParams._package = '/'.join(fully_qualified_class.split('/')[:-1])
128
129  @staticmethod
130  def ExtractImportsAndInnerClasses(contents):
131    contents = contents.replace('\n', '')
132    re_import = re.compile(r'import.*?(?P<class>\S*?);')
133    for match in re.finditer(re_import, contents):
134      JniParams._imports += ['L' + match.group('class').replace('.', '/')]
135
136    re_inner = re.compile(r'(class|interface)\s+?(?P<name>\w+?)\W')
137    for match in re.finditer(re_inner, contents):
138      inner = match.group('name')
139      if not JniParams._fully_qualified_class.endswith(inner):
140        JniParams._inner_classes += [JniParams._fully_qualified_class + '$' +
141                                     inner]
142
143  @staticmethod
144  def JavaToJni(param):
145    """Converts a java param into a JNI signature type."""
146    pod_param_map = {
147        'int': 'I',
148        'boolean': 'Z',
149        'char': 'C',
150        'short': 'S',
151        'long': 'J',
152        'double': 'D',
153        'float': 'F',
154        'byte': 'B',
155        'void': 'V',
156    }
157    object_param_list = [
158        'Ljava/lang/Boolean',
159        'Ljava/lang/Integer',
160        'Ljava/lang/Long',
161        'Ljava/lang/Object',
162        'Ljava/lang/String',
163        'Ljava/lang/Class',
164    ]
165    prefix = ''
166    # Array?
167    while param[-2:] == '[]':
168      prefix += '['
169      param = param[:-2]
170    # Generic?
171    if '<' in param:
172      param = param[:param.index('<')]
173    if param in pod_param_map:
174      return prefix + pod_param_map[param]
175    if '/' in param:
176      # Coming from javap, use the fully qualified param directly.
177      return prefix + 'L' + JniParams.RemapClassName(param) + ';'
178    for qualified_name in (object_param_list +
179                           [JniParams._fully_qualified_class] +
180                           JniParams._inner_classes):
181      if (qualified_name.endswith('/' + param) or
182          qualified_name.endswith('$' + param.replace('.', '$')) or
183          qualified_name == 'L' + param):
184        return prefix + JniParams.RemapClassName(qualified_name) + ';'
185
186    # Is it from an import? (e.g. referecing Class from import pkg.Class;
187    # note that referencing an inner class Inner from import pkg.Class.Inner
188    # is not supported).
189    for qualified_name in JniParams._imports:
190      if qualified_name.endswith('/' + param):
191        # Ensure it's not an inner class.
192        components = qualified_name.split('/')
193        if len(components) > 2 and components[-2][0].isupper():
194          raise SyntaxError('Inner class (%s) can not be imported '
195                            'and used by JNI (%s). Please import the outer '
196                            'class and use Outer.Inner instead.' %
197                            (qualified_name, param))
198        return prefix + JniParams.RemapClassName(qualified_name) + ';'
199
200    # Is it an inner class from an outer class import? (e.g. referencing
201    # Class.Inner from import pkg.Class).
202    if '.' in param:
203      components = param.split('.')
204      outer = '/'.join(components[:-1])
205      inner = components[-1]
206      for qualified_name in JniParams._imports:
207        if qualified_name.endswith('/' + outer):
208          return (prefix + JniParams.RemapClassName(qualified_name) +
209                  '$' + inner + ';')
210
211    # Type not found, falling back to same package as this class.
212    return (prefix + 'L' +
213            JniParams.RemapClassName(JniParams._package + '/' + param) + ';')
214
215  @staticmethod
216  def Signature(params, returns, wrap):
217    """Returns the JNI signature for the given datatypes."""
218    items = ['(']
219    items += [JniParams.JavaToJni(param.datatype) for param in params]
220    items += [')']
221    items += [JniParams.JavaToJni(returns)]
222    if wrap:
223      return '\n' + '\n'.join(['"' + item + '"' for item in items])
224    else:
225      return '"' + ''.join(items) + '"'
226
227  @staticmethod
228  def Parse(params):
229    """Parses the params into a list of Param objects."""
230    if not params:
231      return []
232    ret = []
233    for p in [p.strip() for p in params.split(',')]:
234      items = p.split(' ')
235      if 'final' in items:
236        items.remove('final')
237      param = Param(
238          datatype=items[0],
239          name=(items[1] if len(items) > 1 else 'p%s' % len(ret)),
240      )
241      ret += [param]
242    return ret
243
244  @staticmethod
245  def RemapClassName(class_name):
246    """Remaps class names using the jarjar mapping table."""
247    for old, new in JniParams._remappings:
248      if old in class_name:
249        return class_name.replace(old, new, 1)
250    return class_name
251
252  @staticmethod
253  def SetJarJarMappings(mappings):
254    """Parse jarjar mappings from a string."""
255    JniParams._remappings = []
256    for line in mappings.splitlines():
257      keyword, src, dest = line.split()
258      if keyword != 'rule':
259        continue
260      assert src.endswith('.**')
261      src = src[:-2].replace('.', '/')
262      dest = dest.replace('.', '/')
263      if dest.endswith('@0'):
264        JniParams._remappings.append((src, dest[:-2] + src))
265      else:
266        assert dest.endswith('@1')
267        JniParams._remappings.append((src, dest[:-2]))
268
269
270def ExtractJNINamespace(contents):
271  re_jni_namespace = re.compile('.*?@JNINamespace\("(.*?)"\)')
272  m = re.findall(re_jni_namespace, contents)
273  if not m:
274    return ''
275  return m[0]
276
277
278def ExtractFullyQualifiedJavaClassName(java_file_name, contents):
279  re_package = re.compile('.*?package (.*?);')
280  matches = re.findall(re_package, contents)
281  if not matches:
282    raise SyntaxError('Unable to find "package" line in %s' % java_file_name)
283  return (matches[0].replace('.', '/') + '/' +
284          os.path.splitext(os.path.basename(java_file_name))[0])
285
286
287def ExtractNatives(contents):
288  """Returns a list of dict containing information about a native method."""
289  contents = contents.replace('\n', '')
290  natives = []
291  re_native = re.compile(r'(@NativeClassQualifiedName'
292                         '\(\"(?P<native_class_name>.*?)\"\))?\s*'
293                         '(@NativeCall(\(\"(?P<java_class_name>.*?)\"\)))?\s*'
294                         '(?P<qualifiers>\w+\s\w+|\w+|\s+)\s*?native '
295                         '(?P<return_type>\S*?) '
296                         '(?P<name>\w+?)\((?P<params>.*?)\);')
297  for match in re.finditer(re_native, contents):
298    native = NativeMethod(
299        static='static' in match.group('qualifiers'),
300        java_class_name=match.group('java_class_name'),
301        native_class_name=match.group('native_class_name'),
302        return_type=match.group('return_type'),
303        name=match.group('name').replace('native', ''),
304        params=JniParams.Parse(match.group('params')))
305    natives += [native]
306  return natives
307
308
309def GetStaticCastForReturnType(return_type):
310  type_map = { 'String' : 'jstring',
311               'java/lang/String' : 'jstring',
312               'boolean[]': 'jbooleanArray',
313               'byte[]': 'jbyteArray',
314               'char[]': 'jcharArray',
315               'short[]': 'jshortArray',
316               'int[]': 'jintArray',
317               'long[]': 'jlongArray',
318               'double[]': 'jdoubleArray' }
319  ret = type_map.get(return_type, None)
320  if ret:
321    return ret
322  if return_type.endswith('[]'):
323    return 'jobjectArray'
324  return None
325
326
327def GetEnvCall(is_constructor, is_static, return_type):
328  """Maps the types availabe via env->Call__Method."""
329  if is_constructor:
330    return 'NewObject'
331  env_call_map = {'boolean': 'Boolean',
332                  'byte': 'Byte',
333                  'char': 'Char',
334                  'short': 'Short',
335                  'int': 'Int',
336                  'long': 'Long',
337                  'float': 'Float',
338                  'void': 'Void',
339                  'double': 'Double',
340                  'Object': 'Object',
341                 }
342  call = env_call_map.get(return_type, 'Object')
343  if is_static:
344    call = 'Static' + call
345  return 'Call' + call + 'Method'
346
347
348def GetMangledParam(datatype):
349  """Returns a mangled identifier for the datatype."""
350  if len(datatype) <= 2:
351    return datatype.replace('[', 'A')
352  ret = ''
353  for i in range(1, len(datatype)):
354    c = datatype[i]
355    if c == '[':
356      ret += 'A'
357    elif c.isupper() or datatype[i - 1] in ['/', 'L']:
358      ret += c.upper()
359  return ret
360
361
362def GetMangledMethodName(name, params, return_type):
363  """Returns a mangled method name for the given signature.
364
365     The returned name can be used as a C identifier and will be unique for all
366     valid overloads of the same method.
367
368  Args:
369     name: string.
370     params: list of Param.
371     return_type: string.
372
373  Returns:
374      A mangled name.
375  """
376  mangled_items = []
377  for datatype in [return_type] + [x.datatype for x in params]:
378    mangled_items += [GetMangledParam(JniParams.JavaToJni(datatype))]
379  mangled_name = name + '_'.join(mangled_items)
380  assert re.match(r'[0-9a-zA-Z_]+', mangled_name)
381  return mangled_name
382
383
384def MangleCalledByNatives(called_by_natives):
385  """Mangles all the overloads from the call_by_natives list."""
386  method_counts = collections.defaultdict(
387      lambda: collections.defaultdict(lambda: 0))
388  for called_by_native in called_by_natives:
389    java_class_name = called_by_native.java_class_name
390    name = called_by_native.name
391    method_counts[java_class_name][name] += 1
392  for called_by_native in called_by_natives:
393    java_class_name = called_by_native.java_class_name
394    method_name = called_by_native.name
395    method_id_var_name = method_name
396    if method_counts[java_class_name][method_name] > 1:
397      method_id_var_name = GetMangledMethodName(method_name,
398                                                called_by_native.params,
399                                                called_by_native.return_type)
400    called_by_native.method_id_var_name = method_id_var_name
401  return called_by_natives
402
403
404# Regex to match the JNI return types that should be included in a
405# ScopedJavaLocalRef.
406RE_SCOPED_JNI_RETURN_TYPES = re.compile('jobject|jclass|jstring|.*Array')
407
408# Regex to match a string like "@CalledByNative public void foo(int bar)".
409RE_CALLED_BY_NATIVE = re.compile(
410    '@CalledByNative(?P<Unchecked>(Unchecked)*?)(?:\("(?P<annotation>.*)"\))?'
411    '\s+(?P<prefix>[\w ]*?)'
412    '\s*(?P<return_type>\S+?)'
413    '\s+(?P<name>\w+)'
414    '\s*\((?P<params>[^\)]*)\)')
415
416
417def ExtractCalledByNatives(contents):
418  """Parses all methods annotated with @CalledByNative.
419
420  Args:
421    contents: the contents of the java file.
422
423  Returns:
424    A list of dict with information about the annotated methods.
425    TODO(bulach): return a CalledByNative object.
426
427  Raises:
428    ParseError: if unable to parse.
429  """
430  called_by_natives = []
431  for match in re.finditer(RE_CALLED_BY_NATIVE, contents):
432    called_by_natives += [CalledByNative(
433        system_class=False,
434        unchecked='Unchecked' in match.group('Unchecked'),
435        static='static' in match.group('prefix'),
436        java_class_name=match.group('annotation') or '',
437        return_type=match.group('return_type'),
438        name=match.group('name'),
439        params=JniParams.Parse(match.group('params')))]
440  # Check for any @CalledByNative occurrences that weren't matched.
441  unmatched_lines = re.sub(RE_CALLED_BY_NATIVE, '', contents).split('\n')
442  for line1, line2 in zip(unmatched_lines, unmatched_lines[1:]):
443    if '@CalledByNative' in line1:
444      raise ParseError('could not parse @CalledByNative method signature',
445                       line1, line2)
446  return MangleCalledByNatives(called_by_natives)
447
448
449class JNIFromJavaP(object):
450  """Uses 'javap' to parse a .class file and generate the JNI header file."""
451
452  def __init__(self, contents, namespace):
453    self.contents = contents
454    self.namespace = namespace
455    self.fully_qualified_class = re.match(
456        '.*?(class|interface) (?P<class_name>.*?)( |{)',
457        contents[1]).group('class_name')
458    self.fully_qualified_class = self.fully_qualified_class.replace('.', '/')
459    JniParams.SetFullyQualifiedClass(self.fully_qualified_class)
460    self.java_class_name = self.fully_qualified_class.split('/')[-1]
461    if not self.namespace:
462      self.namespace = 'JNI_' + self.java_class_name
463    re_method = re.compile('(?P<prefix>.*?)(?P<return_type>\S+?) (?P<name>\w+?)'
464                           '\((?P<params>.*?)\)')
465    self.called_by_natives = []
466    for content in contents[2:]:
467      match = re.match(re_method, content)
468      if not match:
469        continue
470      self.called_by_natives += [CalledByNative(
471          system_class=True,
472          unchecked=False,
473          static='static' in match.group('prefix'),
474          java_class_name='',
475          return_type=match.group('return_type').replace('.', '/'),
476          name=match.group('name'),
477          params=JniParams.Parse(match.group('params').replace('.', '/')))]
478    re_constructor = re.compile('.*? public ' +
479                                self.fully_qualified_class.replace('/', '.') +
480                                '\((?P<params>.*?)\)')
481    for content in contents[2:]:
482      match = re.match(re_constructor, content)
483      if not match:
484        continue
485      self.called_by_natives += [CalledByNative(
486          system_class=True,
487          unchecked=False,
488          static=False,
489          java_class_name='',
490          return_type=self.fully_qualified_class,
491          name='Constructor',
492          params=JniParams.Parse(match.group('params').replace('.', '/')),
493          is_constructor=True)]
494    self.called_by_natives = MangleCalledByNatives(self.called_by_natives)
495    self.inl_header_file_generator = InlHeaderFileGenerator(
496        self.namespace, self.fully_qualified_class, [], self.called_by_natives)
497
498  def GetContent(self):
499    return self.inl_header_file_generator.GetContent()
500
501  @staticmethod
502  def CreateFromClass(class_file, namespace):
503    class_name = os.path.splitext(os.path.basename(class_file))[0]
504    p = subprocess.Popen(args=['javap', class_name],
505                         cwd=os.path.dirname(class_file),
506                         stdout=subprocess.PIPE,
507                         stderr=subprocess.PIPE)
508    stdout, _ = p.communicate()
509    jni_from_javap = JNIFromJavaP(stdout.split('\n'), namespace)
510    return jni_from_javap
511
512
513class JNIFromJavaSource(object):
514  """Uses the given java source file to generate the JNI header file."""
515
516  def __init__(self, contents, fully_qualified_class):
517    contents = self._RemoveComments(contents)
518    JniParams.SetFullyQualifiedClass(fully_qualified_class)
519    JniParams.ExtractImportsAndInnerClasses(contents)
520    jni_namespace = ExtractJNINamespace(contents)
521    natives = ExtractNatives(contents)
522    called_by_natives = ExtractCalledByNatives(contents)
523    if len(natives) == 0 and len(called_by_natives) == 0:
524      raise SyntaxError('Unable to find any JNI methods for %s.' %
525                        fully_qualified_class)
526    inl_header_file_generator = InlHeaderFileGenerator(
527        jni_namespace, fully_qualified_class, natives, called_by_natives)
528    self.content = inl_header_file_generator.GetContent()
529
530  def _RemoveComments(self, contents):
531    # We need to support both inline and block comments, and we need to handle
532    # strings that contain '//' or '/*'. Rather than trying to do all that with
533    # regexps, we just pipe the contents through the C preprocessor. We tell cpp
534    # the file has already been preprocessed, so it just removes comments and
535    # doesn't try to parse #include, #pragma etc.
536    #
537    # TODO(husky): This is a bit hacky. It would be cleaner to use a real Java
538    # parser. Maybe we could ditch JNIFromJavaSource and just always use
539    # JNIFromJavaP; or maybe we could rewrite this script in Java and use APT.
540    # http://code.google.com/p/chromium/issues/detail?id=138941
541    p = subprocess.Popen(args=['cpp', '-fpreprocessed'],
542                         stdin=subprocess.PIPE,
543                         stdout=subprocess.PIPE,
544                         stderr=subprocess.PIPE)
545    stdout, _ = p.communicate(contents)
546    return stdout
547
548  def GetContent(self):
549    return self.content
550
551  @staticmethod
552  def CreateFromFile(java_file_name):
553    contents = file(java_file_name).read()
554    fully_qualified_class = ExtractFullyQualifiedJavaClassName(java_file_name,
555                                                               contents)
556    return JNIFromJavaSource(contents, fully_qualified_class)
557
558
559class InlHeaderFileGenerator(object):
560  """Generates an inline header file for JNI integration."""
561
562  def __init__(self, namespace, fully_qualified_class, natives,
563               called_by_natives):
564    self.namespace = namespace
565    self.fully_qualified_class = fully_qualified_class
566    self.class_name = self.fully_qualified_class.split('/')[-1]
567    self.natives = natives
568    self.called_by_natives = called_by_natives
569    self.header_guard = fully_qualified_class.replace('/', '_') + '_JNI'
570
571  def GetContent(self):
572    """Returns the content of the JNI binding file."""
573    template = Template("""\
574// Copyright (c) 2012 The Chromium Authors. All rights reserved.
575// Use of this source code is governed by a BSD-style license that can be
576// found in the LICENSE file.
577
578
579// This file is autogenerated by
580//     ${SCRIPT_NAME}
581// For
582//     ${FULLY_QUALIFIED_CLASS}
583
584#ifndef ${HEADER_GUARD}
585#define ${HEADER_GUARD}
586
587#include <jni.h>
588
589#include "base/android/jni_android.h"
590#include "base/android/scoped_java_ref.h"
591#include "base/basictypes.h"
592#include "base/logging.h"
593
594using base::android::ScopedJavaLocalRef;
595
596// Step 1: forward declarations.
597namespace {
598$CLASS_PATH_DEFINITIONS
599}  // namespace
600
601$OPEN_NAMESPACE
602$FORWARD_DECLARATIONS
603
604// Step 2: method stubs.
605$METHOD_STUBS
606
607// Step 3: RegisterNatives.
608
609static bool RegisterNativesImpl(JNIEnv* env) {
610$REGISTER_NATIVES_IMPL
611  return true;
612}
613$CLOSE_NAMESPACE
614#endif  // ${HEADER_GUARD}
615""")
616    script_components = os.path.abspath(sys.argv[0]).split(os.path.sep)
617    base_index = script_components.index('base')
618    script_name = os.sep.join(script_components[base_index:])
619    values = {
620        'SCRIPT_NAME': script_name,
621        'FULLY_QUALIFIED_CLASS': self.fully_qualified_class,
622        'CLASS_PATH_DEFINITIONS': self.GetClassPathDefinitionsString(),
623        'FORWARD_DECLARATIONS': self.GetForwardDeclarationsString(),
624        'METHOD_STUBS': self.GetMethodStubsString(),
625        'OPEN_NAMESPACE': self.GetOpenNamespaceString(),
626        'REGISTER_NATIVES_IMPL': self.GetRegisterNativesImplString(),
627        'CLOSE_NAMESPACE': self.GetCloseNamespaceString(),
628        'HEADER_GUARD': self.header_guard,
629    }
630    return WrapOutput(template.substitute(values))
631
632  def GetClassPathDefinitionsString(self):
633    ret = []
634    ret += [self.GetClassPathDefinitions()]
635    return '\n'.join(ret)
636
637  def GetForwardDeclarationsString(self):
638    ret = []
639    for native in self.natives:
640      if native.type != 'method':
641        ret += [self.GetForwardDeclaration(native)]
642    return '\n'.join(ret)
643
644  def GetMethodStubsString(self):
645    ret = []
646    for native in self.natives:
647      if native.type == 'method':
648        ret += [self.GetNativeMethodStub(native)]
649    for called_by_native in self.called_by_natives:
650      ret += [self.GetCalledByNativeMethodStub(called_by_native)]
651    return '\n'.join(ret)
652
653  def GetKMethodsString(self, clazz):
654    ret = []
655    for native in self.natives:
656      if (native.java_class_name == clazz or
657          (not native.java_class_name and clazz == self.class_name)):
658        ret += [self.GetKMethodArrayEntry(native)]
659    return '\n'.join(ret)
660
661  def GetRegisterNativesImplString(self):
662    """Returns the implementation for RegisterNatives."""
663    template = Template("""\
664  static const JNINativeMethod kMethods${JAVA_CLASS}[] = {
665${KMETHODS}
666  };
667  const int kMethods${JAVA_CLASS}Size = arraysize(kMethods${JAVA_CLASS});
668
669  if (env->RegisterNatives(g_${JAVA_CLASS}_clazz,
670                           kMethods${JAVA_CLASS},
671                           kMethods${JAVA_CLASS}Size) < 0) {
672    LOG(ERROR) << "RegisterNatives failed in " << __FILE__;
673    return false;
674  }
675""")
676    ret = [self.GetFindClasses()]
677    all_classes = self.GetUniqueClasses(self.natives)
678    all_classes[self.class_name] = self.fully_qualified_class
679    for clazz in all_classes:
680      kmethods = self.GetKMethodsString(clazz)
681      if kmethods:
682        values = {'JAVA_CLASS': clazz,
683                  'KMETHODS': kmethods}
684        ret += [template.substitute(values)]
685    if not ret: return ''
686    return '\n' + '\n'.join(ret)
687
688  def GetOpenNamespaceString(self):
689    if self.namespace:
690      all_namespaces = ['namespace %s {' % ns
691                        for ns in self.namespace.split('::')]
692      return '\n'.join(all_namespaces)
693    return ''
694
695  def GetCloseNamespaceString(self):
696    if self.namespace:
697      all_namespaces = ['}  // namespace %s' % ns
698                        for ns in self.namespace.split('::')]
699      all_namespaces.reverse()
700      return '\n'.join(all_namespaces) + '\n'
701    return ''
702
703  def GetJNIFirstParam(self, native):
704    ret = []
705    if native.type == 'method':
706      ret = ['jobject obj']
707    elif native.type == 'function':
708      if native.static:
709        ret = ['jclass clazz']
710      else:
711        ret = ['jobject obj']
712    return ret
713
714  def GetParamsInDeclaration(self, native):
715    """Returns the params for the stub declaration.
716
717    Args:
718      native: the native dictionary describing the method.
719
720    Returns:
721      A string containing the params.
722    """
723    return ',\n    '.join(self.GetJNIFirstParam(native) +
724                          [JavaDataTypeToC(param.datatype) + ' ' +
725                           param.name
726                           for param in native.params])
727
728  def GetCalledByNativeParamsInDeclaration(self, called_by_native):
729    return ',\n    '.join([JavaDataTypeToC(param.datatype) + ' ' +
730                           param.name
731                           for param in called_by_native.params])
732
733  def GetForwardDeclaration(self, native):
734    template = Template("""
735static ${RETURN} ${NAME}(JNIEnv* env, ${PARAMS});
736""")
737    values = {'RETURN': JavaDataTypeToC(native.return_type),
738              'NAME': native.name,
739              'PARAMS': self.GetParamsInDeclaration(native)}
740    return template.substitute(values)
741
742  def GetNativeMethodStub(self, native):
743    """Returns stubs for native methods."""
744    template = Template("""\
745static ${RETURN} ${NAME}(JNIEnv* env, ${PARAMS_IN_DECLARATION}) {
746  DCHECK(${PARAM0_NAME}) << "${NAME}";
747  ${P0_TYPE}* native = reinterpret_cast<${P0_TYPE}*>(${PARAM0_NAME});
748  return native->${NAME}(env, obj${PARAMS_IN_CALL})${POST_CALL};
749}
750""")
751    params_for_call = ', '.join(p.name for p in native.params[1:])
752    if params_for_call:
753      params_for_call = ', ' + params_for_call
754
755    return_type = JavaDataTypeToC(native.return_type)
756    if re.match(RE_SCOPED_JNI_RETURN_TYPES, return_type):
757      scoped_return_type = 'ScopedJavaLocalRef<' + return_type + '>'
758      post_call = '.Release()'
759    else:
760      scoped_return_type = return_type
761      post_call = ''
762    values = {
763        'RETURN': return_type,
764        'SCOPED_RETURN': scoped_return_type,
765        'NAME': native.name,
766        'PARAMS_IN_DECLARATION': self.GetParamsInDeclaration(native),
767        'PARAM0_NAME': native.params[0].name,
768        'P0_TYPE': native.p0_type,
769        'PARAMS_IN_CALL': params_for_call,
770        'POST_CALL': post_call
771    }
772    return template.substitute(values)
773
774  def GetCalledByNativeMethodStub(self, called_by_native):
775    """Returns a string."""
776    function_signature_template = Template("""\
777static ${RETURN_TYPE} Java_${JAVA_CLASS}_${METHOD_ID_VAR_NAME}(\
778JNIEnv* env${FIRST_PARAM_IN_DECLARATION}${PARAMS_IN_DECLARATION})""")
779    function_header_template = Template("""\
780${FUNCTION_SIGNATURE} {""")
781    function_header_with_unused_template = Template("""\
782${FUNCTION_SIGNATURE} __attribute__ ((unused));
783${FUNCTION_SIGNATURE} {""")
784    template = Template("""
785static base::subtle::AtomicWord g_${JAVA_CLASS}_${METHOD_ID_VAR_NAME} = 0;
786${FUNCTION_HEADER}
787  /* Must call RegisterNativesImpl()  */
788  DCHECK(g_${JAVA_CLASS}_clazz);
789  jmethodID method_id =
790    ${GET_METHOD_ID_IMPL}
791  ${RETURN_DECLARATION}
792  ${PRE_CALL}env->${ENV_CALL}(${FIRST_PARAM_IN_CALL},
793      method_id${PARAMS_IN_CALL})${POST_CALL};
794  ${CHECK_EXCEPTION}
795  ${RETURN_CLAUSE}
796}""")
797    if called_by_native.static or called_by_native.is_constructor:
798      first_param_in_declaration = ''
799      first_param_in_call = ('g_%s_clazz' %
800                             (called_by_native.java_class_name or
801                              self.class_name))
802    else:
803      first_param_in_declaration = ', jobject obj'
804      first_param_in_call = 'obj'
805    params_in_declaration = self.GetCalledByNativeParamsInDeclaration(
806        called_by_native)
807    if params_in_declaration:
808      params_in_declaration = ', ' + params_in_declaration
809    params_for_call = ', '.join(param.name
810                                for param in called_by_native.params)
811    if params_for_call:
812      params_for_call = ', ' + params_for_call
813    pre_call = ''
814    post_call = ''
815    if called_by_native.static_cast:
816      pre_call = 'static_cast<%s>(' % called_by_native.static_cast
817      post_call = ')'
818    check_exception = ''
819    if not called_by_native.unchecked:
820      check_exception = 'base::android::CheckException(env);'
821    return_type = JavaDataTypeToC(called_by_native.return_type)
822    return_declaration = ''
823    return_clause = ''
824    if return_type != 'void':
825      pre_call = '  ' + pre_call
826      return_declaration = return_type + ' ret ='
827      if re.match(RE_SCOPED_JNI_RETURN_TYPES, return_type):
828        return_type = 'ScopedJavaLocalRef<' + return_type + '>'
829        return_clause = 'return ' + return_type + '(env, ret);'
830      else:
831        return_clause = 'return ret;'
832    values = {
833        'JAVA_CLASS': called_by_native.java_class_name or self.class_name,
834        'METHOD': called_by_native.name,
835        'RETURN_TYPE': return_type,
836        'RETURN_DECLARATION': return_declaration,
837        'RETURN_CLAUSE': return_clause,
838        'FIRST_PARAM_IN_DECLARATION': first_param_in_declaration,
839        'PARAMS_IN_DECLARATION': params_in_declaration,
840        'STATIC': 'Static' if called_by_native.static else '',
841        'PRE_CALL': pre_call,
842        'POST_CALL': post_call,
843        'ENV_CALL': called_by_native.env_call,
844        'FIRST_PARAM_IN_CALL': first_param_in_call,
845        'PARAMS_IN_CALL': params_for_call,
846        'METHOD_ID_VAR_NAME': called_by_native.method_id_var_name,
847        'CHECK_EXCEPTION': check_exception,
848        'GET_METHOD_ID_IMPL': self.GetMethodIDImpl(called_by_native)
849    }
850    values['FUNCTION_SIGNATURE'] = (
851        function_signature_template.substitute(values))
852    if called_by_native.system_class:
853      values['FUNCTION_HEADER'] = (
854          function_header_with_unused_template.substitute(values))
855    else:
856      values['FUNCTION_HEADER'] = function_header_template.substitute(values)
857    return template.substitute(values)
858
859  def GetKMethodArrayEntry(self, native):
860    template = Template("""\
861    { "native${NAME}", ${JNI_SIGNATURE}, reinterpret_cast<void*>(${NAME}) },""")
862    values = {'NAME': native.name,
863              'JNI_SIGNATURE': JniParams.Signature(native.params,
864                                                   native.return_type,
865                                                   True)}
866    return template.substitute(values)
867
868  def GetUniqueClasses(self, origin):
869    ret = {self.class_name: self.fully_qualified_class}
870    for entry in origin:
871      class_name = self.class_name
872      jni_class_path = self.fully_qualified_class
873      if entry.java_class_name:
874        class_name = entry.java_class_name
875        jni_class_path = self.fully_qualified_class + '$' + class_name
876      ret[class_name] = jni_class_path
877    return ret
878
879  def GetClassPathDefinitions(self):
880    """Returns the ClassPath constants."""
881    ret = []
882    template = Template("""\
883const char k${JAVA_CLASS}ClassPath[] = "${JNI_CLASS_PATH}";""")
884    native_classes = self.GetUniqueClasses(self.natives)
885    called_by_native_classes = self.GetUniqueClasses(self.called_by_natives)
886    all_classes = native_classes
887    all_classes.update(called_by_native_classes)
888    for clazz in all_classes:
889      values = {
890          'JAVA_CLASS': clazz,
891          'JNI_CLASS_PATH': JniParams.RemapClassName(all_classes[clazz]),
892      }
893      ret += [template.substitute(values)]
894    ret += ''
895    for clazz in called_by_native_classes:
896      template = Template("""\
897// Leaking this jclass as we cannot use LazyInstance from some threads.
898jclass g_${JAVA_CLASS}_clazz = NULL;""")
899      values = {
900          'JAVA_CLASS': clazz,
901      }
902      ret += [template.substitute(values)]
903    return '\n'.join(ret)
904
905  def GetFindClasses(self):
906    """Returns the imlementation of FindClass for all known classes."""
907    template = Template("""\
908  g_${JAVA_CLASS}_clazz = reinterpret_cast<jclass>(env->NewGlobalRef(
909      base::android::GetClass(env, k${JAVA_CLASS}ClassPath).obj()));""")
910    ret = []
911    for clazz in self.GetUniqueClasses(self.called_by_natives):
912      values = {'JAVA_CLASS': clazz}
913      ret += [template.substitute(values)]
914    return '\n'.join(ret)
915
916  def GetMethodIDImpl(self, called_by_native):
917    """Returns the implementation of GetMethodID."""
918    template = Template("""\
919  base::android::MethodID::LazyGet<
920      base::android::MethodID::TYPE_${STATIC}>(
921      env, g_${JAVA_CLASS}_clazz,
922      "${JNI_NAME}",
923      ${JNI_SIGNATURE},
924      &g_${JAVA_CLASS}_${METHOD_ID_VAR_NAME});
925""")
926    jni_name = called_by_native.name
927    jni_return_type = called_by_native.return_type
928    if called_by_native.is_constructor:
929      jni_name = '<init>'
930      jni_return_type = 'void'
931    values = {
932        'JAVA_CLASS': called_by_native.java_class_name or self.class_name,
933        'JNI_NAME': jni_name,
934        'METHOD_ID_VAR_NAME': called_by_native.method_id_var_name,
935        'STATIC': 'STATIC' if called_by_native.static else 'INSTANCE',
936        'JNI_SIGNATURE': JniParams.Signature(called_by_native.params,
937                                             jni_return_type,
938                                             True)
939    }
940    return template.substitute(values)
941
942
943def WrapOutput(output):
944  ret = []
945  for line in output.splitlines():
946    # Do not wrap lines under 80 characters or preprocessor directives.
947    if len(line) < 80 or line.lstrip()[:1] == '#':
948      stripped = line.rstrip()
949      if len(ret) == 0 or len(ret[-1]) or len(stripped):
950        ret.append(stripped)
951    else:
952      first_line_indent = ' ' * (len(line) - len(line.lstrip()))
953      subsequent_indent =  first_line_indent + ' ' * 4
954      if line.startswith('//'):
955        subsequent_indent = '//' + subsequent_indent
956      wrapper = textwrap.TextWrapper(width=80,
957                                     subsequent_indent=subsequent_indent,
958                                     break_long_words=False)
959      ret += [wrapped.rstrip() for wrapped in wrapper.wrap(line)]
960  ret += ['']
961  return '\n'.join(ret)
962
963
964def ExtractJarInputFile(jar_file, input_file, out_dir):
965  """Extracts input file from jar and returns the filename.
966
967  The input file is extracted to the same directory that the generated jni
968  headers will be placed in.  This is passed as an argument to script.
969
970  Args:
971    jar_file: the jar file containing the input files to extract.
972    input_files: the list of files to extract from the jar file.
973    out_dir: the name of the directories to extract to.
974
975  Returns:
976    the name of extracted input file.
977  """
978  jar_file = zipfile.ZipFile(jar_file)
979
980  out_dir = os.path.join(out_dir, os.path.dirname(input_file))
981  try:
982    os.makedirs(out_dir)
983  except OSError as e:
984    if e.errno != errno.EEXIST:
985      raise
986  extracted_file_name = os.path.join(out_dir, os.path.basename(input_file))
987  with open(extracted_file_name, 'w') as outfile:
988    outfile.write(jar_file.read(input_file))
989
990  return extracted_file_name
991
992
993def GenerateJNIHeader(input_file, output_file, namespace, skip_if_same):
994  try:
995    if os.path.splitext(input_file)[1] == '.class':
996      jni_from_javap = JNIFromJavaP.CreateFromClass(input_file, namespace)
997      content = jni_from_javap.GetContent()
998    else:
999      jni_from_java_source = JNIFromJavaSource.CreateFromFile(input_file)
1000      content = jni_from_java_source.GetContent()
1001  except ParseError, e:
1002    print e
1003    sys.exit(1)
1004  if output_file:
1005    if not os.path.exists(os.path.dirname(os.path.abspath(output_file))):
1006      os.makedirs(os.path.dirname(os.path.abspath(output_file)))
1007    if skip_if_same and os.path.exists(output_file):
1008      with file(output_file, 'r') as f:
1009        existing_content = f.read()
1010        if existing_content == content:
1011          return
1012    with file(output_file, 'w') as f:
1013      f.write(content)
1014  else:
1015    print output
1016
1017
1018def main(argv):
1019  usage = """usage: %prog [OPTIONS]
1020This script will parse the given java source code extracting the native
1021declarations and print the header file to stdout (or a file).
1022See SampleForTests.java for more details.
1023  """
1024  option_parser = optparse.OptionParser(usage=usage)
1025  option_parser.add_option('-j', dest='jar_file',
1026                           help='Extract the list of input files from'
1027                           ' a specified jar file.'
1028                           ' Uses javap to extract the methods from a'
1029                           ' pre-compiled class. --input should point'
1030                           ' to pre-compiled Java .class files.')
1031  option_parser.add_option('-n', dest='namespace',
1032                           help='Uses as a namespace in the generated header,'
1033                           ' instead of the javap class name.')
1034  option_parser.add_option('--input_file',
1035                           help='Single input file name. The output file name '
1036                           'will be derived from it. Must be used with '
1037                           '--output_dir.')
1038  option_parser.add_option('--output_dir',
1039                           help='The output directory. Must be used with '
1040                           '--input')
1041  option_parser.add_option('--optimize_generation', type="int",
1042                           default=0, help='Whether we should optimize JNI '
1043                           'generation by not regenerating files if they have '
1044                           'not changed.')
1045  option_parser.add_option('--jarjar',
1046                           help='Path to optional jarjar rules file.')
1047  options, args = option_parser.parse_args(argv)
1048  if options.jar_file:
1049    input_file = ExtractJarInputFile(options.jar_file, options.input_file,
1050                                     options.output_dir)
1051  else:
1052    input_file = options.input_file
1053  output_file = None
1054  if options.output_dir:
1055    root_name = os.path.splitext(os.path.basename(input_file))[0]
1056    output_file = os.path.join(options.output_dir, root_name) + '_jni.h'
1057  if options.jarjar:
1058    with open(options.jarjar) as f:
1059      JniParams.SetJarJarMappings(f.read())
1060  GenerateJNIHeader(input_file, output_file, options.namespace,
1061                    options.optimize_generation)
1062
1063
1064if __name__ == '__main__':
1065  sys.exit(main(sys.argv))
1066