зеркало из https://github.com/mozilla/gecko-dev.git
517 строки
21 KiB
Java
517 строки
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) {
|
|
return a.getName().compareTo(b.getName());
|
|
}
|
|
});
|
|
|
|
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));
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|