Home | History | Annotate | Download | only in jni_generator
      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.
      7 If you change this, please run and update the tests."""
      8 
      9 import collections
     10 import errno
     11 import optparse
     12 import os
     13 import re
     14 import string
     15 from string import Template
     16 import subprocess
     17 import sys
     18 import textwrap
     19 import zipfile
     20 
     21 
     22 class 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 
     35 class 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 
     43 class 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 
     67 class 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 
     85 def 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 
    117 class 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 
    270 def 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 
    278 def 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 
    287 def 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 
    309 def 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 
    327 def 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 
    348 def 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 
    362 def 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 
    384 def 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.
    406 RE_SCOPED_JNI_RETURN_TYPES = re.compile('jobject|jclass|jstring|.*Array')
    407 
    408 # Regex to match a string like "@CalledByNative public void foo(int bar)".
    409 RE_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 
    417 def 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 
    449 class 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 
    513 class 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 
    559 class 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 
    594 using base::android::ScopedJavaLocalRef;
    595 
    596 // Step 1: forward declarations.
    597 namespace {
    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 
    609 static 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("""
    735 static ${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("""\
    745 static ${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("""\
    777 static ${RETURN_TYPE} Java_${JAVA_CLASS}_${METHOD_ID_VAR_NAME}(\
    778 JNIEnv* 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("""
    785 static 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("""\
    883 const 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.
    898 jclass 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 
    943 def 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 
    964 def 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 
    993 def 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 
   1018 def main(argv):
   1019   usage = """usage: %prog [OPTIONS]
   1020 This script will parse the given java source code extracting the native
   1021 declarations and print the header file to stdout (or a file).
   1022 See 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 
   1064 if __name__ == '__main__':
   1065   sys.exit(main(sys.argv))
   1066