/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package org.mozilla.gecko.annotationProcessors; /** * Generate C++ bindings for SDK classes using a config file. * *

java SDKProcessor [ ]+ * *

: jar file containing the SDK classes (e.g. android.jar) : SDK * version for generated class members (bindings will not be generated for members with SDK versions * higher than max-sdk-version) : output directory for generated binding files : * config file for generating bindings : prefix used for generated binding files * *

Each config file is a text file following the .ini format: * *

; comment [section1] property = value * *

# comment [section2] property = value * *

Each section specifies a qualified SDK class. Each property specifies a member of that class. * The class and/or the property may specify options found in the WrapForJNI annotation. For * example, * *

# Generate bindings for Bundle using default options: [android.os.Bundle] * *

# Generate bindings for Bundle using class options: [android.os.Bundle = * exceptionMode:nsresult] * *

# Generate bindings for Bundle using method options: [android.os.Bundle] putInt = * stubName:PutInteger * *

# Generate bindings for Bundle using class options with method override: # (note that all * options are overriden at the same time.) [android.os.Bundle = exceptionMode:nsresult] # putInt * will have stubName "PutInteger", and exceptionMode of "abort" putInt = stubName:PutInteger # * putChar will have stubName "PutCharacter", and exceptionMode of "nsresult" putChar = * stubName:PutCharacter, exceptionMode:nsresult * *

# Overloded methods can be specified using its signature [android.os.Bundle] # Skip the copy * constructor (Landroid/os/Bundle;)V = skip:true * *

# Generic member types can be specified [android.view.KeyEvent = skip:true] # Skip everything * except fields = skip:false * *

# Skip everything except putInt and putChar [android.os.Bundle = skip:true] putInt = * skip:false putChar = * *

# Avoid conflicts in native bindings [android.os.Bundle] # Bundle(PersistableBundle) native * binding can conflict with Bundle(ClassLoader) (Landroid/os/PersistableBundle;)V = * stubName:NewFromPersistableBundle * *

# Generate a getter instead of a literal for certain runtime constants * [android.os.Build$VERSION = skip:true] SDK_INT = noLiteral:true */ import com.android.tools.lint.LintCliClient; import com.android.tools.lint.checks.ApiLookup; import com.android.tools.lint.client.api.LintClient; import java.io.BufferedReader; import java.io.File; import java.io.FileOutputStream; import java.io.FileReader; import java.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Member; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.Locale; import org.mozilla.gecko.annotationProcessors.classloader.AnnotatableEntity; import org.mozilla.gecko.annotationProcessors.classloader.ClassWithOptions; import org.mozilla.gecko.annotationProcessors.utils.Utils; public class SDKProcessor { public static final String GENERATED_COMMENT = "// GENERATED CODE\n" + "// Generated by the Java program at /build/annotationProcessors at compile time\n" + "// from annotations on Java methods. To update, change the annotations on the\n" + "// corresponding Javamethods and rerun the build. Manually updating this file\n" + "// will cause your build to fail.\n" + "\n"; private static ApiLookup sApiLookup; private static int sMaxSdkVersion; private static class ParseException extends Exception { public ParseException(final String message) { super(message); } } private static class ClassInfo { public final String name; // Map constructor/field/method signature to a set of annotation values. private final HashMap mAnnotations = new HashMap<>(); // Default set of annotation values to use. private final String mDefaultAnnotation; // List of nested classes to forward declare. private final ArrayList> mNestedClasses; public ClassInfo(final String text) { final String[] mapping = text.split("=", 2); name = mapping[0].trim(); mDefaultAnnotation = mapping.length > 1 ? mapping[1].trim() : null; mNestedClasses = new ArrayList<>(); } public void addAnnotation(final String text) throws ParseException { final String[] mapping = text.split("=", 2); final String prop = mapping[0].trim(); if (prop.isEmpty()) { throw new ParseException("Missing member name: " + text); } if (mapping.length < 2) { throw new ParseException("Missing equal sign: " + text); } if (mAnnotations.get(prop) != null) { throw new ParseException("Already has member: " + prop); } mAnnotations.put(prop, mapping[1].trim()); } public AnnotationInfo getAnnotationInfo(final Member member) throws ParseException { String stubName = Utils.getNativeName(member); AnnotationInfo.ExceptionMode mode = AnnotationInfo.ExceptionMode.ABORT; AnnotationInfo.CallingThread thread = AnnotationInfo.CallingThread.ANY; AnnotationInfo.DispatchTarget target = AnnotationInfo.DispatchTarget.CURRENT; boolean noLiteral = false; boolean isGeneric = false; final String name = Utils.getMemberName(member); String annotation = mAnnotations.get( name + (member instanceof Field ? ":" : "") + Utils.getSignature(member)); if (annotation == null) { // Match name without signature annotation = mAnnotations.get(name); } if (annotation == null) { // Match , , annotation = mAnnotations.get( "<" + member.getClass().getSimpleName().toLowerCase(Locale.ROOT) + '>'); isGeneric = true; } if (annotation == null) { // Fallback on class options, if any. annotation = mDefaultAnnotation; } if (annotation == null || annotation.isEmpty()) { return new AnnotationInfo(stubName, mode, thread, target, noLiteral); } final String[] elements = annotation.split(","); for (final String element : elements) { final String[] pair = element.split(":", 2); if (pair.length < 2) { throw new ParseException("Missing option value: " + element); } final String pairName = pair[0].trim(); final String pairValue = pair[1].trim(); switch (pairName) { case "skip": if (Boolean.valueOf(pairValue)) { // Return null to signal skipping current method. return null; } break; case "stubName": if (isGeneric) { // Prevent specifying stubName for class options. throw new ParseException("stubName doesn't make sense here: " + pairValue); } stubName = pairValue; break; case "exceptionMode": mode = Utils.getEnumValue(AnnotationInfo.ExceptionMode.class, pairValue); break; case "calledFrom": thread = Utils.getEnumValue(AnnotationInfo.CallingThread.class, pairValue); break; case "dispatchTo": target = Utils.getEnumValue(AnnotationInfo.DispatchTarget.class, pairValue); break; case "noLiteral": noLiteral = Boolean.valueOf(pairValue); break; default: throw new ParseException("Unknown option: " + pairName); } } return new AnnotationInfo(stubName, mode, thread, target, noLiteral); } } public static void main(String[] args) throws Exception { // We expect a list of jars on the commandline. If missing, whinge about it. if (args.length < 3 || args.length % 2 != 1) { System.err.println( "Usage: java SDKProcessor sdkjar max-sdk-version outdir [configfile fileprefix]*"); System.exit(1); } System.out.println("Processing platform bindings..."); final File sdkJar = new File(args[0]); sMaxSdkVersion = Integer.parseInt(args[1]); final String outdir = args[2]; final LintCliClient lintClient = new LintCliClient(LintClient.CLIENT_CLI); sApiLookup = ApiLookup.get(lintClient); for (int argIndex = 3; argIndex < args.length; argIndex += 2) { final String configFile = args[argIndex]; final String generatedFilePrefix = args[argIndex + 1]; System.out.println("Processing bindings from " + configFile); // Start the clock! long s = System.currentTimeMillis(); // Get an iterator over the classes in the jar files given... // Iterator jarClassIterator = // IterableJarLoadingURLClassLoader.getIteratorOverJars(args); StringBuilder headerFile = new StringBuilder(GENERATED_COMMENT); headerFile.append( "#ifndef " + generatedFilePrefix + "_h__\n" + "#define " + generatedFilePrefix + "_h__\n" + "\n" + "#include \"mozilla/jni/Refs.h\"\n" + "\n" + "namespace mozilla {\n" + "namespace java {\n" + "namespace sdk {\n" + "\n"); StringBuilder implementationFile = new StringBuilder(GENERATED_COMMENT); implementationFile.append( "#include \"" + generatedFilePrefix + ".h\"\n" + "#include \"mozilla/jni/Accessors.h\"\n" + "\n" + "namespace mozilla {\n" + "namespace java {\n" + "namespace sdk {\n" + "\n"); // Used to track the calls to the various class-specific initialisation functions. ClassLoader loader = null; try { loader = URLClassLoader.newInstance( new URL[] {sdkJar.toURI().toURL()}, SDKProcessor.class.getClassLoader()); } catch (Exception e) { throw new RuntimeException(e.toString()); } try { ClassInfo[] classes = getClassList(configFile); classes = addNestedClassForwardDeclarations(classes, loader); for (final ClassInfo cls : classes) { System.out.println("Looking up: " + cls.name); generateClass(Class.forName(cls.name, true, loader), cls, implementationFile, headerFile); } } catch (final IllegalStateException | IOException | ParseException e) { System.err.println("***"); System.err.println("*** Error parsing config file: " + configFile); System.err.println("*** " + e); System.err.println("***"); if (e.getCause() != null) { e.getCause().printStackTrace(System.err); } System.exit(1); return; } implementationFile.append("} /* sdk */\n" + "} /* java */\n" + "} /* mozilla */\n"); headerFile.append("} /* sdk */\n" + "} /* java */\n" + "} /* mozilla */\n" + "#endif\n"); writeOutputFiles(outdir, generatedFilePrefix, headerFile, implementationFile); long e = System.currentTimeMillis(); System.out.println("SDK processing complete in " + (e - s) + "ms"); } } private static int getAPIVersion(Class cls, Member m) { if (m instanceof Method || m instanceof Constructor) { return sApiLookup.getMethodVersion( cls.getName().replace('.', '/'), Utils.getMemberName(m), Utils.getSignature(m)); } else if (m instanceof Field) { return sApiLookup.getFieldVersion( Utils.getClassDescriptor(m.getDeclaringClass()), m.getName()); } else { throw new IllegalArgumentException("expected member to be Method, Constructor, or Field"); } } private static Member[] sortAndFilterMembers(Class cls, Member[] members) { Arrays.sort( members, new Comparator() { @Override public int compare(Member a, Member b) { int result = a.getName().compareTo(b.getName()); if (result == 0) { if (a instanceof Constructor && b instanceof Constructor) { String sa = Arrays.toString(((Constructor) a).getParameterTypes()); String sb = Arrays.toString(((Constructor) b).getParameterTypes()); result = sa.compareTo(sb); } else if (a instanceof Method && b instanceof Method) { String sa = Arrays.toString(((Method) a).getParameterTypes()); String sb = Arrays.toString(((Method) b).getParameterTypes()); result = sa.compareTo(sb); } } return result; } }); ArrayList list = new ArrayList<>(); for (final Member m : members) { if (m.getDeclaringClass() == Object.class) { // Skip methods from Object. continue; } // Sometimes (e.g. Bundle) has methods that moved to/from a superclass in a later SDK // version, so we check for both classes and see if we can find a minimum SDK version. int version = getAPIVersion(cls, m); final int version2 = getAPIVersion(m.getDeclaringClass(), m); if (version2 > 0 && version2 < version) { version = version2; } if (version > sMaxSdkVersion) { System.out.println( "Skipping " + m.getDeclaringClass().getName() + "." + Utils.getMemberName(m) + ", version " + version + " > " + sMaxSdkVersion); continue; } // Sometimes (e.g. KeyEvent) a field can appear in both a class and a superclass. In // that case we want to filter out the version that appears in the superclass, or // we'll have bindings with duplicate names. try { if (m instanceof Field && !m.equals(cls.getField(m.getName()))) { // m is a field in a superclass that has been hidden by // a field with the same name in a subclass. System.out.println( "Skipping " + Utils.getMemberName(m) + " from " + m.getDeclaringClass().getName()); continue; } } catch (final NoSuchFieldException e) { } list.add(m); } return list.toArray(new Member[list.size()]); } private static void generateMembers(CodeGenerator generator, ClassInfo clsInfo, Member[] members) throws ParseException { for (Member m : members) { if (!Modifier.isPublic(m.getModifiers())) { continue; } // Default for SDK bindings. final AnnotationInfo info = clsInfo.getAnnotationInfo(m); if (info == null) { // Skip this member. continue; } final AnnotatableEntity entity = new AnnotatableEntity(m, info); if (m instanceof Constructor) { generator.generateConstructor(entity); } else if (m instanceof Method) { generator.generateMethod(entity); } else if (m instanceof Field) { generator.generateField(entity); } else { throw new IllegalArgumentException("expected member to be Constructor, Method, or Field"); } } } private static String getGeneratedName(Class clazz) { ArrayList classes = new ArrayList<>(); do { classes.add(clazz.getSimpleName()); clazz = clazz.getDeclaringClass(); } while (clazz != null); Collections.reverse(classes); return String.join("::", classes); } private static void generateClass( Class clazz, ClassInfo clsInfo, StringBuilder implementationFile, StringBuilder headerFile) throws ParseException { String generatedName = getGeneratedName(clazz); CodeGenerator generator = new CodeGenerator(new ClassWithOptions(clazz, generatedName, /* ifdef */ "")); // Forward declaration for nested classes ClassWithOptions[] nestedClasses = clsInfo.mNestedClasses.stream() .map( nestedClass -> new ClassWithOptions(nestedClass, getGeneratedName(nestedClass), null)) .sorted((a, b) -> a.generatedName.compareTo(b.generatedName)) .toArray(ClassWithOptions[]::new); generator.generateClasses(nestedClasses); generateMembers(generator, clsInfo, sortAndFilterMembers(clazz, clazz.getConstructors())); generateMembers(generator, clsInfo, sortAndFilterMembers(clazz, clazz.getMethods())); generateMembers(generator, clsInfo, sortAndFilterMembers(clazz, clazz.getFields())); headerFile.append(generator.getHeaderFileContents()); implementationFile.append(generator.getWrapperFileContents()); } private static ClassInfo[] getClassList(BufferedReader reader) throws ParseException, IOException { final ArrayList classes = new ArrayList<>(); ClassInfo currentClass = null; String line; while ((line = reader.readLine()) != null) { line = line.trim(); if (line.isEmpty()) { continue; } switch (line.charAt(0)) { case ';': case '#': // Comment continue; case '[': // New section if (line.charAt(line.length() - 1) != ']') { throw new ParseException("Missing trailing ']': " + line); } currentClass = new ClassInfo(line.substring(1, line.length() - 1)); classes.add(currentClass); break; default: // New mapping if (currentClass == null) { throw new ParseException("Missing class: " + line); } currentClass.addAnnotation(line); break; } } if (classes.isEmpty()) { throw new ParseException("No class found in config file"); } return classes.toArray(new ClassInfo[classes.size()]); } private static ClassInfo[] getClassList(final String path) throws ParseException, IOException { BufferedReader reader = null; try { reader = new BufferedReader(new FileReader(path)); return getClassList(reader); } finally { if (reader != null) { reader.close(); } } } /** * For each nested class we wish to generate bindings for, this ensures that the generated binding * for its outer class (recursively, until we reach a top-level class) will contain a forward * declaration of the nested class. */ private static ClassInfo[] addNestedClassForwardDeclarations( final ClassInfo[] classes, final ClassLoader loader) throws ClassNotFoundException { final HashMap classMap = new HashMap<>(); for (final ClassInfo cls : classes) { classMap.put(cls.name, cls); } for (final ClassInfo classInfo : classes) { Class innerClass = Class.forName(classInfo.name, true, loader); while (innerClass.getDeclaringClass() != null) { Class outerClass = innerClass.getDeclaringClass(); ClassInfo outerClassInfo = classMap.get(outerClass.getName()); if (outerClassInfo == null) { // If there isn't already a ClassInfo object for the outer class then we must insert one. // This ensures that we actually generate a declaration for the outer class, in which we // can forward-declare the inner class. "skip:true" ensures we do not generate bindings // for the outer class' member's, as we simply want to forward declare the inner class. outerClassInfo = new ClassInfo(String.format("%s = skip:true", outerClass.getName())); classMap.put(outerClass.getName(), outerClassInfo); } // Add the inner class to the outer class' mNestedClasses, ensuring the outer class' // generated code will forward-declare the inner class. outerClassInfo.mNestedClasses.add(innerClass); innerClass = outerClass; } } // Sort to ensure we generate the classes in a deterministic order, and that outer classes are // declared before nested classes. return classMap.values().stream() .sorted((a, b) -> a.name.compareTo(b.name)) .toArray(ClassInfo[]::new); } private static void writeOutputFiles( String aOutputDir, String aPrefix, StringBuilder aHeaderFile, StringBuilder aImplementationFile) { FileOutputStream implStream = null; try { implStream = new FileOutputStream(new File(aOutputDir, aPrefix + ".cpp")); implStream.write(aImplementationFile.toString().getBytes()); } catch (IOException e) { System.err.println("Unable to write " + aOutputDir + ". Perhaps a permissions issue?"); e.printStackTrace(System.err); } finally { if (implStream != null) { try { implStream.close(); } catch (IOException e) { System.err.println("Unable to close implStream due to " + e); e.printStackTrace(System.err); } } } FileOutputStream headerStream = null; try { headerStream = new FileOutputStream(new File(aOutputDir, aPrefix + ".h")); headerStream.write(aHeaderFile.toString().getBytes()); } catch (IOException e) { System.err.println("Unable to write " + aOutputDir + ". Perhaps a permissions issue?"); e.printStackTrace(System.err); } finally { if (headerStream != null) { try { headerStream.close(); } catch (IOException e) { System.err.println("Unable to close headerStream due to " + e); e.printStackTrace(System.err); } } } } }