diff options
Diffstat (limited to 'mobile/android/annotations')
12 files changed, 2858 insertions, 0 deletions
diff --git a/mobile/android/annotations/build.gradle b/mobile/android/annotations/build.gradle new file mode 100644 index 0000000000..59afd14028 --- /dev/null +++ b/mobile/android/annotations/build.gradle @@ -0,0 +1,14 @@ +buildDir "${topobjdir}/gradle/build/mobile/android/annotations" + +apply plugin: 'java' + +// lint should be X+23.Y.Z of gradle_plugin version, according to: +// http://googlesamples.github.io/android-custom-lint-rules/api-guide.html#example:samplelintcheckgithubproject/lintversion? + +dependencies { + implementation 'com.android.tools.lint:lint:30.4.2' + implementation 'com.android.tools.lint:lint-checks:30.4.2' +} + +sourceCompatibility = JavaVersion.VERSION_11 +targetCompatibility = JavaVersion.VERSION_11 diff --git a/mobile/android/annotations/src/main/java/org/mozilla/gecko/annotationProcessors/AnnotationInfo.java b/mobile/android/annotations/src/main/java/org/mozilla/gecko/annotationProcessors/AnnotationInfo.java new file mode 100644 index 0000000000..0404a467f5 --- /dev/null +++ b/mobile/android/annotations/src/main/java/org/mozilla/gecko/annotationProcessors/AnnotationInfo.java @@ -0,0 +1,59 @@ +/* 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; + +/** Object holding annotation data. Used by GeneratableElementIterator. */ +public class AnnotationInfo { + public enum ExceptionMode { + ABORT, + NSRESULT, + IGNORE; + + String nativeValue() { + return "mozilla::jni::ExceptionMode::" + name(); + } + } + + public enum CallingThread { + GECKO, + UI, + ANY; + + String nativeValue() { + return "mozilla::jni::CallingThread::" + name(); + } + } + + public enum DispatchTarget { + GECKO, + GECKO_PRIORITY, + PROXY, + CURRENT; + + String nativeValue() { + return "mozilla::jni::DispatchTarget::" + name(); + } + } + + public final String wrapperName; + public final ExceptionMode exceptionMode; + public final CallingThread callingThread; + public final DispatchTarget dispatchTarget; + public final boolean noLiteral; + + public AnnotationInfo( + String wrapperName, + ExceptionMode exceptionMode, + CallingThread callingThread, + DispatchTarget dispatchTarget, + boolean noLiteral) { + + this.wrapperName = wrapperName; + this.exceptionMode = exceptionMode; + this.callingThread = callingThread; + this.dispatchTarget = dispatchTarget; + this.noLiteral = noLiteral; + } +} diff --git a/mobile/android/annotations/src/main/java/org/mozilla/gecko/annotationProcessors/AnnotationProcessor.java b/mobile/android/annotations/src/main/java/org/mozilla/gecko/annotationProcessors/AnnotationProcessor.java new file mode 100644 index 0000000000..8db77eed0b --- /dev/null +++ b/mobile/android/annotations/src/main/java/org/mozilla/gecko/annotationProcessors/AnnotationProcessor.java @@ -0,0 +1,231 @@ +/* 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; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.util.Arrays; +import java.util.Iterator; +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; + +public class AnnotationProcessor { + private static final String NATIVES_NAME = "Natives"; + private static final String WRAPPERS_NAME = "Wrappers"; + private static final String EXPORT_PREFIX = "mozilla/java/"; + + 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 Java methods and rerun the build. Manually updating this file\n" + + "// will cause your build to fail.\n" + + "\n"; + + public static void main(String[] args) { + // We expect a list of jars on the commandline. If missing, whinge about it. + if (args.length < 2) { + System.err.println("Usage: java AnnotationProcessor outprefix jarfiles ..."); + System.exit(1); + } + + final String OUTPUT_PREFIX = args[0]; + final String QUALIFIER = OUTPUT_PREFIX + "JNI"; + + (new File(QUALIFIER)).mkdir(); + + System.out.println("Processing annotations..."); + + // We want to produce the same output as last time as often as possible. Ordering of + // generated statements, therefore, needs to be consistent. + final String[] jars = Arrays.copyOfRange(args, 1, args.length); + Arrays.sort(jars); + + // Start the clock! + long s = System.currentTimeMillis(); + + int ret = 0; + + // Get an iterator over the classes in the jar files given... + Iterator<ClassWithOptions> jarClassIterator = + IterableJarLoadingURLClassLoader.getIteratorOverJars(jars); + + while (jarClassIterator.hasNext()) { + final ClassWithOptions annotatedClass = jarClassIterator.next(); + if (!annotatedClass.hasGenerated()) { + continue; + } + + final String sourceFileName = + QUALIFIER + annotatedClass.generatedName + WRAPPERS_NAME + ".cpp"; + final String headerFileName = + QUALIFIER + File.separator + annotatedClass.generatedName + WRAPPERS_NAME + ".h"; + final String headerExportedFileName = + EXPORT_PREFIX + annotatedClass.generatedName + WRAPPERS_NAME + ".h"; + final String nativesFileName = + QUALIFIER + File.separator + annotatedClass.generatedName + NATIVES_NAME + ".h"; + final String nativesExportedFileName = + EXPORT_PREFIX + annotatedClass.generatedName + NATIVES_NAME + ".h"; + + final StringBuilder headerFile = new StringBuilder(GENERATED_COMMENT); + final StringBuilder implementationFile = new StringBuilder(GENERATED_COMMENT); + final StringBuilder nativesFile = new StringBuilder(GENERATED_COMMENT); + + headerFile.append( + "#ifndef " + + getHeaderGuardName(headerExportedFileName) + + "\n" + + "#define " + + getHeaderGuardName(headerExportedFileName) + + "\n" + + "\n" + + "#ifndef MOZ_PREPROCESSOR\n" + + "#include \"mozilla/jni/Refs.h\"\n" + + "#endif\n" + + "\n" + + "namespace mozilla {\n" + + "namespace java {\n" + + "\n"); + + implementationFile.append( + "#ifndef MOZ_PREPROCESSOR\n" + + "#include \"" + + headerExportedFileName + + "\"\n" + + "#include \"mozilla/jni/Accessors.h\"\n" + + "#endif\n" + + "\n" + + "namespace mozilla {\n" + + "namespace java {\n" + + "\n"); + + nativesFile.append( + "#ifndef " + + getHeaderGuardName(nativesExportedFileName) + + "\n" + + "#define " + + getHeaderGuardName(nativesExportedFileName) + + "\n" + + "\n" + + "#ifndef MOZ_PREPROCESSOR\n" + + "#include \"" + + headerExportedFileName + + "\"\n" + + "#include \"mozilla/jni/Natives.h\"\n" + + "#endif\n" + + "\n" + + "namespace mozilla {\n" + + "namespace java {\n" + + "\n"); + + generateClass(annotatedClass, headerFile, implementationFile, nativesFile); + + implementationFile.append("} /* java */\n" + "} /* mozilla */\n"); + + headerFile.append( + "} /* java */\n" + + "} /* mozilla */\n\n" + + "#endif // " + + getHeaderGuardName(headerExportedFileName) + + "\n"); + + nativesFile.append( + "} /* java */\n" + + "} /* mozilla */\n\n" + + "#endif // " + + getHeaderGuardName(nativesExportedFileName) + + "\n"); + + ret |= writeOutputFile(sourceFileName, implementationFile); + ret |= writeOutputFile(headerFileName, headerFile); + ret |= writeOutputFile(nativesFileName, nativesFile); + } + long e = System.currentTimeMillis(); + System.out.println("Annotation processing complete in " + (e - s) + "ms"); + + System.exit(ret); + } + + private static void generateClass( + final ClassWithOptions annotatedClass, + final StringBuilder headerFile, + final StringBuilder implementationFile, + final StringBuilder nativesFile) { + // Get an iterator over the appropriately generated methods of this class + final GeneratableElementIterator methodIterator = + new GeneratableElementIterator(annotatedClass); + final ClassWithOptions[] innerClasses = methodIterator.getInnerClasses(); + + final CodeGenerator generatorInstance = new CodeGenerator(annotatedClass); + generatorInstance.generateClasses(innerClasses); + + // Iterate all annotated members in this class.. + while (methodIterator.hasNext()) { + AnnotatableEntity aElementTuple = methodIterator.next(); + switch (aElementTuple.mEntityType) { + case METHOD: + generatorInstance.generateMethod(aElementTuple); + break; + case NATIVE: + generatorInstance.generateNative(aElementTuple); + break; + case FIELD: + generatorInstance.generateField(aElementTuple); + break; + case CONSTRUCTOR: + generatorInstance.generateConstructor(aElementTuple); + break; + } + } + + headerFile.append(generatorInstance.getHeaderFileContents()); + implementationFile.append(generatorInstance.getWrapperFileContents()); + nativesFile.append(generatorInstance.getNativesFileContents()); + + for (ClassWithOptions innerClass : innerClasses) { + generateClass(innerClass, headerFile, implementationFile, nativesFile); + } + } + + private static String getHeaderGuardName(final String name) { + return name.replaceAll("\\W", "_"); + } + + private static int writeOutputFile(final String name, final StringBuilder content) { + final byte[] contentBytes = content.toString().getBytes(StandardCharsets.UTF_8); + + try { + final byte[] existingBytes = Files.readAllBytes(new File(name).toPath()); + if (Arrays.equals(contentBytes, existingBytes)) { + return 0; + } + } catch (FileNotFoundException e) { + // Pass. + } catch (NoSuchFileException e) { + // Pass. + } catch (IOException e) { + System.err.println("Unable to read " + name + ". Perhaps a permissions issue?"); + e.printStackTrace(System.err); + return 1; + } + + try (FileOutputStream outStream = new FileOutputStream(name)) { + outStream.write(contentBytes); + } catch (IOException e) { + System.err.println("Unable to write " + name + ". Perhaps a permissions issue?"); + e.printStackTrace(System.err); + return 1; + } + + return 0; + } +} diff --git a/mobile/android/annotations/src/main/java/org/mozilla/gecko/annotationProcessors/CodeGenerator.java b/mobile/android/annotations/src/main/java/org/mozilla/gecko/annotationProcessors/CodeGenerator.java new file mode 100644 index 0000000000..d39b074f50 --- /dev/null +++ b/mobile/android/annotations/src/main/java/org/mozilla/gecko/annotationProcessors/CodeGenerator.java @@ -0,0 +1,839 @@ +/* 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; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.util.HashSet; +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 CodeGenerator { + private static final Class<?>[] EMPTY_CLASS_ARRAY = new Class<?>[0]; + + // Buffers holding the strings to ultimately be written to the output files. + private final StringBuilder cpp = new StringBuilder(); + private final StringBuilder header = new StringBuilder(); + private final StringBuilder natives = new StringBuilder(); + private final StringBuilder nativesInits = new StringBuilder(); + + private final Class<?> cls; + private final String clsName; + private final ClassWithOptions options; + private AnnotationInfo.CallingThread callingThread = null; + private int numNativesInits; + + private final HashSet<String> takenMethodNames = new HashSet<String>(); + + public CodeGenerator(ClassWithOptions annotatedClass) { + this.cls = annotatedClass.wrappedClass; + this.clsName = annotatedClass.generatedName; + this.options = annotatedClass; + + final String unqualifiedName = Utils.getUnqualifiedName(clsName); + header.append( + Utils.getIfdefHeader(annotatedClass.ifdef) + + "class " + + clsName + + " : public mozilla::jni::ObjectBase<" + + unqualifiedName + + ">\n" + + "{\n" + + "public:\n" + + " static const char name[];\n" + + "\n" + + " explicit " + + unqualifiedName + + "(const Context& ctx) : ObjectBase<" + + unqualifiedName + + ">(ctx) {}\n" + + "\n"); + + cpp.append( + Utils.getIfdefHeader(annotatedClass.ifdef) + + "const char " + + clsName + + "::name[] =\n" + + " \"" + + cls.getName().replace('.', '/') + + "\";\n" + + "\n"); + + natives.append( + Utils.getIfdefHeader(annotatedClass.ifdef) + + "template<class Impl>\n" + + "class " + + clsName + + "::Natives : " + + "public mozilla::jni::NativeImpl<" + + unqualifiedName + + ", Impl>\n" + + "{\n" + + "public:\n"); + } + + private String getTraitsName(String uniqueName, boolean includeScope) { + return (includeScope ? clsName + "::" : "") + uniqueName + "_t"; + } + + /** + * Return the C++ type name for this class or any class within the chain of declaring classes, if + * the target class matches the given type. + * + * <p>Return null if the given type does not match any class searched. + */ + private String getMatchingClassType(final Class<?> type) { + Class<?> cls = this.cls; + String clsName = this.clsName; + + while (cls != null) { + if (type.equals(cls)) { + return clsName; + } + cls = cls.getDeclaringClass(); + clsName = clsName.substring(0, Math.max(0, clsName.lastIndexOf("::"))); + } + return null; + } + + private String getNativeParameterType(Class<?> type, AnnotationInfo info) { + final String clsName = getMatchingClassType(type); + if (clsName != null) { + return Utils.getUnqualifiedName(clsName) + "::Param"; + } + return Utils.getNativeParameterType(type, info); + } + + private String getNativeReturnType(Class<?> type, AnnotationInfo info) { + final String clsName = getMatchingClassType(type); + if (clsName != null) { + return Utils.getUnqualifiedName(clsName) + "::LocalRef"; + } + return Utils.getNativeReturnType(type, info); + } + + private void generateMember( + AnnotationInfo info, Member member, String uniqueName, Class<?> type, Class<?>[] argTypes) { + // Sanity check. + if (info.noLiteral + && !(member instanceof Field && Utils.isStatic(member) && Utils.isFinal(member))) { + throw new IllegalStateException(clsName + "::" + uniqueName + " is not a static final field"); + } + + final StringBuilder args = new StringBuilder(); + for (Class<?> argType : argTypes) { + args.append("\n " + getNativeParameterType(argType, info) + ","); + } + if (args.length() > 0) { + args.setLength(args.length() - 1); + } + + header.append( + " struct " + + getTraitsName(uniqueName, /* includeScope */ false) + + " {\n" + + " typedef " + + Utils.getUnqualifiedName(clsName) + + " Owner;\n" + + " typedef " + + getNativeReturnType(type, info) + + " ReturnType;\n" + + " typedef " + + getNativeParameterType(type, info) + + " SetterType;\n" + + " typedef mozilla::jni::Args<" + + args + + "> Args;\n" + + " static constexpr char name[] = \"" + + Utils.getMemberName(member) + + "\";\n" + + " static constexpr char signature[] =\n" + + " \"" + + Utils.getSignature(member) + + "\";\n" + + " static const bool isStatic = " + + Utils.isStatic(member) + + ";\n" + + " static const mozilla::jni::ExceptionMode exceptionMode =\n" + + " " + + info.exceptionMode.nativeValue() + + ";\n" + + " static const mozilla::jni::CallingThread callingThread =\n" + + " " + + info.callingThread.nativeValue() + + ";\n" + + " static const mozilla::jni::DispatchTarget dispatchTarget =\n" + + " " + + info.dispatchTarget.nativeValue() + + ";\n" + + " };\n" + + "\n"); + + cpp.append( + "constexpr char " + + getTraitsName(uniqueName, /* includeScope */ true) + + "::name[];\n" + + "constexpr char " + + getTraitsName(uniqueName, /* includeScope */ true) + + "::signature[];\n" + + "\n"); + + if (this.callingThread == null) { + this.callingThread = info.callingThread; + } else if (this.callingThread != info.callingThread) { + // We have a mix of calling threads, so specify "any" for the whole class. + this.callingThread = AnnotationInfo.CallingThread.ANY; + } + } + + private String getUniqueMethodName(String basename) { + String newName = basename; + int index = 1; + + while (takenMethodNames.contains(newName)) { + newName = basename + (++index); + } + + takenMethodNames.add(newName); + return newName; + } + + /** + * Generate a method prototype that includes return and argument types, without specifiers + * (static, const, etc.). + */ + private String generatePrototype( + String name, + Class<?>[] argTypes, + Class<?> returnType, + AnnotationInfo info, + boolean includeScope, + boolean includeArgName, + boolean isConst) { + + final StringBuilder proto = new StringBuilder(); + int argIndex = 0; + + proto.append("auto "); + + if (includeScope) { + proto.append(clsName).append("::"); + } + + proto.append(name).append('('); + + for (Class<?> argType : argTypes) { + proto.append(getNativeParameterType(argType, info)); + if (includeArgName) { + proto.append(" a").append(argIndex++); + } + proto.append(", "); + } + + if (info.exceptionMode == AnnotationInfo.ExceptionMode.NSRESULT + && !returnType.equals(void.class)) { + proto.append(getNativeReturnType(returnType, info)).append('*'); + if (includeArgName) { + proto.append(" a").append(argIndex++); + } + proto.append(", "); + } + + if (proto.substring(proto.length() - 2).equals(", ")) { + proto.setLength(proto.length() - 2); + } + + proto.append(')'); + + if (isConst) { + proto.append(" const"); + } + + if (info.exceptionMode == AnnotationInfo.ExceptionMode.NSRESULT) { + proto.append(" -> nsresult"); + } else { + proto.append(" -> ").append(getNativeReturnType(returnType, info)); + } + return proto.toString(); + } + + /** + * Generate a method declaration that includes the prototype with specifiers, but without the + * method body. + */ + private String generateDeclaration( + String name, + Class<?>[] argTypes, + Class<?> returnType, + AnnotationInfo info, + boolean isStatic) { + + return (isStatic ? "static " : "") + + generatePrototype( + name, + argTypes, + returnType, + info, + /* includeScope */ false, /* includeArgName */ + false, + /* isConst */ !isStatic) + + ';'; + } + + /** + * Generate a method definition that includes the prototype with specifiers, and with the method + * body. + */ + private String generateDefinition( + String accessorName, + String name, + Class<?>[] argTypes, + Class<?> returnType, + AnnotationInfo info, + boolean isStatic) { + + final StringBuilder def = + new StringBuilder( + generatePrototype( + name, + argTypes, + returnType, + info, + /* includeScope */ true, /* includeArgName */ + true, + /* isConst */ !isStatic)); + def.append("\n{\n"); + + // Generate code to handle the return value, if needed. + // We initialize rv to NS_OK instead of NS_ERROR_* because loading NS_OK (0) uses + // fewer instructions. We are guaranteed to set rv to the correct value later. + + if (info.exceptionMode == AnnotationInfo.ExceptionMode.NSRESULT + && returnType.equals(void.class)) { + def.append(" nsresult rv = NS_OK;\n" + " "); + + } else if (info.exceptionMode == AnnotationInfo.ExceptionMode.NSRESULT) { + // Non-void return type + final String resultArg = "a" + argTypes.length; + def.append( + " MOZ_ASSERT(" + + resultArg + + ");\n" + + " nsresult rv = NS_OK;\n" + + " *" + + resultArg + + " = "); + + } else { + def.append(" return "); + } + + // Generate a call, e.g., Method<Traits>::Call(a0, a1, a2); + + def.append(accessorName) + .append("(") + .append(Utils.getUnqualifiedName(clsName) + (isStatic ? "::Context()" : "::mCtx")); + + if (info.exceptionMode == AnnotationInfo.ExceptionMode.NSRESULT) { + def.append(", &rv"); + } else { + def.append(", nullptr"); + } + + // Generate the call argument list. + for (int argIndex = 0; argIndex < argTypes.length; argIndex++) { + def.append(", a").append(argIndex); + } + + def.append(");\n"); + + if (info.exceptionMode == AnnotationInfo.ExceptionMode.NSRESULT) { + def.append(" return rv;\n"); + } + + return def.append("}").toString(); + } + + private static void appendParameterList( + final StringBuilder builder, final Class<?> genScope, final Class<?> paramTypes[]) { + builder.append("("); + + final int maxParamIndex = paramTypes.length - 1; + + for (int i = 0; i < paramTypes.length; ++i) { + builder.append(Utils.getSimplifiedJavaClassName(genScope, paramTypes[i])); + if (i < maxParamIndex) { + builder.append(", "); + } + } + + builder.append(")"); + } + + /** + * This method generates a comment for C++ headers containing a simplified form of a method's Java + * signature. This is entirely for informational purposes to assist developers with disambiguating + * arguments to the native wrappers. + */ + private static String generateJavaStyleMethodSignatureHint( + final Method method, final boolean isStatic) { + final StringBuilder builder = new StringBuilder(" // "); + + if (isStatic) { + builder.append("static "); + } + + final Class<?> declaringClass = method.getDeclaringClass(); + + builder + .append(Utils.getSimplifiedJavaClassName(declaringClass, method.getReturnType())) + .append(" ") + .append(method.getName()); + + appendParameterList(builder, declaringClass, method.getParameterTypes()); + + builder.append("\n"); + return builder.toString(); + } + + /** + * This method generates a comment for C++ headers containing a simplified form of a + * constructors's Java signature. This is entirely for informational purposes to assist developers + * with disambiguating arguments to the native wrappers. + */ + private static String generateJavaStyleConstructorSignatureHint( + final Constructor<?> constructor) { + final StringBuilder builder = new StringBuilder(" // "); + + final Class<?> declaringClass = constructor.getDeclaringClass(); + + builder.append(declaringClass.getSimpleName()); + + appendParameterList(builder, declaringClass, constructor.getParameterTypes()); + + builder.append("\n"); + return builder.toString(); + } + + /** + * Append the appropriate generated code to the buffers for the method provided. + * + * @param annotatedMethod The Java method, plus annotation data. + */ + public void generateMethod(AnnotatableEntity annotatedMethod) { + // Unpack the tuple and extract some useful fields from the Method.. + final Method method = annotatedMethod.getMethod(); + final AnnotationInfo info = annotatedMethod.mAnnotationInfo; + final String uniqueName = getUniqueMethodName(info.wrapperName); + final Class<?>[] argTypes = method.getParameterTypes(); + final Class<?> returnType = method.getReturnType(); + + if (method.isSynthetic()) { + return; + } + + // Sanity check + if (info.dispatchTarget != AnnotationInfo.DispatchTarget.CURRENT) { + throw new IllegalStateException( + "Invalid dispatch target \"" + + info.dispatchTarget.name().toLowerCase(Locale.ROOT) + + "\" for non-native method " + + clsName + + "::" + + uniqueName); + } + + generateMember(info, method, uniqueName, returnType, argTypes); + + final boolean isStatic = Utils.isStatic(method); + + header.append(generateJavaStyleMethodSignatureHint(method, isStatic)); + + header.append( + " " + + generateDeclaration(info.wrapperName, argTypes, returnType, info, isStatic) + + "\n" + + "\n"); + + cpp.append( + generateDefinition( + "mozilla::jni::Method<" + + getTraitsName(uniqueName, /* includeScope */ false) + + ">::Call", + info.wrapperName, + argTypes, + returnType, + info, + isStatic) + + "\n" + + "\n"); + } + + /** + * Append the appropriate generated code to the buffers for the native method provided. + * + * @param annotatedMethod The Java native method, plus annotation data. + */ + public void generateNative(AnnotatableEntity annotatedMethod) { + // Unpack the tuple and extract some useful fields from the Method.. + final Method method = annotatedMethod.getMethod(); + final AnnotationInfo info = annotatedMethod.mAnnotationInfo; + final String uniqueName = getUniqueMethodName(info.wrapperName); + final Class<?>[] argTypes = method.getParameterTypes(); + final Class<?> returnType = method.getReturnType(); + + // Sanity check + if (info.exceptionMode != AnnotationInfo.ExceptionMode.ABORT + && info.exceptionMode != AnnotationInfo.ExceptionMode.IGNORE) { + throw new IllegalStateException( + "Invalid exception mode \"" + + info.exceptionMode.name().toLowerCase(Locale.ROOT) + + "\" for native method " + + clsName + + "::" + + uniqueName); + } + if (info.dispatchTarget != AnnotationInfo.DispatchTarget.CURRENT && returnType != void.class) { + throw new IllegalStateException( + "Must return void when not dispatching to current thread for native method " + + clsName + + "::" + + uniqueName); + } + + generateNativeSignatureHint(info, method, uniqueName, returnType, argTypes); + generateMember(info, method, uniqueName, returnType, argTypes); + + final String traits = getTraitsName(uniqueName, /* includeScope */ true); + + if (nativesInits.length() > 0) { + nativesInits.append(','); + } + + nativesInits.append( + "\n" + + "\n" + + " mozilla::jni::MakeNativeMethod<" + + traits + + ">(\n" + + " mozilla::jni::NativeStub<" + + traits + + ", Impl>\n" + + " ::template Wrap<&Impl::" + + info.wrapperName + + ">)"); + numNativesInits++; + } + + private void generateNativeSignatureHint( + AnnotationInfo info, + Member member, + String uniqueName, + Class<?> returnType, + Class<?>[] argTypes) { + final StringBuilder hint = + new StringBuilder(" // Suggested header signature for native method:\n // "); + + if (Utils.isStatic(member)) { + hint.append("static "); + } + + hint.append(Utils.getNativeReturnTypeHint(returnType, info)) + .append(" ") + .append(uniqueName) + .append("("); + + final int maxParamIndex = argTypes.length - 1; + + for (int i = 0; i < argTypes.length; ++i) { + hint.append(Utils.getNativeParameterTypeHint(argTypes[i], info)); + if (i < maxParamIndex) { + hint.append(", "); + } + } + + hint.append(");\n"); + + header.append(hint.toString()); + } + + private String getLiteral(Object val, AnnotationInfo info) { + final Class<?> type = val.getClass(); + + if (type.equals(char.class) || type.equals(Character.class)) { + final char c = (char) val; + if (c >= 0x20 && c < 0x7F) { + return "'" + c + '\''; + } + return "u'\\u" + Integer.toHexString(0x10000 | (int) c).substring(1) + '\''; + + } else if (type.equals(CharSequence.class) || type.equals(String.class)) { + final CharSequence str = (CharSequence) val; + final StringBuilder out = new StringBuilder("u\""); + for (int i = 0; i < str.length(); i++) { + final char c = str.charAt(i); + if (c >= 0x20 && c < 0x7F) { + out.append(c); + } else { + out.append("\\u").append(Integer.toHexString(0x10000 | (int) c).substring(1)); + } + } + return out.append('"').toString(); + } + + return String.valueOf(val); + } + + public void generateField(AnnotatableEntity annotatedField) { + final Field field = annotatedField.getField(); + final AnnotationInfo info = annotatedField.mAnnotationInfo; + final String uniqueName = info.wrapperName; + final Class<?> type = field.getType(); + + // Handle various cases where we don't care about the field. + if (field.isSynthetic() + || field.getName().equals("$VALUES") + || field.getName().equals("CREATOR")) { + return; + } + + // Sanity check + if (info.dispatchTarget != AnnotationInfo.DispatchTarget.CURRENT) { + throw new IllegalStateException( + "Invalid dispatch target \"" + + info.dispatchTarget.name().toLowerCase(Locale.ROOT) + + "\" for field " + + clsName + + "::" + + uniqueName); + } + + final boolean isStatic = Utils.isStatic(field); + final boolean isFinal = Utils.isFinal(field); + + if (!info.noLiteral + && isStatic + && isFinal + && (type.isPrimitive() || type.equals(String.class))) { + Object val = null; + try { + field.setAccessible(true); + val = field.get(null); + } catch (final IllegalAccessException e) { + } + + if (val != null && type.isPrimitive()) { + // For static final primitive fields, we can use a "static const" declaration. + header.append( + " static const " + + Utils.getNativeReturnType(type, info) + + ' ' + + info.wrapperName + + " = " + + getLiteral(val, info) + + ";\n" + + "\n"); + return; + + } else if (val != null && type.equals(String.class)) { + final String nativeType = "char16_t"; + + header.append(" static const " + nativeType + ' ' + info.wrapperName + "[];\n" + "\n"); + + cpp.append( + "const " + + nativeType + + ' ' + + clsName + + "::" + + info.wrapperName + + "[] = " + + getLiteral(val, info) + + ";\n" + + "\n"); + return; + } + + // Fall back to using accessors if we encounter an exception. + } + + generateMember(info, field, uniqueName, type, EMPTY_CLASS_ARRAY); + + final Class<?>[] getterArgs = EMPTY_CLASS_ARRAY; + + header.append( + " " + + generateDeclaration(info.wrapperName, getterArgs, type, info, isStatic) + + "\n" + + "\n"); + + cpp.append( + generateDefinition( + "mozilla::jni::Field<" + + getTraitsName(uniqueName, /* includeScope */ false) + + ">::Get", + info.wrapperName, + getterArgs, + type, + info, + isStatic) + + "\n" + + "\n"); + + if (isFinal) { + return; + } + + final Class<?>[] setterArgs = new Class<?>[] {type}; + + header.append( + " " + + generateDeclaration(info.wrapperName, setterArgs, void.class, info, isStatic) + + "\n" + + "\n"); + + cpp.append( + generateDefinition( + "mozilla::jni::Field<" + + getTraitsName(uniqueName, /* includeScope */ false) + + ">::Set", + info.wrapperName, + setterArgs, + void.class, + info, + isStatic) + + "\n" + + "\n"); + } + + public void generateConstructor(AnnotatableEntity annotatedConstructor) { + // Unpack the tuple and extract some useful fields from the Method.. + final Constructor<?> method = annotatedConstructor.getConstructor(); + final AnnotationInfo info = annotatedConstructor.mAnnotationInfo; + final String wrapperName = info.wrapperName.equals("<init>") ? "New" : info.wrapperName; + final String uniqueName = getUniqueMethodName(wrapperName); + final Class<?>[] argTypes = method.getParameterTypes(); + final Class<?> returnType = cls; + + if (method.isSynthetic()) { + return; + } + + // Sanity check + if (info.dispatchTarget != AnnotationInfo.DispatchTarget.CURRENT) { + throw new IllegalStateException( + "Invalid dispatch target \"" + + info.dispatchTarget.name().toLowerCase(Locale.ROOT) + + "\" for constructor " + + clsName + + "::" + + uniqueName); + } + + generateMember(info, method, uniqueName, returnType, argTypes); + + header.append(generateJavaStyleConstructorSignatureHint(method)); + + header.append( + " " + + generateDeclaration(wrapperName, argTypes, returnType, info, /* isStatic */ true) + + "\n" + + "\n"); + + cpp.append( + generateDefinition( + "mozilla::jni::Constructor<" + + getTraitsName(uniqueName, /* includeScope */ false) + + ">::Call", + wrapperName, + argTypes, + returnType, + info, /* isStatic */ + true) + + "\n" + + "\n"); + } + + public void generateClasses(final ClassWithOptions[] classes) { + if (classes.length == 0) { + return; + } + + for (final ClassWithOptions cls : classes) { + // Extract "Inner" from "Outer::Inner". + header.append(" class " + Utils.getUnqualifiedName(cls.generatedName) + ";\n"); + } + header.append('\n'); + } + + /** + * Get the finalised bytes to go into the generated wrappers file. + * + * @return The bytes to be written to the wrappers file. + */ + public String getWrapperFileContents() { + cpp.append(Utils.getIfdefFooter(options.ifdef)); + return cpp.toString(); + } + + private boolean haveNatives() { + return nativesInits.length() > 0 || Utils.isJNIObject(cls); + } + + /** + * Get the finalised bytes to go into the generated header file. + * + * @return The bytes to be written to the header file. + */ + public String getHeaderFileContents() { + if (this.callingThread == null) { + this.callingThread = AnnotationInfo.CallingThread.ANY; + } + + header.append( + " static const mozilla::jni::CallingThread callingThread =\n" + + " " + + this.callingThread.nativeValue() + + ";\n" + + "\n"); + + if (haveNatives()) { + header.append(" template<class Impl> class Natives;\n"); + } + header.append("};\n" + "\n" + Utils.getIfdefFooter(options.ifdef)); + return header.toString(); + } + + /** + * Get the finalised bytes to go into the generated natives header file. + * + * @return The bytes to be written to the header file. + */ + public String getNativesFileContents() { + if (!haveNatives()) { + return ""; + } + natives.append( + " static const JNINativeMethod methods[" + + numNativesInits + + "];\n" + + "};\n" + + "\n" + + "template<class Impl>\n" + + "const JNINativeMethod " + + clsName + + "::Natives<Impl>::methods[] = {" + + nativesInits + + '\n' + + "};\n" + + "\n" + + Utils.getIfdefFooter(options.ifdef)); + return natives.toString(); + } +} 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); + } + } + } + } +} diff --git a/mobile/android/annotations/src/main/java/org/mozilla/gecko/annotationProcessors/classloader/AnnotatableEntity.java b/mobile/android/annotations/src/main/java/org/mozilla/gecko/annotationProcessors/classloader/AnnotatableEntity.java new file mode 100644 index 0000000000..b2df6587c1 --- /dev/null +++ b/mobile/android/annotations/src/main/java/org/mozilla/gecko/annotationProcessors/classloader/AnnotatableEntity.java @@ -0,0 +1,68 @@ +/* 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.classloader; + +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 org.mozilla.gecko.annotationProcessors.AnnotationInfo; + +/** + * Union type to hold either a method, field, or ctor. Allows us to iterate "The generatable stuff", + * despite the fact that such things can be of either flavour. + */ +public class AnnotatableEntity { + public enum ENTITY_TYPE { + METHOD, + NATIVE, + FIELD, + CONSTRUCTOR + } + + private final Member mMember; + public final ENTITY_TYPE mEntityType; + + public final AnnotationInfo mAnnotationInfo; + + public AnnotatableEntity(Member aObject, AnnotationInfo aAnnotationInfo) { + mMember = aObject; + mAnnotationInfo = aAnnotationInfo; + + if (aObject instanceof Method) { + if (Modifier.isNative(aObject.getModifiers())) { + mEntityType = ENTITY_TYPE.NATIVE; + } else { + mEntityType = ENTITY_TYPE.METHOD; + } + } else if (aObject instanceof Field) { + mEntityType = ENTITY_TYPE.FIELD; + } else { + mEntityType = ENTITY_TYPE.CONSTRUCTOR; + } + } + + public Method getMethod() { + if (mEntityType != ENTITY_TYPE.METHOD && mEntityType != ENTITY_TYPE.NATIVE) { + throw new UnsupportedOperationException("Attempt to cast to unsupported member type."); + } + return (Method) mMember; + } + + public Field getField() { + if (mEntityType != ENTITY_TYPE.FIELD) { + throw new UnsupportedOperationException("Attempt to cast to unsupported member type."); + } + return (Field) mMember; + } + + public Constructor<?> getConstructor() { + if (mEntityType != ENTITY_TYPE.CONSTRUCTOR) { + throw new UnsupportedOperationException("Attempt to cast to unsupported member type."); + } + return (Constructor<?>) mMember; + } +} diff --git a/mobile/android/annotations/src/main/java/org/mozilla/gecko/annotationProcessors/classloader/ClassWithOptions.java b/mobile/android/annotations/src/main/java/org/mozilla/gecko/annotationProcessors/classloader/ClassWithOptions.java new file mode 100644 index 0000000000..d1e4e6cfe0 --- /dev/null +++ b/mobile/android/annotations/src/main/java/org/mozilla/gecko/annotationProcessors/classloader/ClassWithOptions.java @@ -0,0 +1,36 @@ +/* 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.classloader; + +import org.mozilla.gecko.annotationProcessors.utils.GeneratableElementIterator; + +public class ClassWithOptions { + public final Class<?> wrappedClass; + public final String generatedName; + public final String ifdef; + + public ClassWithOptions(Class<?> someClass, String name, String ifdef) { + wrappedClass = someClass; + generatedName = name; + this.ifdef = ifdef; + } + + public boolean hasGenerated() { + final GeneratableElementIterator methodIterator = new GeneratableElementIterator(this); + + if (methodIterator.hasNext()) { + return true; + } + + final ClassWithOptions[] innerClasses = methodIterator.getInnerClasses(); + for (ClassWithOptions innerClass : innerClasses) { + if (innerClass.hasGenerated()) { + return true; + } + } + + return false; + } +} diff --git a/mobile/android/annotations/src/main/java/org/mozilla/gecko/annotationProcessors/classloader/IterableJarLoadingURLClassLoader.java b/mobile/android/annotations/src/main/java/org/mozilla/gecko/annotationProcessors/classloader/IterableJarLoadingURLClassLoader.java new file mode 100644 index 0000000000..50200ef3ec --- /dev/null +++ b/mobile/android/annotations/src/main/java/org/mozilla/gecko/annotationProcessors/classloader/IterableJarLoadingURLClassLoader.java @@ -0,0 +1,76 @@ +/* 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.classloader; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Collections; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +/** + * A classloader which can be initialised with a list of jar files and which can provide an iterator + * over the top level classes in the jar files it was initialised with. classNames is kept sorted to + * ensure iteration order is consistent across program invocations. Otherwise, we'd forever be + * reporting the outdatedness of the generated code as we permute its contents. + */ +public class IterableJarLoadingURLClassLoader extends URLClassLoader { + LinkedList<String> classNames = new LinkedList<String>(); + + /** + * Create an instance and return its iterator. Provides an iterator over the classes in the jar + * files provided as arguments. Inner classes are not supported. + * + * @param args A list of jar file names an iterator over the classes of which is desired. + * @return An iterator over the top level classes in the jar files provided, in arbitrary order. + */ + public static Iterator<ClassWithOptions> getIteratorOverJars(String[] args) { + URL[] urlArray = new URL[args.length]; + LinkedList<String> aClassNames = new LinkedList<String>(); + + for (int i = 0; i < args.length; i++) { + try { + urlArray[i] = (new File(args[i])).toURI().toURL(); + + Enumeration<JarEntry> entries = new JarFile(args[i]).entries(); + while (entries.hasMoreElements()) { + JarEntry e = entries.nextElement(); + String fName = e.getName(); + if (!fName.endsWith(".class")) { + continue; + } + final String className = fName.substring(0, fName.length() - 6).replace('/', '.'); + + aClassNames.add(className); + } + } catch (IOException e) { + System.err.println("Error loading jar file \"" + args[i] + '"'); + e.printStackTrace(System.err); + } + } + Collections.sort(aClassNames); + return new JarClassIterator(new IterableJarLoadingURLClassLoader(urlArray, aClassNames)); + } + + /** + * Constructs a classloader capable of loading all classes given as URLs in urls. Used by static + * method above. + * + * @param urls URLs for all classes the new instance shall be capable of loading. + * @param aClassNames A list of names of the classes this instance shall be capable of loading. + */ + protected IterableJarLoadingURLClassLoader( + URL[] urls, + LinkedList<String> + aClassNames) { // Array to populate with URLs for each class in the given jars. + super(urls); + classNames = aClassNames; + } +} diff --git a/mobile/android/annotations/src/main/java/org/mozilla/gecko/annotationProcessors/classloader/JarClassIterator.java b/mobile/android/annotations/src/main/java/org/mozilla/gecko/annotationProcessors/classloader/JarClassIterator.java new file mode 100644 index 0000000000..f93667be8d --- /dev/null +++ b/mobile/android/annotations/src/main/java/org/mozilla/gecko/annotationProcessors/classloader/JarClassIterator.java @@ -0,0 +1,105 @@ +/* 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.classloader; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.Iterator; + +/** + * Class for iterating over an IterableJarLoadingURLClassLoader's classes. + * + * <p>This class is not thread safe: use it only from a single thread. + */ +public class JarClassIterator implements Iterator<ClassWithOptions> { + private IterableJarLoadingURLClassLoader mTarget; + private Iterator<String> mTargetClassListIterator; + + private ClassWithOptions lookAhead; + + public JarClassIterator(IterableJarLoadingURLClassLoader aTarget) { + mTarget = aTarget; + mTargetClassListIterator = aTarget.classNames.iterator(); + } + + @Override + public boolean hasNext() { + return fillLookAheadIfPossible(); + } + + @Override + public ClassWithOptions next() { + if (!fillLookAheadIfPossible()) { + throw new IllegalStateException("Failed to look ahead in next()!"); + } + ClassWithOptions next = lookAhead; + lookAhead = null; + return next; + } + + private boolean fillLookAheadIfPossible() { + if (lookAhead != null) { + return true; + } + + if (!mTargetClassListIterator.hasNext()) { + return false; + } + + String className = mTargetClassListIterator.next(); + try { + Class<?> ret = mTarget.loadClass(className); + + // Incremental builds can leave stale classfiles in the jar. Such classfiles will cause + // an exception at this point. We can safely ignore these classes - they cannot possibly + // ever be loaded as they conflict with their parent class and will be killed by + // Proguard later on anyway. + final Class<?> enclosingClass; + try { + enclosingClass = ret.getEnclosingClass(); + } catch (IncompatibleClassChangeError e) { + return fillLookAheadIfPossible(); + } + + if (enclosingClass != null) { + // Anonymous inner class - unsupported. + // Or named inner class, which will be processed when we process the outer class. + return fillLookAheadIfPossible(); + } + + String ifdef = ""; + for (final Annotation annotation : ret.getDeclaredAnnotations()) { + Class<? extends Annotation> annotationType = annotation.annotationType(); + if (!annotationType.getName().equals("org.mozilla.gecko.annotation.BuildFlag")) { + continue; + } + + try { + final Method valueMethod = annotationType.getDeclaredMethod("value"); + valueMethod.setAccessible(true); + ifdef = (String) valueMethod.invoke(annotation); + break; + } catch (final Exception e) { + System.err.println("Unable to read BuildFlag annotation."); + e.printStackTrace(System.err); + System.exit(1); + } + } + + lookAhead = new ClassWithOptions(ret, ret.getSimpleName(), ifdef); + return true; + } catch (ClassNotFoundException e) { + System.err.println("Unable to enumerate class: " + className + ". Corrupted jar file?"); + e.printStackTrace(); + System.exit(2); + } + return false; + } + + @Override + public void remove() { + throw new UnsupportedOperationException("Removal of classes from iterator not supported."); + } +} diff --git a/mobile/android/annotations/src/main/java/org/mozilla/gecko/annotationProcessors/utils/AlphabeticAnnotatableEntityComparator.java b/mobile/android/annotations/src/main/java/org/mozilla/gecko/annotationProcessors/utils/AlphabeticAnnotatableEntityComparator.java new file mode 100644 index 0000000000..47d8b82fba --- /dev/null +++ b/mobile/android/annotations/src/main/java/org/mozilla/gecko/annotationProcessors/utils/AlphabeticAnnotatableEntityComparator.java @@ -0,0 +1,81 @@ +/* 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.utils; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.util.Comparator; + +public class AlphabeticAnnotatableEntityComparator<T extends Member> implements Comparator<T> { + @Override + public int compare(T aLhs, T aRhs) { + // Constructors, Methods, Fields. + boolean lIsConstructor = aLhs instanceof Constructor; + boolean rIsConstructor = aRhs instanceof Constructor; + boolean lIsMethod = aLhs instanceof Method; + boolean rIsField = aRhs instanceof Field; + + if (lIsConstructor) { + if (!rIsConstructor) { + return -1; + } + } else if (lIsMethod) { + if (rIsConstructor) { + return 1; + } else if (rIsField) { + return -1; + } + } else { + if (!rIsField) { + return 1; + } + } + + // Verify these objects are the same type and cast them. + if (aLhs instanceof Method) { + return compare((Method) aLhs, (Method) aRhs); + } else if (aLhs instanceof Field) { + return compare((Field) aLhs, (Field) aRhs); + } else { + return compare((Constructor) aLhs, (Constructor) aRhs); + } + } + + // Alas, the type system fails us. + private static int compare(Method aLhs, Method aRhs) { + // Initially, attempt to differentiate the methods be name alone.. + String lName = aLhs.getName(); + String rName = aRhs.getName(); + + int ret = lName.compareTo(rName); + if (ret != 0) { + return ret; + } + + // The names were the same, so we need to compare signatures to find their uniqueness.. + lName = Utils.getSignature(aLhs); + rName = Utils.getSignature(aRhs); + + return lName.compareTo(rName); + } + + private static int compare(Constructor<?> aLhs, Constructor<?> aRhs) { + // The names will be the same, so we need to compare signatures to find their uniqueness.. + String lName = Utils.getSignature(aLhs); + String rName = Utils.getSignature(aRhs); + + return lName.compareTo(rName); + } + + private static int compare(Field aLhs, Field aRhs) { + // Compare field names.. + String lName = aLhs.getName(); + String rName = aRhs.getName(); + + return lName.compareTo(rName); + } +} diff --git a/mobile/android/annotations/src/main/java/org/mozilla/gecko/annotationProcessors/utils/GeneratableElementIterator.java b/mobile/android/annotations/src/main/java/org/mozilla/gecko/annotationProcessors/utils/GeneratableElementIterator.java new file mode 100644 index 0000000000..0ef25cab52 --- /dev/null +++ b/mobile/android/annotations/src/main/java/org/mozilla/gecko/annotationProcessors/utils/GeneratableElementIterator.java @@ -0,0 +1,291 @@ +/* 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.utils; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Iterator; +import org.mozilla.gecko.annotationProcessors.AnnotationInfo; +import org.mozilla.gecko.annotationProcessors.classloader.AnnotatableEntity; +import org.mozilla.gecko.annotationProcessors.classloader.ClassWithOptions; + +/** + * Iterator over the methods in a given method list which have the WrappedJNIMethod annotation. + * Returns an object containing both the annotation (Which may contain interesting parameters) and + * the argument. + */ +public class GeneratableElementIterator implements Iterator<AnnotatableEntity> { + private final ClassWithOptions mClass; + private final Member[] mObjects; + private AnnotatableEntity mNextReturnValue; + private int mElementIndex; + private AnnotationInfo mClassInfo; + + private boolean mIterateEveryEntry; + private boolean mIterateEnumValues; + private boolean mSkipCurrentEntry; + + public GeneratableElementIterator(ClassWithOptions annotatedClass) { + mClass = annotatedClass; + + final Class<?> aClass = annotatedClass.wrappedClass; + // Get all the elements of this class as AccessibleObjects. + Member[] aMethods = aClass.getDeclaredMethods(); + Member[] aFields = aClass.getDeclaredFields(); + Member[] aCtors = aClass.getDeclaredConstructors(); + + // Shove them all into one buffer. + Member[] objs = new Member[aMethods.length + aFields.length + aCtors.length]; + + int offset = 0; + System.arraycopy(aMethods, 0, objs, 0, aMethods.length); + offset += aMethods.length; + System.arraycopy(aFields, 0, objs, offset, aFields.length); + offset += aFields.length; + System.arraycopy(aCtors, 0, objs, offset, aCtors.length); + + // Sort the elements to ensure determinism. + Arrays.sort(objs, new AlphabeticAnnotatableEntityComparator<Member>()); + mObjects = objs; + + // Check for "Wrap ALL the things" flag. + for (Annotation annotation : aClass.getDeclaredAnnotations()) { + mClassInfo = buildAnnotationInfo(aClass, annotation); + if (mClassInfo != null) { + if (aClass.isEnum()) { + // We treat "Wrap ALL the things" differently for enums. See the javadoc for + // isAnnotatedEnumField for more information. + mIterateEnumValues = true; + } else { + mIterateEveryEntry = true; + } + break; + } + } + + if (mSkipCurrentEntry) { + throw new IllegalArgumentException("Cannot skip entire class"); + } + + findNextValue(); + } + + private Class<?>[] getFilteredInnerClasses() { + // Go through all inner classes and see which ones we want to generate. + final Class<?>[] candidates = mClass.wrappedClass.getDeclaredClasses(); + int count = 0; + + for (int i = 0; i < candidates.length; ++i) { + final GeneratableElementIterator testIterator = + new GeneratableElementIterator(new ClassWithOptions(candidates[i], null, /* ifdef */ "")); + if (testIterator.hasNext() || testIterator.getFilteredInnerClasses() != null) { + count++; + continue; + } + // Clear out ones that don't match. + candidates[i] = null; + } + return count > 0 ? candidates : null; + } + + public ClassWithOptions[] getInnerClasses() { + final Class<?>[] candidates = getFilteredInnerClasses(); + if (candidates == null) { + return new ClassWithOptions[0]; + } + + int count = 0; + for (Class<?> candidate : candidates) { + if (candidate != null) { + count++; + } + } + + final ClassWithOptions[] ret = new ClassWithOptions[count]; + count = 0; + for (Class<?> candidate : candidates) { + if (candidate != null) { + ret[count++] = + new ClassWithOptions( + candidate, mClass.generatedName + "::" + candidate.getSimpleName(), /* ifdef */ ""); + } + } + assert ret.length == count; + + Arrays.sort( + ret, + new Comparator<ClassWithOptions>() { + @Override + public int compare(ClassWithOptions lhs, ClassWithOptions rhs) { + return lhs.generatedName.compareTo(rhs.generatedName); + } + }); + return ret; + } + + private AnnotationInfo buildAnnotationInfo(AnnotatedElement element, Annotation annotation) { + Class<? extends Annotation> annotationType = annotation.annotationType(); + final String annotationTypeName = annotationType.getName(); + if (!annotationTypeName.equals("org.mozilla.gecko.annotation.WrapForJNI")) { + return null; + } + + String stubName = null; + AnnotationInfo.ExceptionMode exceptionMode = null; + AnnotationInfo.CallingThread callingThread = null; + AnnotationInfo.DispatchTarget dispatchTarget = null; + boolean noLiteral = false; + + try { + final Method skipMethod = annotationType.getDeclaredMethod("skip"); + skipMethod.setAccessible(true); + if ((Boolean) skipMethod.invoke(annotation)) { + mSkipCurrentEntry = true; + return null; + } + + // Determine the explicitly-given name of the stub to generate, if any. + final Method stubNameMethod = annotationType.getDeclaredMethod("stubName"); + stubNameMethod.setAccessible(true); + stubName = (String) stubNameMethod.invoke(annotation); + + final Method exceptionModeMethod = annotationType.getDeclaredMethod("exceptionMode"); + exceptionModeMethod.setAccessible(true); + exceptionMode = + Utils.getEnumValue( + AnnotationInfo.ExceptionMode.class, (String) exceptionModeMethod.invoke(annotation)); + + final Method calledFromMethod = annotationType.getDeclaredMethod("calledFrom"); + calledFromMethod.setAccessible(true); + callingThread = + Utils.getEnumValue( + AnnotationInfo.CallingThread.class, (String) calledFromMethod.invoke(annotation)); + + final Method dispatchToMethod = annotationType.getDeclaredMethod("dispatchTo"); + dispatchToMethod.setAccessible(true); + dispatchTarget = + Utils.getEnumValue( + AnnotationInfo.DispatchTarget.class, (String) dispatchToMethod.invoke(annotation)); + + final Method noLiteralMethod = annotationType.getDeclaredMethod("noLiteral"); + noLiteralMethod.setAccessible(true); + noLiteral = (Boolean) noLiteralMethod.invoke(annotation); + + } catch (NoSuchMethodException e) { + System.err.println( + "Unable to find expected field on WrapForJNI annotation. Did the signature change?"); + e.printStackTrace(System.err); + System.exit(3); + } catch (IllegalAccessException e) { + System.err.println( + "IllegalAccessException reading fields on WrapForJNI annotation. Seems the semantics of Reflection have changed..."); + e.printStackTrace(System.err); + System.exit(4); + } catch (InvocationTargetException e) { + System.err.println( + "InvocationTargetException reading fields on WrapForJNI annotation. This really shouldn't happen."); + e.printStackTrace(System.err); + System.exit(5); + } + + // If the method name was not explicitly given in the annotation generate one... + if (stubName.isEmpty()) { + stubName = Utils.getNativeName(element); + } + + return new AnnotationInfo(stubName, exceptionMode, callingThread, dispatchTarget, noLiteral); + } + + /** + * Find and cache the next appropriately annotated method, plus the annotation parameter, if one + * exists. Otherwise cache null, so hasNext returns false. + */ + private void findNextValue() { + while (mElementIndex < mObjects.length) { + Member candidateElement = mObjects[mElementIndex]; + mElementIndex++; + for (Annotation annotation : ((AnnotatedElement) candidateElement).getDeclaredAnnotations()) { + AnnotationInfo info = buildAnnotationInfo((AnnotatedElement) candidateElement, annotation); + if (info != null) { + mNextReturnValue = new AnnotatableEntity(candidateElement, info); + return; + } + } + + if (mSkipCurrentEntry) { + mSkipCurrentEntry = false; + continue; + } + + // If no annotation found, we might be expected to generate anyway + // using default arguments, thanks to the "Generate everything" annotation. + if (mIterateEveryEntry || isAnnotatedEnumField(candidateElement)) { + AnnotationInfo annotationInfo = + new AnnotationInfo( + Utils.getNativeName(candidateElement), + mClassInfo.exceptionMode, + mClassInfo.callingThread, + mClassInfo.dispatchTarget, + mClassInfo.noLiteral); + mNextReturnValue = new AnnotatableEntity(candidateElement, annotationInfo); + return; + } + } + mNextReturnValue = null; + } + + /** + * For enums that are annotated in their entirety, we typically only need to generate the + * enumerated values, but not other members. This method determines whether the given member is + * likely to be one of the enumerated values: We look for public, static, final fields that share + * the same class as the declaring enum's class. + * + * <p>Note that any additional members that should be wrapped may be explicitly annotated on a + * case-by-case basis. + */ + private boolean isAnnotatedEnumField(final Member member) { + if (!mIterateEnumValues) { + return false; + } + + if (!Utils.isPublic(member) + || !Utils.isStatic(member) + || !Utils.isFinal(member) + || !(member instanceof Field)) { + return false; + } + + final Class<?> enumClass = mClass.wrappedClass; + + final Field field = (Field) member; + final Class<?> fieldClass = field.getType(); + + return enumClass.equals(fieldClass); + } + + @Override + public boolean hasNext() { + return mNextReturnValue != null; + } + + @Override + public AnnotatableEntity next() { + AnnotatableEntity ret = mNextReturnValue; + findNextValue(); + return ret; + } + + @Override + public void remove() { + throw new UnsupportedOperationException( + "Removal of methods from GeneratableElementIterator not supported."); + } +} diff --git a/mobile/android/annotations/src/main/java/org/mozilla/gecko/annotationProcessors/utils/Utils.java b/mobile/android/annotations/src/main/java/org/mozilla/gecko/annotationProcessors/utils/Utils.java new file mode 100644 index 0000000000..3ed9546223 --- /dev/null +++ b/mobile/android/annotations/src/main/java/org/mozilla/gecko/annotationProcessors/utils/Utils.java @@ -0,0 +1,480 @@ +/* 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.utils; + +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Locale; +import org.mozilla.gecko.annotationProcessors.AnnotationInfo; + +/** A collection of utility methods used by CodeGenerator. Largely used for translating types. */ +public class Utils { + + // A collection of lookup tables to simplify the functions to follow... + private static final HashMap<String, String> NATIVE_TYPES = new HashMap<String, String>(); + + static { + NATIVE_TYPES.put("void", "void"); + NATIVE_TYPES.put("boolean", "bool"); + NATIVE_TYPES.put("byte", "int8_t"); + NATIVE_TYPES.put("char", "char16_t"); + NATIVE_TYPES.put("short", "int16_t"); + NATIVE_TYPES.put("int", "int32_t"); + NATIVE_TYPES.put("long", "int64_t"); + NATIVE_TYPES.put("float", "float"); + NATIVE_TYPES.put("double", "double"); + } + + private static final HashMap<String, String> NATIVE_ARRAY_TYPES = new HashMap<String, String>(); + + static { + NATIVE_ARRAY_TYPES.put("boolean", "mozilla::jni::BooleanArray"); + NATIVE_ARRAY_TYPES.put("byte", "mozilla::jni::ByteArray"); + NATIVE_ARRAY_TYPES.put("char", "mozilla::jni::CharArray"); + NATIVE_ARRAY_TYPES.put("short", "mozilla::jni::ShortArray"); + NATIVE_ARRAY_TYPES.put("int", "mozilla::jni::IntArray"); + NATIVE_ARRAY_TYPES.put("long", "mozilla::jni::LongArray"); + NATIVE_ARRAY_TYPES.put("float", "mozilla::jni::FloatArray"); + NATIVE_ARRAY_TYPES.put("double", "mozilla::jni::DoubleArray"); + } + + private static final HashMap<String, String> CLASS_DESCRIPTORS = new HashMap<String, String>(); + + static { + CLASS_DESCRIPTORS.put("void", "V"); + CLASS_DESCRIPTORS.put("boolean", "Z"); + CLASS_DESCRIPTORS.put("byte", "B"); + CLASS_DESCRIPTORS.put("char", "C"); + CLASS_DESCRIPTORS.put("short", "S"); + CLASS_DESCRIPTORS.put("int", "I"); + CLASS_DESCRIPTORS.put("long", "J"); + CLASS_DESCRIPTORS.put("float", "F"); + CLASS_DESCRIPTORS.put("double", "D"); + } + + private static boolean isMozClass(final Class<?> type) { + return type.getName().startsWith("org.mozilla."); + } + + private static boolean useObjectForType(final Class<?> type, final boolean isHint) { + // Essentially we want to know whether we can use generated wrappers or not: + // If |type| is not ours, then it most likely doesn't have generated C++ wrappers. + // Furthermore, we skip interfaces as we generally do not wrap those. + return !isHint || type.equals(Object.class) || !isMozClass(type) || type.isInterface(); + } + + /** + * Returns the simplified name of a class that includes any outer classes but excludes + * package/namespace qualifiers. + * + * @param genScope The current scope of the class containing the current declaration. @Param type + * The class whose simplified name is to be generated. @Param connector String to be used for + * concatenating scopes. + * @return String containing the result + */ + private static String getSimplifiedClassName( + final Class<?> genScope, final Class<?> type, final String connector) { + final ArrayList<String> names = new ArrayList<>(); + + // Starting with |type|, walk up our enclosing classes until we either reach genScope or we + // have reached the outermost scope. We save them to a list because we need to reverse them + // during output. + Class<?> c = type; + do { + names.add(c.getSimpleName()); + c = c.getEnclosingClass(); + } while (c != null && (genScope == null || !genScope.equals(c))); + + // Walk through names in reverse order, joining them using |connector| + final StringBuilder builder = new StringBuilder(); + for (int i = names.size() - 1; i >= 0; --i) { + builder.append(names.get(i)); + if (i > 0) { + builder.append(connector); + } + } + + return builder.toString(); + } + + /** + * Returns the simplified name of a Java class that includes any outer classes but excludes + * package qualifiers. Used for Java signature hints. + * + * @param genScope The current scope of the class containing the current declaration. @Param type + * The class whose simplified name is to be generated. + * @return String containing the result + */ + public static String getSimplifiedJavaClassName(final Class<?> genScope, final Class<?> type) { + return getSimplifiedClassName(genScope, type, "."); + } + + /** Returns the fully-qualified name of the native class wrapper for the given type. */ + public static String getWrappedNativeClassName(final Class<?> type) { + return "mozilla::java::" + getSimplifiedClassName(null, type, "::"); + } + + /** + * Get the C++ parameter type corresponding to the provided type parameter. + * + * @param type Class to determine the corresponding JNI type for. + * @return C++ type as a String + */ + public static String getNativeParameterType(Class<?> type, AnnotationInfo info) { + return getNativeParameterType(type, info, false); + } + + /** + * Get the C++ hint type corresponding to the provided type parameter. The returned type may be + * more specific than the type returned by getNativeParameterType, as this method is used for + * generating comments instead of machine-readable code. + * + * @param type Class to determine the corresponding JNI type for. + * @return C++ type as a String + */ + public static String getNativeParameterTypeHint(Class<?> type, AnnotationInfo info) { + return getNativeParameterType(type, info, true); + } + + private static String getNativeParameterType( + final Class<?> type, final AnnotationInfo info, final boolean isHint) { + final String name = type.getName().replace('.', '/'); + + String value = NATIVE_TYPES.get(name); + if (value != null) { + return value; + } + + if (type.isArray()) { + final String compName = type.getComponentType().getName(); + value = NATIVE_ARRAY_TYPES.get(compName); + if (value != null) { + return value + "::Param"; + } + return "mozilla::jni::ObjectArray::Param"; + } + + if (type.equals(String.class) || type.equals(CharSequence.class)) { + return "mozilla::jni::String::Param"; + } + + if (type.equals(Class.class)) { + // You're doing reflection on Java objects from inside C, returning Class objects + // to C, generating the corresponding code using this Java program. Really?! + return "mozilla::jni::Class::Param"; + } + + if (type.equals(Throwable.class)) { + return "mozilla::jni::Throwable::Param"; + } + + if (type.equals(ByteBuffer.class)) { + return "mozilla::jni::ByteBuffer::Param"; + } + + if (useObjectForType(type, isHint)) { + return "mozilla::jni::Object::Param"; + } + + return getWrappedNativeClassName(type) + "::Param"; + } + + /** + * Get the C++ return type corresponding to the provided type parameter. + * + * @param type Class to determine the corresponding JNI type for. + * @return C++ type as a String + */ + public static String getNativeReturnType(Class<?> type, AnnotationInfo info) { + return getNativeReturnType(type, info, false); + } + + /** + * Get the C++ hint return type corresponding to the provided type parameter. The returned type + * may be more specific than the type returned by getNativeReturnType, as this method is used for + * generating comments instead of machine-readable code. + * + * @param type Class to determine the corresponding JNI type for. + * @return C++ type as a String + */ + public static String getNativeReturnTypeHint(Class<?> type, AnnotationInfo info) { + return getNativeReturnType(type, info, true); + } + + private static String getNativeReturnType( + final Class<?> type, final AnnotationInfo info, final boolean isHint) { + final String name = type.getName().replace('.', '/'); + + String value = NATIVE_TYPES.get(name); + if (value != null) { + return value; + } + + if (type.isArray()) { + final String compName = type.getComponentType().getName(); + value = NATIVE_ARRAY_TYPES.get(compName); + if (value != null) { + return value + "::LocalRef"; + } + return "mozilla::jni::ObjectArray::LocalRef"; + } + + if (type.equals(String.class)) { + return "mozilla::jni::String::LocalRef"; + } + + if (type.equals(Class.class)) { + // You're doing reflection on Java objects from inside C, returning Class objects + // to C, generating the corresponding code using this Java program. Really?! + return "mozilla::jni::Class::LocalRef"; + } + + if (type.equals(Throwable.class)) { + return "mozilla::jni::Throwable::LocalRef"; + } + + if (type.equals(ByteBuffer.class)) { + return "mozilla::jni::ByteBuffer::LocalRef"; + } + + if (useObjectForType(type, isHint)) { + return "mozilla::jni::Object::LocalRef"; + } + + return getWrappedNativeClassName(type) + "::LocalRef"; + } + + /** + * Get the JNI class descriptor corresponding to the provided type parameter. + * + * @param type Class to determine the corresponding JNI descriptor for. + * @return Class descripor as a String + */ + public static String getClassDescriptor(Class<?> type) { + final String name = type.getName().replace('.', '/'); + + final String classDescriptor = CLASS_DESCRIPTORS.get(name); + if (classDescriptor != null) { + return classDescriptor; + } + + if (type.isArray()) { + // Array names are already in class descriptor form. + return name; + } + + return "L" + name + ';'; + } + + /** + * Get the JNI signaure for a member. + * + * @param member Member to get the signature for. + * @return JNI signature as a string + */ + public static String getSignature(Member member) { + return member instanceof Field + ? getSignature((Field) member) + : member instanceof Method + ? getSignature((Method) member) + : getSignature((Constructor<?>) member); + } + + /** + * Get the JNI signaure for a field. + * + * @param member Field to get the signature for. + * @return JNI signature as a string + */ + public static String getSignature(Field member) { + return getClassDescriptor(member.getType()); + } + + private static String getSignature(Class<?>[] args, Class<?> ret) { + final StringBuilder sig = new StringBuilder("("); + for (int i = 0; i < args.length; i++) { + sig.append(getClassDescriptor(args[i])); + } + return sig.append(')').append(getClassDescriptor(ret)).toString(); + } + + /** + * Get the JNI signaure for a method. + * + * @param member Method to get the signature for. + * @return JNI signature as a string + */ + public static String getSignature(Method member) { + return getSignature(member.getParameterTypes(), member.getReturnType()); + } + + /** + * Get the JNI signaure for a constructor. + * + * @param member Constructor to get the signature for. + * @return JNI signature as a string + */ + public static String getSignature(Constructor<?> member) { + return getSignature(member.getParameterTypes(), void.class); + } + + /** + * Get the C++ name for a member. + * + * @param member Member to get the name for. + * @return JNI name as a string + */ + public static String getNativeName(Member member) { + final String name = getMemberName(member); + return name.substring(0, 1).toUpperCase(Locale.ROOT) + name.substring(1); + } + + /** + * Get the C++ name for a member. + * + * @param member Member to get the name for. + * @return JNI name as a string + */ + public static String getNativeName(Class<?> clz) { + final String name = clz.getName(); + return name.substring(0, 1).toUpperCase(Locale.ROOT) + name.substring(1); + } + + /** + * Get the C++ name for a member. + * + * @param member Member to get the name for. + * @return JNI name as a string + */ + public static String getNativeName(AnnotatedElement element) { + if (element instanceof Class<?>) { + return getNativeName((Class<?>) element); + } else if (element instanceof Member) { + return getNativeName((Member) element); + } else { + return null; + } + } + + /** + * Get the JNI name for a member. + * + * @param member Member to get the name for. + * @return JNI name as a string + */ + public static String getMemberName(Member member) { + if (member instanceof Constructor) { + return "<init>"; + } + return member.getName(); + } + + public static String getUnqualifiedName(String name) { + return name.substring(name.lastIndexOf(':') + 1); + } + + /** + * Determine if a member is declared static. + * + * @param member The Member to check. + * @return true if the member is declared static, false otherwise. + */ + public static boolean isStatic(final Member member) { + return Modifier.isStatic(member.getModifiers()); + } + + /** + * Determine if a member is declared final. + * + * @param member The Member to check. + * @return true if the member is declared final, false otherwise. + */ + public static boolean isFinal(final Member member) { + return Modifier.isFinal(member.getModifiers()); + } + + /** + * Determine if a member is declared public. + * + * @param member The Member to check. + * @return true if the member is declared public, false otherwise. + */ + public static boolean isPublic(final Member member) { + return Modifier.isPublic(member.getModifiers()); + } + + /** + * Return an enum value with the given name. + * + * @param type Enum class type. + * @param name Enum value name. + * @return Enum value with the given name. + */ + public static <T extends Enum<T>> T getEnumValue(Class<T> type, String name) { + try { + return Enum.valueOf(type, name.toUpperCase(Locale.ROOT)); + + } catch (IllegalArgumentException e) { + final Object[] values; + try { + values = (Object[]) type.getDeclaredMethod("values").invoke(null); + } catch (final NoSuchMethodException + | IllegalAccessException + | InvocationTargetException exception) { + throw new RuntimeException("Cannot access enum: " + type, exception); + } + + StringBuilder names = new StringBuilder(); + + for (int i = 0; i < values.length; i++) { + if (i != 0) { + names.append(", "); + } + names.append(values[i].toString().toLowerCase(Locale.ROOT)); + } + + System.err.println("***"); + System.err.println("*** Invalid value \"" + name + "\" for " + type.getSimpleName()); + System.err.println("*** Specify one of " + names.toString()); + System.err.println("***"); + e.printStackTrace(System.err); + System.exit(1); + return null; + } + } + + public static String getIfdefHeader(String ifdef) { + if (ifdef.isEmpty()) { + return ""; + } else if (ifdef.startsWith("!")) { + return "#ifndef " + ifdef.substring(1) + "\n"; + } + return "#ifdef " + ifdef + "\n"; + } + + public static String getIfdefFooter(String ifdef) { + if (ifdef.isEmpty()) { + return ""; + } + return "#endif // " + ifdef + "\n"; + } + + public static boolean isJNIObject(Class<?> cls) { + for (; cls != null; cls = cls.getSuperclass()) { + if (cls.getName().equals("org.mozilla.gecko.mozglue.JNIObject")) { + return true; + } + } + return false; + } +} |