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