gecko-dev/build/annotationProcessors/SDKProcessor.java
Mike Hommey b25c52c92a Bug 1428989 - Generate Android bindings more deterministically. r=nalexander
There are multiple methods with the same name and that differ in their
arguments. They end up being ordered in the source file randomly,
despite there being some sorting done, because the sorting was only done
on the method name.

Now, when the method name matches, also compare the arguments.

--HG--
extra : rebase_source : a89b8c9dbad1d7506e0068119ba25cd34150bafc
2018-01-09 16:44:27 +09:00

530 lines
21 KiB
Java

/* 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 <sdkjar> <configfile> <outdir> <fileprefix> <max-sdk-version>
*
* <sdkjar>: jar file containing the SDK classes (e.g. android.jar)
* <configfile>: config file for generating bindings
* <outdir>: output directory for generated binding files
* <fileprefix>: prefix used for generated binding files
* <max-sdk-version>: SDK version for generated class members (bindings will not be
* generated for members with SDK versions higher than max-sdk-version)
*
* The 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
* <init>(Landroid/os/Bundle;)V = skip:true
*
* # Generic member types can be specified
* [android.view.KeyEvent = skip:true]
* # Skip everything except fields
* <field> = 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)
* <init>(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.checks.ApiLookup;
import com.android.tools.lint.LintCliClient;
import org.mozilla.gecko.annotationProcessors.classloader.AnnotatableEntity;
import org.mozilla.gecko.annotationProcessors.classloader.ClassWithOptions;
import org.mozilla.gecko.annotationProcessors.classloader.IterableJarLoadingURLClassLoader;
import org.mozilla.gecko.annotationProcessors.utils.GeneratableElementIterator;
import org.mozilla.gecko.annotationProcessors.utils.Utils;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
import java.net.URL;
import java.net.URLClassLoader;
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;
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;
public ClassInfo(final String text) {
final String[] mapping = text.split("=", 2);
name = mapping[0].trim();
mDefaultAnnotation = mapping.length > 1 ? mapping[1].trim() : null;
}
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 < 5) {
System.err.println("Usage: java SDKProcessor sdkjar configfile outdir fileprefix max-sdk-version");
System.exit(1);
}
System.out.println("Processing platform bindings...");
String sdkJar = args[0];
String outdir = args[2];
String generatedFilePrefix = args[3];
sMaxSdkVersion = Integer.parseInt(args[4]);
LintCliClient lintClient = new LintCliClient();
sApiLookup = ApiLookup.get(lintClient);
// 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[] { new URL("file://" + sdkJar) },
SDKProcessor.class.getClassLoader());
} catch (Exception e) {
throw new RuntimeException(e.toString());
}
try {
final ClassInfo[] classes = getClassList(args[1]);
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: " + args[1]);
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.getCallVersion(
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 void generateClass(Class<?> clazz,
ClassInfo clsInfo,
StringBuilder implementationFile,
StringBuilder headerFile) throws ParseException {
String generatedName = clazz.getSimpleName();
CodeGenerator generator = new CodeGenerator(
new ClassWithOptions(clazz, generatedName, /* ifdef */ ""));
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();
}
}
}
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);
}
}
}
}
}