#!/usr/bin/env python # Copyright (c) 2012 The Chromium Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """Extracts native methods from a Java file and generates the JNI bindings. If you change this, please run and update the tests.""" import collections import errno import optparse import os import re import string from string import Template import subprocess import sys import textwrap import zipfile class ParseError(Exception): """Exception thrown when we can't parse the input file.""" def __init__(self, description, *context_lines): Exception.__init__(self) self.description = description self.context_lines = context_lines def __str__(self): context = '\n'.join(self.context_lines) return '***\nERROR: %s\n\n%s\n***' % (self.description, context) class Param(object): """Describes a param for a method, either java or native.""" def __init__(self, **kwargs): self.datatype = kwargs['datatype'] self.name = kwargs['name'] class NativeMethod(object): """Describes a C/C++ method that is called by Java code""" def __init__(self, **kwargs): self.static = kwargs['static'] self.java_class_name = kwargs['java_class_name'] self.return_type = kwargs['return_type'] self.name = kwargs['name'] self.params = kwargs['params'] if self.params: assert type(self.params) is list assert type(self.params[0]) is Param if (self.params and self.params[0].datatype == 'int' and self.params[0].name.startswith('native')): self.type = 'method' self.p0_type = self.params[0].name[len('native'):] if kwargs.get('native_class_name'): self.p0_type = kwargs['native_class_name'] else: self.type = 'function' self.method_id_var_name = kwargs.get('method_id_var_name', None) class CalledByNative(object): """Describes a java method exported to c/c++""" def __init__(self, **kwargs): self.system_class = kwargs['system_class'] self.unchecked = kwargs['unchecked'] self.static = kwargs['static'] self.java_class_name = kwargs['java_class_name'] self.return_type = kwargs['return_type'] self.name = kwargs['name'] self.params = kwargs['params'] self.method_id_var_name = kwargs.get('method_id_var_name', None) self.is_constructor = kwargs.get('is_constructor', False) self.env_call = GetEnvCall(self.is_constructor, self.static, self.return_type) self.static_cast = GetStaticCastForReturnType(self.return_type) def JavaDataTypeToC(java_type): """Returns a C datatype for the given java type.""" java_pod_type_map = { 'int': 'jint', 'byte': 'jbyte', 'char': 'jchar', 'short': 'jshort', 'boolean': 'jboolean', 'long': 'jlong', 'double': 'jdouble', 'float': 'jfloat', } java_type_map = { 'void': 'void', 'String': 'jstring', 'java/lang/String': 'jstring', 'Class': 'jclass', 'java/lang/Class': 'jclass', } if java_type in java_pod_type_map: return java_pod_type_map[java_type] elif java_type in java_type_map: return java_type_map[java_type] elif java_type.endswith('[]'): if java_type[:-2] in java_pod_type_map: return java_pod_type_map[java_type[:-2]] + 'Array' return 'jobjectArray' else: return 'jobject' class JniParams(object): _imports = [] _fully_qualified_class = '' _package = '' _inner_classes = [] _remappings = [] @staticmethod def SetFullyQualifiedClass(fully_qualified_class): JniParams._fully_qualified_class = 'L' + fully_qualified_class JniParams._package = '/'.join(fully_qualified_class.split('/')[:-1]) @staticmethod def ExtractImportsAndInnerClasses(contents): contents = contents.replace('\n', '') re_import = re.compile(r'import.*?(?P\S*?);') for match in re.finditer(re_import, contents): JniParams._imports += ['L' + match.group('class').replace('.', '/')] re_inner = re.compile(r'(class|interface)\s+?(?P\w+?)\W') for match in re.finditer(re_inner, contents): inner = match.group('name') if not JniParams._fully_qualified_class.endswith(inner): JniParams._inner_classes += [JniParams._fully_qualified_class + '$' + inner] @staticmethod def JavaToJni(param): """Converts a java param into a JNI signature type.""" pod_param_map = { 'int': 'I', 'boolean': 'Z', 'char': 'C', 'short': 'S', 'long': 'J', 'double': 'D', 'float': 'F', 'byte': 'B', 'void': 'V', } object_param_list = [ 'Ljava/lang/Boolean', 'Ljava/lang/Integer', 'Ljava/lang/Long', 'Ljava/lang/Object', 'Ljava/lang/String', 'Ljava/lang/Class', ] prefix = '' # Array? while param[-2:] == '[]': prefix += '[' param = param[:-2] # Generic? if '<' in param: param = param[:param.index('<')] if param in pod_param_map: return prefix + pod_param_map[param] if '/' in param: # Coming from javap, use the fully qualified param directly. return prefix + 'L' + JniParams.RemapClassName(param) + ';' for qualified_name in (object_param_list + [JniParams._fully_qualified_class] + JniParams._inner_classes): if (qualified_name.endswith('/' + param) or qualified_name.endswith('$' + param.replace('.', '$')) or qualified_name == 'L' + param): return prefix + JniParams.RemapClassName(qualified_name) + ';' # Is it from an import? (e.g. referecing Class from import pkg.Class; # note that referencing an inner class Inner from import pkg.Class.Inner # is not supported). for qualified_name in JniParams._imports: if qualified_name.endswith('/' + param): # Ensure it's not an inner class. components = qualified_name.split('/') if len(components) > 2 and components[-2][0].isupper(): raise SyntaxError('Inner class (%s) can not be imported ' 'and used by JNI (%s). Please import the outer ' 'class and use Outer.Inner instead.' % (qualified_name, param)) return prefix + JniParams.RemapClassName(qualified_name) + ';' # Is it an inner class from an outer class import? (e.g. referencing # Class.Inner from import pkg.Class). if '.' in param: components = param.split('.') outer = '/'.join(components[:-1]) inner = components[-1] for qualified_name in JniParams._imports: if qualified_name.endswith('/' + outer): return (prefix + JniParams.RemapClassName(qualified_name) + '$' + inner + ';') # Type not found, falling back to same package as this class. return (prefix + 'L' + JniParams.RemapClassName(JniParams._package + '/' + param) + ';') @staticmethod def Signature(params, returns, wrap): """Returns the JNI signature for the given datatypes.""" items = ['('] items += [JniParams.JavaToJni(param.datatype) for param in params] items += [')'] items += [JniParams.JavaToJni(returns)] if wrap: return '\n' + '\n'.join(['"' + item + '"' for item in items]) else: return '"' + ''.join(items) + '"' @staticmethod def Parse(params): """Parses the params into a list of Param objects.""" if not params: return [] ret = [] for p in [p.strip() for p in params.split(',')]: items = p.split(' ') if 'final' in items: items.remove('final') param = Param( datatype=items[0], name=(items[1] if len(items) > 1 else 'p%s' % len(ret)), ) ret += [param] return ret @staticmethod def RemapClassName(class_name): """Remaps class names using the jarjar mapping table.""" for old, new in JniParams._remappings: if old in class_name: return class_name.replace(old, new, 1) return class_name @staticmethod def SetJarJarMappings(mappings): """Parse jarjar mappings from a string.""" JniParams._remappings = [] for line in mappings.splitlines(): keyword, src, dest = line.split() if keyword != 'rule': continue assert src.endswith('.**') src = src[:-2].replace('.', '/') dest = dest.replace('.', '/') if dest.endswith('@0'): JniParams._remappings.append((src, dest[:-2] + src)) else: assert dest.endswith('@1') JniParams._remappings.append((src, dest[:-2])) def ExtractJNINamespace(contents): re_jni_namespace = re.compile('.*?@JNINamespace\("(.*?)"\)') m = re.findall(re_jni_namespace, contents) if not m: return '' return m[0] def ExtractFullyQualifiedJavaClassName(java_file_name, contents): re_package = re.compile('.*?package (.*?);') matches = re.findall(re_package, contents) if not matches: raise SyntaxError('Unable to find "package" line in %s' % java_file_name) return (matches[0].replace('.', '/') + '/' + os.path.splitext(os.path.basename(java_file_name))[0]) def ExtractNatives(contents): """Returns a list of dict containing information about a native method.""" contents = contents.replace('\n', '') natives = [] re_native = re.compile(r'(@NativeClassQualifiedName' '\(\"(?P.*?)\"\))?\s*' '(@NativeCall(\(\"(?P.*?)\"\)))?\s*' '(?P\w+\s\w+|\w+|\s+)\s*?native ' '(?P\S*?) ' '(?P\w+?)\((?P.*?)\);') for match in re.finditer(re_native, contents): native = NativeMethod( static='static' in match.group('qualifiers'), java_class_name=match.group('java_class_name'), native_class_name=match.group('native_class_name'), return_type=match.group('return_type'), name=match.group('name').replace('native', ''), params=JniParams.Parse(match.group('params'))) natives += [native] return natives def GetStaticCastForReturnType(return_type): type_map = { 'String' : 'jstring', 'java/lang/String' : 'jstring', 'boolean[]': 'jbooleanArray', 'byte[]': 'jbyteArray', 'char[]': 'jcharArray', 'short[]': 'jshortArray', 'int[]': 'jintArray', 'long[]': 'jlongArray', 'double[]': 'jdoubleArray' } ret = type_map.get(return_type, None) if ret: return ret if return_type.endswith('[]'): return 'jobjectArray' return None def GetEnvCall(is_constructor, is_static, return_type): """Maps the types availabe via env->Call__Method.""" if is_constructor: return 'NewObject' env_call_map = {'boolean': 'Boolean', 'byte': 'Byte', 'char': 'Char', 'short': 'Short', 'int': 'Int', 'long': 'Long', 'float': 'Float', 'void': 'Void', 'double': 'Double', 'Object': 'Object', } call = env_call_map.get(return_type, 'Object') if is_static: call = 'Static' + call return 'Call' + call + 'Method' def GetMangledParam(datatype): """Returns a mangled identifier for the datatype.""" if len(datatype) <= 2: return datatype.replace('[', 'A') ret = '' for i in range(1, len(datatype)): c = datatype[i] if c == '[': ret += 'A' elif c.isupper() or datatype[i - 1] in ['/', 'L']: ret += c.upper() return ret def GetMangledMethodName(name, params, return_type): """Returns a mangled method name for the given signature. The returned name can be used as a C identifier and will be unique for all valid overloads of the same method. Args: name: string. params: list of Param. return_type: string. Returns: A mangled name. """ mangled_items = [] for datatype in [return_type] + [x.datatype for x in params]: mangled_items += [GetMangledParam(JniParams.JavaToJni(datatype))] mangled_name = name + '_'.join(mangled_items) assert re.match(r'[0-9a-zA-Z_]+', mangled_name) return mangled_name def MangleCalledByNatives(called_by_natives): """Mangles all the overloads from the call_by_natives list.""" method_counts = collections.defaultdict( lambda: collections.defaultdict(lambda: 0)) for called_by_native in called_by_natives: java_class_name = called_by_native.java_class_name name = called_by_native.name method_counts[java_class_name][name] += 1 for called_by_native in called_by_natives: java_class_name = called_by_native.java_class_name method_name = called_by_native.name method_id_var_name = method_name if method_counts[java_class_name][method_name] > 1: method_id_var_name = GetMangledMethodName(method_name, called_by_native.params, called_by_native.return_type) called_by_native.method_id_var_name = method_id_var_name return called_by_natives # Regex to match the JNI return types that should be included in a # ScopedJavaLocalRef. RE_SCOPED_JNI_RETURN_TYPES = re.compile('jobject|jclass|jstring|.*Array') # Regex to match a string like "@CalledByNative public void foo(int bar)". RE_CALLED_BY_NATIVE = re.compile( '@CalledByNative(?P(Unchecked)*?)(?:\("(?P.*)"\))?' '\s+(?P[\w ]*?)' '\s*(?P\S+?)' '\s+(?P\w+)' '\s*\((?P[^\)]*)\)') def ExtractCalledByNatives(contents): """Parses all methods annotated with @CalledByNative. Args: contents: the contents of the java file. Returns: A list of dict with information about the annotated methods. TODO(bulach): return a CalledByNative object. Raises: ParseError: if unable to parse. """ called_by_natives = [] for match in re.finditer(RE_CALLED_BY_NATIVE, contents): called_by_natives += [CalledByNative( system_class=False, unchecked='Unchecked' in match.group('Unchecked'), static='static' in match.group('prefix'), java_class_name=match.group('annotation') or '', return_type=match.group('return_type'), name=match.group('name'), params=JniParams.Parse(match.group('params')))] # Check for any @CalledByNative occurrences that weren't matched. unmatched_lines = re.sub(RE_CALLED_BY_NATIVE, '', contents).split('\n') for line1, line2 in zip(unmatched_lines, unmatched_lines[1:]): if '@CalledByNative' in line1: raise ParseError('could not parse @CalledByNative method signature', line1, line2) return MangleCalledByNatives(called_by_natives) class JNIFromJavaP(object): """Uses 'javap' to parse a .class file and generate the JNI header file.""" def __init__(self, contents, namespace): self.contents = contents self.namespace = namespace self.fully_qualified_class = re.match( '.*?(class|interface) (?P.*?)( |{)', contents[1]).group('class_name') self.fully_qualified_class = self.fully_qualified_class.replace('.', '/') JniParams.SetFullyQualifiedClass(self.fully_qualified_class) self.java_class_name = self.fully_qualified_class.split('/')[-1] if not self.namespace: self.namespace = 'JNI_' + self.java_class_name re_method = re.compile('(?P.*?)(?P\S+?) (?P\w+?)' '\((?P.*?)\)') self.called_by_natives = [] for content in contents[2:]: match = re.match(re_method, content) if not match: continue self.called_by_natives += [CalledByNative( system_class=True, unchecked=False, static='static' in match.group('prefix'), java_class_name='', return_type=match.group('return_type').replace('.', '/'), name=match.group('name'), params=JniParams.Parse(match.group('params').replace('.', '/')))] re_constructor = re.compile('.*? public ' + self.fully_qualified_class.replace('/', '.') + '\((?P.*?)\)') for content in contents[2:]: match = re.match(re_constructor, content) if not match: continue self.called_by_natives += [CalledByNative( system_class=True, unchecked=False, static=False, java_class_name='', return_type=self.fully_qualified_class, name='Constructor', params=JniParams.Parse(match.group('params').replace('.', '/')), is_constructor=True)] self.called_by_natives = MangleCalledByNatives(self.called_by_natives) self.inl_header_file_generator = InlHeaderFileGenerator( self.namespace, self.fully_qualified_class, [], self.called_by_natives) def GetContent(self): return self.inl_header_file_generator.GetContent() @staticmethod def CreateFromClass(class_file, namespace): class_name = os.path.splitext(os.path.basename(class_file))[0] p = subprocess.Popen(args=['javap', class_name], cwd=os.path.dirname(class_file), stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, _ = p.communicate() jni_from_javap = JNIFromJavaP(stdout.split('\n'), namespace) return jni_from_javap class JNIFromJavaSource(object): """Uses the given java source file to generate the JNI header file.""" def __init__(self, contents, fully_qualified_class): contents = self._RemoveComments(contents) JniParams.SetFullyQualifiedClass(fully_qualified_class) JniParams.ExtractImportsAndInnerClasses(contents) jni_namespace = ExtractJNINamespace(contents) natives = ExtractNatives(contents) called_by_natives = ExtractCalledByNatives(contents) if len(natives) == 0 and len(called_by_natives) == 0: raise SyntaxError('Unable to find any JNI methods for %s.' % fully_qualified_class) inl_header_file_generator = InlHeaderFileGenerator( jni_namespace, fully_qualified_class, natives, called_by_natives) self.content = inl_header_file_generator.GetContent() def _RemoveComments(self, contents): # We need to support both inline and block comments, and we need to handle # strings that contain '//' or '/*'. Rather than trying to do all that with # regexps, we just pipe the contents through the C preprocessor. We tell cpp # the file has already been preprocessed, so it just removes comments and # doesn't try to parse #include, #pragma etc. # # TODO(husky): This is a bit hacky. It would be cleaner to use a real Java # parser. Maybe we could ditch JNIFromJavaSource and just always use # JNIFromJavaP; or maybe we could rewrite this script in Java and use APT. # http://code.google.com/p/chromium/issues/detail?id=138941 p = subprocess.Popen(args=['cpp', '-fpreprocessed'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, _ = p.communicate(contents) return stdout def GetContent(self): return self.content @staticmethod def CreateFromFile(java_file_name): contents = file(java_file_name).read() fully_qualified_class = ExtractFullyQualifiedJavaClassName(java_file_name, contents) return JNIFromJavaSource(contents, fully_qualified_class) class InlHeaderFileGenerator(object): """Generates an inline header file for JNI integration.""" def __init__(self, namespace, fully_qualified_class, natives, called_by_natives): self.namespace = namespace self.fully_qualified_class = fully_qualified_class self.class_name = self.fully_qualified_class.split('/')[-1] self.natives = natives self.called_by_natives = called_by_natives self.header_guard = fully_qualified_class.replace('/', '_') + '_JNI' def GetContent(self): """Returns the content of the JNI binding file.""" template = Template("""\ // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // This file is autogenerated by // ${SCRIPT_NAME} // For // ${FULLY_QUALIFIED_CLASS} #ifndef ${HEADER_GUARD} #define ${HEADER_GUARD} #include #include "base/android/jni_android.h" #include "base/android/scoped_java_ref.h" #include "base/basictypes.h" #include "base/logging.h" using base::android::ScopedJavaLocalRef; // Step 1: forward declarations. namespace { $CLASS_PATH_DEFINITIONS } // namespace $OPEN_NAMESPACE $FORWARD_DECLARATIONS // Step 2: method stubs. $METHOD_STUBS // Step 3: RegisterNatives. static bool RegisterNativesImpl(JNIEnv* env) { $REGISTER_NATIVES_IMPL return true; } $CLOSE_NAMESPACE #endif // ${HEADER_GUARD} """) script_components = os.path.abspath(sys.argv[0]).split(os.path.sep) base_index = script_components.index('base') script_name = os.sep.join(script_components[base_index:]) values = { 'SCRIPT_NAME': script_name, 'FULLY_QUALIFIED_CLASS': self.fully_qualified_class, 'CLASS_PATH_DEFINITIONS': self.GetClassPathDefinitionsString(), 'FORWARD_DECLARATIONS': self.GetForwardDeclarationsString(), 'METHOD_STUBS': self.GetMethodStubsString(), 'OPEN_NAMESPACE': self.GetOpenNamespaceString(), 'REGISTER_NATIVES_IMPL': self.GetRegisterNativesImplString(), 'CLOSE_NAMESPACE': self.GetCloseNamespaceString(), 'HEADER_GUARD': self.header_guard, } return WrapOutput(template.substitute(values)) def GetClassPathDefinitionsString(self): ret = [] ret += [self.GetClassPathDefinitions()] return '\n'.join(ret) def GetForwardDeclarationsString(self): ret = [] for native in self.natives: if native.type != 'method': ret += [self.GetForwardDeclaration(native)] return '\n'.join(ret) def GetMethodStubsString(self): ret = [] for native in self.natives: if native.type == 'method': ret += [self.GetNativeMethodStub(native)] for called_by_native in self.called_by_natives: ret += [self.GetCalledByNativeMethodStub(called_by_native)] return '\n'.join(ret) def GetKMethodsString(self, clazz): ret = [] for native in self.natives: if (native.java_class_name == clazz or (not native.java_class_name and clazz == self.class_name)): ret += [self.GetKMethodArrayEntry(native)] return '\n'.join(ret) def GetRegisterNativesImplString(self): """Returns the implementation for RegisterNatives.""" template = Template("""\ static const JNINativeMethod kMethods${JAVA_CLASS}[] = { ${KMETHODS} }; const int kMethods${JAVA_CLASS}Size = arraysize(kMethods${JAVA_CLASS}); if (env->RegisterNatives(g_${JAVA_CLASS}_clazz, kMethods${JAVA_CLASS}, kMethods${JAVA_CLASS}Size) < 0) { LOG(ERROR) << "RegisterNatives failed in " << __FILE__; return false; } """) ret = [self.GetFindClasses()] all_classes = self.GetUniqueClasses(self.natives) all_classes[self.class_name] = self.fully_qualified_class for clazz in all_classes: kmethods = self.GetKMethodsString(clazz) if kmethods: values = {'JAVA_CLASS': clazz, 'KMETHODS': kmethods} ret += [template.substitute(values)] if not ret: return '' return '\n' + '\n'.join(ret) def GetOpenNamespaceString(self): if self.namespace: all_namespaces = ['namespace %s {' % ns for ns in self.namespace.split('::')] return '\n'.join(all_namespaces) return '' def GetCloseNamespaceString(self): if self.namespace: all_namespaces = ['} // namespace %s' % ns for ns in self.namespace.split('::')] all_namespaces.reverse() return '\n'.join(all_namespaces) + '\n' return '' def GetJNIFirstParam(self, native): ret = [] if native.type == 'method': ret = ['jobject obj'] elif native.type == 'function': if native.static: ret = ['jclass clazz'] else: ret = ['jobject obj'] return ret def GetParamsInDeclaration(self, native): """Returns the params for the stub declaration. Args: native: the native dictionary describing the method. Returns: A string containing the params. """ return ',\n '.join(self.GetJNIFirstParam(native) + [JavaDataTypeToC(param.datatype) + ' ' + param.name for param in native.params]) def GetCalledByNativeParamsInDeclaration(self, called_by_native): return ',\n '.join([JavaDataTypeToC(param.datatype) + ' ' + param.name for param in called_by_native.params]) def GetForwardDeclaration(self, native): template = Template(""" static ${RETURN} ${NAME}(JNIEnv* env, ${PARAMS}); """) values = {'RETURN': JavaDataTypeToC(native.return_type), 'NAME': native.name, 'PARAMS': self.GetParamsInDeclaration(native)} return template.substitute(values) def GetNativeMethodStub(self, native): """Returns stubs for native methods.""" template = Template("""\ static ${RETURN} ${NAME}(JNIEnv* env, ${PARAMS_IN_DECLARATION}) { DCHECK(${PARAM0_NAME}) << "${NAME}"; ${P0_TYPE}* native = reinterpret_cast<${P0_TYPE}*>(${PARAM0_NAME}); return native->${NAME}(env, obj${PARAMS_IN_CALL})${POST_CALL}; } """) params_for_call = ', '.join(p.name for p in native.params[1:]) if params_for_call: params_for_call = ', ' + params_for_call return_type = JavaDataTypeToC(native.return_type) if re.match(RE_SCOPED_JNI_RETURN_TYPES, return_type): scoped_return_type = 'ScopedJavaLocalRef<' + return_type + '>' post_call = '.Release()' else: scoped_return_type = return_type post_call = '' values = { 'RETURN': return_type, 'SCOPED_RETURN': scoped_return_type, 'NAME': native.name, 'PARAMS_IN_DECLARATION': self.GetParamsInDeclaration(native), 'PARAM0_NAME': native.params[0].name, 'P0_TYPE': native.p0_type, 'PARAMS_IN_CALL': params_for_call, 'POST_CALL': post_call } return template.substitute(values) def GetCalledByNativeMethodStub(self, called_by_native): """Returns a string.""" function_signature_template = Template("""\ static ${RETURN_TYPE} Java_${JAVA_CLASS}_${METHOD_ID_VAR_NAME}(\ JNIEnv* env${FIRST_PARAM_IN_DECLARATION}${PARAMS_IN_DECLARATION})""") function_header_template = Template("""\ ${FUNCTION_SIGNATURE} {""") function_header_with_unused_template = Template("""\ ${FUNCTION_SIGNATURE} __attribute__ ((unused)); ${FUNCTION_SIGNATURE} {""") template = Template(""" static base::subtle::AtomicWord g_${JAVA_CLASS}_${METHOD_ID_VAR_NAME} = 0; ${FUNCTION_HEADER} /* Must call RegisterNativesImpl() */ DCHECK(g_${JAVA_CLASS}_clazz); jmethodID method_id = ${GET_METHOD_ID_IMPL} ${RETURN_DECLARATION} ${PRE_CALL}env->${ENV_CALL}(${FIRST_PARAM_IN_CALL}, method_id${PARAMS_IN_CALL})${POST_CALL}; ${CHECK_EXCEPTION} ${RETURN_CLAUSE} }""") if called_by_native.static or called_by_native.is_constructor: first_param_in_declaration = '' first_param_in_call = ('g_%s_clazz' % (called_by_native.java_class_name or self.class_name)) else: first_param_in_declaration = ', jobject obj' first_param_in_call = 'obj' params_in_declaration = self.GetCalledByNativeParamsInDeclaration( called_by_native) if params_in_declaration: params_in_declaration = ', ' + params_in_declaration params_for_call = ', '.join(param.name for param in called_by_native.params) if params_for_call: params_for_call = ', ' + params_for_call pre_call = '' post_call = '' if called_by_native.static_cast: pre_call = 'static_cast<%s>(' % called_by_native.static_cast post_call = ')' check_exception = '' if not called_by_native.unchecked: check_exception = 'base::android::CheckException(env);' return_type = JavaDataTypeToC(called_by_native.return_type) return_declaration = '' return_clause = '' if return_type != 'void': pre_call = ' ' + pre_call return_declaration = return_type + ' ret =' if re.match(RE_SCOPED_JNI_RETURN_TYPES, return_type): return_type = 'ScopedJavaLocalRef<' + return_type + '>' return_clause = 'return ' + return_type + '(env, ret);' else: return_clause = 'return ret;' values = { 'JAVA_CLASS': called_by_native.java_class_name or self.class_name, 'METHOD': called_by_native.name, 'RETURN_TYPE': return_type, 'RETURN_DECLARATION': return_declaration, 'RETURN_CLAUSE': return_clause, 'FIRST_PARAM_IN_DECLARATION': first_param_in_declaration, 'PARAMS_IN_DECLARATION': params_in_declaration, 'STATIC': 'Static' if called_by_native.static else '', 'PRE_CALL': pre_call, 'POST_CALL': post_call, 'ENV_CALL': called_by_native.env_call, 'FIRST_PARAM_IN_CALL': first_param_in_call, 'PARAMS_IN_CALL': params_for_call, 'METHOD_ID_VAR_NAME': called_by_native.method_id_var_name, 'CHECK_EXCEPTION': check_exception, 'GET_METHOD_ID_IMPL': self.GetMethodIDImpl(called_by_native) } values['FUNCTION_SIGNATURE'] = ( function_signature_template.substitute(values)) if called_by_native.system_class: values['FUNCTION_HEADER'] = ( function_header_with_unused_template.substitute(values)) else: values['FUNCTION_HEADER'] = function_header_template.substitute(values) return template.substitute(values) def GetKMethodArrayEntry(self, native): template = Template("""\ { "native${NAME}", ${JNI_SIGNATURE}, reinterpret_cast(${NAME}) },""") values = {'NAME': native.name, 'JNI_SIGNATURE': JniParams.Signature(native.params, native.return_type, True)} return template.substitute(values) def GetUniqueClasses(self, origin): ret = {self.class_name: self.fully_qualified_class} for entry in origin: class_name = self.class_name jni_class_path = self.fully_qualified_class if entry.java_class_name: class_name = entry.java_class_name jni_class_path = self.fully_qualified_class + '$' + class_name ret[class_name] = jni_class_path return ret def GetClassPathDefinitions(self): """Returns the ClassPath constants.""" ret = [] template = Template("""\ const char k${JAVA_CLASS}ClassPath[] = "${JNI_CLASS_PATH}";""") native_classes = self.GetUniqueClasses(self.natives) called_by_native_classes = self.GetUniqueClasses(self.called_by_natives) all_classes = native_classes all_classes.update(called_by_native_classes) for clazz in all_classes: values = { 'JAVA_CLASS': clazz, 'JNI_CLASS_PATH': JniParams.RemapClassName(all_classes[clazz]), } ret += [template.substitute(values)] ret += '' for clazz in called_by_native_classes: template = Template("""\ // Leaking this jclass as we cannot use LazyInstance from some threads. jclass g_${JAVA_CLASS}_clazz = NULL;""") values = { 'JAVA_CLASS': clazz, } ret += [template.substitute(values)] return '\n'.join(ret) def GetFindClasses(self): """Returns the imlementation of FindClass for all known classes.""" template = Template("""\ g_${JAVA_CLASS}_clazz = reinterpret_cast(env->NewGlobalRef( base::android::GetClass(env, k${JAVA_CLASS}ClassPath).obj()));""") ret = [] for clazz in self.GetUniqueClasses(self.called_by_natives): values = {'JAVA_CLASS': clazz} ret += [template.substitute(values)] return '\n'.join(ret) def GetMethodIDImpl(self, called_by_native): """Returns the implementation of GetMethodID.""" template = Template("""\ base::android::MethodID::LazyGet< base::android::MethodID::TYPE_${STATIC}>( env, g_${JAVA_CLASS}_clazz, "${JNI_NAME}", ${JNI_SIGNATURE}, &g_${JAVA_CLASS}_${METHOD_ID_VAR_NAME}); """) jni_name = called_by_native.name jni_return_type = called_by_native.return_type if called_by_native.is_constructor: jni_name = '' jni_return_type = 'void' values = { 'JAVA_CLASS': called_by_native.java_class_name or self.class_name, 'JNI_NAME': jni_name, 'METHOD_ID_VAR_NAME': called_by_native.method_id_var_name, 'STATIC': 'STATIC' if called_by_native.static else 'INSTANCE', 'JNI_SIGNATURE': JniParams.Signature(called_by_native.params, jni_return_type, True) } return template.substitute(values) def WrapOutput(output): ret = [] for line in output.splitlines(): # Do not wrap lines under 80 characters or preprocessor directives. if len(line) < 80 or line.lstrip()[:1] == '#': stripped = line.rstrip() if len(ret) == 0 or len(ret[-1]) or len(stripped): ret.append(stripped) else: first_line_indent = ' ' * (len(line) - len(line.lstrip())) subsequent_indent = first_line_indent + ' ' * 4 if line.startswith('//'): subsequent_indent = '//' + subsequent_indent wrapper = textwrap.TextWrapper(width=80, subsequent_indent=subsequent_indent, break_long_words=False) ret += [wrapped.rstrip() for wrapped in wrapper.wrap(line)] ret += [''] return '\n'.join(ret) def ExtractJarInputFile(jar_file, input_file, out_dir): """Extracts input file from jar and returns the filename. The input file is extracted to the same directory that the generated jni headers will be placed in. This is passed as an argument to script. Args: jar_file: the jar file containing the input files to extract. input_files: the list of files to extract from the jar file. out_dir: the name of the directories to extract to. Returns: the name of extracted input file. """ jar_file = zipfile.ZipFile(jar_file) out_dir = os.path.join(out_dir, os.path.dirname(input_file)) try: os.makedirs(out_dir) except OSError as e: if e.errno != errno.EEXIST: raise extracted_file_name = os.path.join(out_dir, os.path.basename(input_file)) with open(extracted_file_name, 'w') as outfile: outfile.write(jar_file.read(input_file)) return extracted_file_name def GenerateJNIHeader(input_file, output_file, namespace, skip_if_same): try: if os.path.splitext(input_file)[1] == '.class': jni_from_javap = JNIFromJavaP.CreateFromClass(input_file, namespace) content = jni_from_javap.GetContent() else: jni_from_java_source = JNIFromJavaSource.CreateFromFile(input_file) content = jni_from_java_source.GetContent() except ParseError, e: print e sys.exit(1) if output_file: if not os.path.exists(os.path.dirname(os.path.abspath(output_file))): os.makedirs(os.path.dirname(os.path.abspath(output_file))) if skip_if_same and os.path.exists(output_file): with file(output_file, 'r') as f: existing_content = f.read() if existing_content == content: return with file(output_file, 'w') as f: f.write(content) else: print output def main(argv): usage = """usage: %prog [OPTIONS] This script will parse the given java source code extracting the native declarations and print the header file to stdout (or a file). See SampleForTests.java for more details. """ option_parser = optparse.OptionParser(usage=usage) option_parser.add_option('-j', dest='jar_file', help='Extract the list of input files from' ' a specified jar file.' ' Uses javap to extract the methods from a' ' pre-compiled class. --input should point' ' to pre-compiled Java .class files.') option_parser.add_option('-n', dest='namespace', help='Uses as a namespace in the generated header,' ' instead of the javap class name.') option_parser.add_option('--input_file', help='Single input file name. The output file name ' 'will be derived from it. Must be used with ' '--output_dir.') option_parser.add_option('--output_dir', help='The output directory. Must be used with ' '--input') option_parser.add_option('--optimize_generation', type="int", default=0, help='Whether we should optimize JNI ' 'generation by not regenerating files if they have ' 'not changed.') option_parser.add_option('--jarjar', help='Path to optional jarjar rules file.') options, args = option_parser.parse_args(argv) if options.jar_file: input_file = ExtractJarInputFile(options.jar_file, options.input_file, options.output_dir) else: input_file = options.input_file output_file = None if options.output_dir: root_name = os.path.splitext(os.path.basename(input_file))[0] output_file = os.path.join(options.output_dir, root_name) + '_jni.h' if options.jarjar: with open(options.jarjar) as f: JniParams.SetJarJarMappings(f.read()) GenerateJNIHeader(input_file, output_file, options.namespace, options.optimize_generation) if __name__ == '__main__': sys.exit(main(sys.argv))