summaryrefslogtreecommitdiffstats
path: root/mobile/android/annotations/src/main/java/org/mozilla/gecko/annotationProcessors/SDKProcessor.java
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/annotations/src/main/java/org/mozilla/gecko/annotationProcessors/SDKProcessor.java')
-rw-r--r--mobile/android/annotations/src/main/java/org/mozilla/gecko/annotationProcessors/SDKProcessor.java578
1 files changed, 578 insertions, 0 deletions
diff --git a/mobile/android/annotations/src/main/java/org/mozilla/gecko/annotationProcessors/SDKProcessor.java b/mobile/android/annotations/src/main/java/org/mozilla/gecko/annotationProcessors/SDKProcessor.java
new file mode 100644
index 0000000000..c0a69de49c
--- /dev/null
+++ b/mobile/android/annotations/src/main/java/org/mozilla/gecko/annotationProcessors/SDKProcessor.java
@@ -0,0 +1,578 @@
+/* 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.
+ *
+ * <p>java SDKProcessor <sdkjar> <max-sdk-version> <outdir> [<configfile> <fileprefix>]+
+ *
+ * <p><sdkjar>: jar file containing the SDK classes (e.g. android.jar) <max-sdk-version>: SDK
+ * version for generated class members (bindings will not be generated for members with SDK versions
+ * higher than max-sdk-version) <outdir>: output directory for generated binding files <configfile>:
+ * config file for generating bindings <fileprefix>: prefix used for generated binding files
+ *
+ * <p>Each config file is a text file following the .ini format:
+ *
+ * <p>; comment [section1] property = value
+ *
+ * <p># comment [section2] property = value
+ *
+ * <p>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,
+ *
+ * <p># Generate bindings for Bundle using default options: [android.os.Bundle]
+ *
+ * <p># Generate bindings for Bundle using class options: [android.os.Bundle =
+ * exceptionMode:nsresult]
+ *
+ * <p># Generate bindings for Bundle using method options: [android.os.Bundle] putInt =
+ * stubName:PutInteger
+ *
+ * <p># 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
+ *
+ * <p># Overloded methods can be specified using its signature [android.os.Bundle] # Skip the copy
+ * constructor <init>(Landroid/os/Bundle;)V = skip:true
+ *
+ * <p># Generic member types can be specified [android.view.KeyEvent = skip:true] # Skip everything
+ * except fields <field> = skip:false
+ *
+ * <p># Skip everything except putInt and putChar [android.os.Bundle = skip:true] putInt =
+ * skip:false putChar =
+ *
+ * <p># Avoid conflicts in native bindings [android.os.Bundle] # Bundle(PersistableBundle) native
+ * binding can conflict with Bundle(ClassLoader) <init>(Landroid/os/PersistableBundle;)V =
+ * stubName:NewFromPersistableBundle
+ *
+ * <p># 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<String, String> mAnnotations = new HashMap<>();
+ // Default set of annotation values to use.
+ private final String mDefaultAnnotation;
+ // List of nested classes to forward declare.
+ private final ArrayList<Class<?>> 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 <constructor>, <field>, <method>
+ 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<ClassWithOptions> 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<Member>() {
+ @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<Member> 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<String> 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<ClassInfo> 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<String, ClassInfo> 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);
+ }
+ }
+ }
+ }
+}