diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /mobile/android/annotations/src/main/java/org/mozilla/gecko/annotationProcessors/SDKProcessor.java | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
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.java | 578 |
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); + } + } + } + } +} |