summaryrefslogtreecommitdiffstats
path: root/chromium/base/android/jni_generator/java/src/org/chromium/jni_generator/JniProcessor.java
diff options
context:
space:
mode:
Diffstat (limited to 'chromium/base/android/jni_generator/java/src/org/chromium/jni_generator/JniProcessor.java')
-rw-r--r--chromium/base/android/jni_generator/java/src/org/chromium/jni_generator/JniProcessor.java530
1 files changed, 530 insertions, 0 deletions
diff --git a/chromium/base/android/jni_generator/java/src/org/chromium/jni_generator/JniProcessor.java b/chromium/base/android/jni_generator/java/src/org/chromium/jni_generator/JniProcessor.java
new file mode 100644
index 00000000000..06af39bc018
--- /dev/null
+++ b/chromium/base/android/jni_generator/java/src/org/chromium/jni_generator/JniProcessor.java
@@ -0,0 +1,530 @@
+// Copyright 2018 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.jni_generator;
+
+import com.google.auto.service.AutoService;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.squareup.javapoet.AnnotationSpec;
+import com.squareup.javapoet.ArrayTypeName;
+import com.squareup.javapoet.ClassName;
+import com.squareup.javapoet.FieldSpec;
+import com.squareup.javapoet.JavaFile;
+import com.squareup.javapoet.MethodSpec;
+import com.squareup.javapoet.ParameterSpec;
+import com.squareup.javapoet.ParameterizedTypeName;
+import com.squareup.javapoet.TypeName;
+import com.squareup.javapoet.TypeSpec;
+
+import org.chromium.base.JniStaticTestMocker;
+import org.chromium.base.NativeLibraryLoadedStatus;
+import org.chromium.base.annotations.NativeMethods;
+import org.chromium.build.annotations.CheckDiscard;
+import org.chromium.build.annotations.MainDex;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.processing.AbstractProcessor;
+import javax.annotation.processing.Processor;
+import javax.annotation.processing.RoundEnvironment;
+import javax.lang.model.SourceVersion;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ElementKind;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.Modifier;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.element.VariableElement;
+import javax.lang.model.type.ArrayType;
+import javax.lang.model.type.TypeKind;
+import javax.lang.model.type.TypeMirror;
+import javax.tools.Diagnostic;
+
+/**
+ * Annotation processor that finds inner interfaces annotated with
+ * {@link NativeMethods} and generates a class with native bindings
+ * (GEN_JNI) and a class specific wrapper class with name (classnameJni)
+ *
+ * NativeClass - refers to the class that contains all native declarations.
+ * NativeWrapperClass - refers to the class that is generated for each class
+ * containing an interface annotated with NativeMethods.
+ *
+ */
+@AutoService(Processor.class)
+public class JniProcessor extends AbstractProcessor {
+ private static final Class<NativeMethods> JNI_STATIC_NATIVES_CLASS = NativeMethods.class;
+ private static final Class<MainDex> MAIN_DEX_CLASS = MainDex.class;
+ private static final Class<CheckDiscard> CHECK_DISCARD_CLASS = CheckDiscard.class;
+
+ private static final String CHECK_DISCARD_CRBUG = "crbug.com/993421";
+ private static final String NATIVE_WRAPPER_CLASS_POSTFIX = "Jni";
+
+ private static final ClassName JNI_STATUS_CLASS_NAME =
+ ClassName.get(NativeLibraryLoadedStatus.class);
+
+ static final String NATIVE_TEST_FIELD_NAME = "TESTING_ENABLED";
+ static final String NATIVE_REQUIRE_MOCK_FIELD_NAME = "REQUIRE_MOCK";
+
+ // Builder for NativeClass which will hold all our native method declarations.
+ private TypeSpec.Builder mNativesBuilder;
+
+ private ClassName mGenJniClassName = ClassName.get("org.chromium.base.natives", "GEN_JNI");
+
+ // Types that are non-primitives and should not be
+ // casted to objects in native method declarations.
+ static final ImmutableSet JNI_OBJECT_TYPE_EXCEPTIONS =
+ ImmutableSet.of("java.lang.String", "java.lang.Throwable", "java.lang.Class", "void");
+
+ static String getNameOfWrapperClass(String containingClassName) {
+ return containingClassName + NATIVE_WRAPPER_CLASS_POSTFIX;
+ }
+
+ @Override
+ public Set<String> getSupportedAnnotationTypes() {
+ return ImmutableSet.of(JNI_STATIC_NATIVES_CLASS.getCanonicalName());
+ }
+
+ @Override
+ public SourceVersion getSupportedSourceVersion() {
+ return SourceVersion.latestSupported();
+ }
+
+ /**
+ * Processes annotations that match getSupportedAnnotationTypes()
+ * Called each 'round' of annotation processing, must fail gracefully if set is empty.
+ */
+ @Override
+ public boolean process(
+ Set<? extends TypeElement> annotations, RoundEnvironment roundEnvironment) {
+ // Do nothing on an empty round.
+ if (annotations.isEmpty()) {
+ return true;
+ }
+
+ List<JavaFile> writeQueue = Lists.newArrayList();
+ for (Element e : roundEnvironment.getElementsAnnotatedWith(JNI_STATIC_NATIVES_CLASS)) {
+ // @NativeMethods can only annotate types so this is safe.
+ TypeElement type = (TypeElement) e;
+
+ if (!e.getKind().isInterface()) {
+ printError("@NativeMethods must annotate an interface", e);
+ }
+
+ if (mNativesBuilder == null) {
+ String genJniPrefix = e.getAnnotation(JNI_STATIC_NATIVES_CLASS).value();
+ if (!genJniPrefix.isEmpty()) {
+ mGenJniClassName =
+ ClassName.get("org.chromium.base.natives", genJniPrefix + "_GEN_JNI");
+ }
+
+ FieldSpec.Builder testingFlagBuilder =
+ FieldSpec.builder(TypeName.BOOLEAN, NATIVE_TEST_FIELD_NAME)
+ .addModifiers(Modifier.STATIC, Modifier.PUBLIC);
+ FieldSpec.Builder throwFlagBuilder =
+ FieldSpec.builder(TypeName.BOOLEAN, NATIVE_REQUIRE_MOCK_FIELD_NAME)
+ .addModifiers(Modifier.STATIC, Modifier.PUBLIC);
+
+ // State of mNativesBuilder needs to be preserved between processing rounds.
+ mNativesBuilder = TypeSpec.classBuilder(mGenJniClassName)
+ .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
+ .addField(testingFlagBuilder.build())
+ .addField(throwFlagBuilder.build());
+ }
+
+ // Interface must be nested within a class.
+ Element outerElement = e.getEnclosingElement();
+ if (!(outerElement instanceof TypeElement)) {
+ printError(
+ "Interface annotated with @JNIInterface must be nested within a class", e);
+ }
+
+ TypeElement outerType = (TypeElement) outerElement;
+ ClassName outerTypeName = ClassName.get(outerType);
+ String outerClassName = outerTypeName.simpleName();
+ String packageName = outerTypeName.packageName();
+
+ // Get all methods in annotated interface.
+ List<ExecutableElement> interfaceMethods = getMethodsFromType(type);
+
+ // Map from the current method names to the method spec for a static native
+ // method that will be in our big NativeClass.
+ // Collisions are not allowed - no overloading.
+ Map<String, MethodSpec> methodMap =
+ createNativeMethodSpecs(interfaceMethods, outerTypeName);
+
+ // Add these to our NativeClass.
+ for (MethodSpec method : methodMap.values()) {
+ mNativesBuilder.addMethod(method);
+ }
+
+ // Generate a NativeWrapperClass for outerType by implementing the
+ // annotated interface. Need to pass it the method map because each
+ // method overridden will be a wrapper that calls its
+ // native counterpart in NativeClass.
+ boolean isNativesInterfacePublic = type.getModifiers().contains(Modifier.PUBLIC);
+ // If the outerType needs to be in the main dex, then the generated NativeWrapperClass
+ // should also be added to the main dex.
+ boolean addMainDexAnnotation = outerElement.getAnnotation(MAIN_DEX_CLASS) != null;
+
+ TypeSpec nativeWrapperClassSpec =
+ createNativeWrapperClassSpec(getNameOfWrapperClass(outerClassName),
+ isNativesInterfacePublic, addMainDexAnnotation, type, methodMap);
+
+ // Queue this file for writing.
+ // Can't write right now because the wrapper class depends on NativeClass
+ // to be written and we can't write NativeClass until all @NativeMethods
+ // interfaces are processed because each will add new native methods.
+ JavaFile file = JavaFile.builder(packageName, nativeWrapperClassSpec)
+ .addFileComment("Generated by JniProcessor.java")
+ .build();
+ writeQueue.add(file);
+ }
+
+ // Nothing needs to be written this round.
+ if (writeQueue.size() == 0) {
+ return true;
+ }
+
+ try {
+ // Need to write NativeClass first because the wrapper classes
+ // depend on it.
+ JavaFile nativeClassFile =
+ JavaFile.builder(mGenJniClassName.packageName(), mNativesBuilder.build())
+ .addFileComment("Generated by JniProcessor.java")
+ .build();
+
+ nativeClassFile.writeTo(processingEnv.getFiler());
+
+ for (JavaFile f : writeQueue) {
+ f.writeTo(processingEnv.getFiler());
+ }
+ } catch (Exception e) {
+ processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, e.getMessage());
+ }
+ return true;
+ }
+
+ List<ExecutableElement> getMethodsFromType(TypeElement t) {
+ List<ExecutableElement> methods = Lists.newArrayList();
+ for (Element e : t.getEnclosedElements()) {
+ if (e.getKind() == ElementKind.METHOD) {
+ methods.add((ExecutableElement) e);
+ }
+ }
+ return methods;
+ }
+
+ /**
+ * Gets method name for methods inside of NativeClass
+ */
+ String getNativeMethodName(String packageName, String className, String oldMethodName) {
+ // e.g. org.chromium.base.Foo_Class.bar
+ // => org_chromium_base_Foo_1Class_bar()
+ return String.format("%s.%s.%s", packageName, className, oldMethodName)
+ .replaceAll("_", "_1")
+ .replaceAll("\\.", "_");
+ }
+
+ /**
+ * Creates method specs for the native methods of NativeClass given
+ * the method declarations from a {@link NativeMethods} annotated interface
+ * @param interfaceMethods method declarations from a {@link NativeMethods} annotated
+ * interface
+ * @param outerType ClassName of class that contains the annotated interface
+ * @return map from old method name to new native method specification
+ */
+ Map<String, MethodSpec> createNativeMethodSpecs(
+ List<ExecutableElement> interfaceMethods, ClassName outerType) {
+ Map<String, MethodSpec> methodMap = Maps.newTreeMap();
+ for (ExecutableElement m : interfaceMethods) {
+ String oldMethodName = m.getSimpleName().toString();
+ String newMethodName = getNativeMethodName(
+ outerType.packageName(), outerType.simpleName(), oldMethodName);
+ MethodSpec.Builder builder = MethodSpec.methodBuilder(newMethodName)
+ .addModifiers(Modifier.PUBLIC)
+ .addModifiers(Modifier.FINAL)
+ .addModifiers(Modifier.STATIC)
+ .addModifiers(Modifier.NATIVE);
+ builder.addJavadoc(createNativeMethodJavadocString(outerType, m));
+
+ copyMethodParamsAndReturnType(builder, m, true);
+ if (methodMap.containsKey(oldMethodName)) {
+ printError("Overloading is not currently implemented with this processor ", m);
+ }
+ methodMap.put(oldMethodName, builder.build());
+ }
+ return methodMap;
+ }
+
+ /**
+ * Creates a generated annotation that contains the name of this class.
+ */
+ static AnnotationSpec createAnnotationWithValue(Class<?> annotationClazz, String value) {
+ return AnnotationSpec.builder(annotationClazz)
+ .addMember("value", String.format("\"%s\"", value))
+ .build();
+ }
+
+ void printError(String s) {
+ processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, s);
+ }
+
+ void printError(String s, Element e) {
+ processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
+ String.format("Error processing @NativeMethods interface: %s", s), e);
+ }
+
+ /**
+ * Creates a class spec for an implementation of an {@link NativeMethods} annotated interface
+ * that will wrap calls to the NativesClass which contains the actual native method
+ * declarations.
+ *
+ * This class should contain:
+ * 1. Wrappers for all GEN_JNI static native methods
+ * 2. A getter that when testing is disabled, will return the native implementation and
+ * when testing is enabled, will call the mock of the native implementation.
+ * 3. A field that holds the testNatives instance for when testing is enabled
+ * 4. A TEST_HOOKS field that implements an anonymous instance of {@link JniStaticTestMocker}
+ * which will set the testNatives implementation when called in tests
+ *
+ * @param name name of the wrapper class.
+ * @param isPublic if true, a public modifier will be added to this native wrapper.
+ * @param isMainDex if true, the @MainDex annotation will be added to this native wrapper.
+ * @param nativeInterface the {@link NativeMethods} annotated type that this native wrapper
+ * will implement.
+ * @param methodMap a map from the old method name to the new method spec in NativeClass.
+ * */
+ TypeSpec createNativeWrapperClassSpec(String name, boolean isPublic, boolean isMainDex,
+ TypeElement nativeInterface, Map<String, MethodSpec> methodMap) {
+ // The wrapper class builder.
+ TypeName nativeInterfaceType = TypeName.get(nativeInterface.asType());
+ TypeSpec.Builder builder =
+ TypeSpec.classBuilder(name).addSuperinterface(nativeInterfaceType);
+ if (isPublic) {
+ builder.addModifiers(Modifier.PUBLIC);
+ }
+ if (isMainDex) {
+ builder.addAnnotation(MAIN_DEX_CLASS);
+ }
+ builder.addAnnotation(createAnnotationWithValue(CHECK_DISCARD_CLASS, CHECK_DISCARD_CRBUG));
+
+ // Start by adding all the native method wrappers.
+ for (Element enclosed : nativeInterface.getEnclosedElements()) {
+ if (enclosed.getKind() != ElementKind.METHOD) {
+ printError("Cannot have a non-method in a @NativeMethods annotated interface",
+ enclosed);
+ }
+
+ // ElementKind.Method is ExecutableElement so this cast is safe.
+ // interfaceMethod will is the method we are overloading.
+ ExecutableElement interfaceMethod = (ExecutableElement) enclosed;
+
+ // Method in NativesClass that we'll be calling.
+ MethodSpec nativesMethod = methodMap.get(interfaceMethod.getSimpleName().toString());
+
+ // Add a matching method in this class that overrides the declaration
+ // in nativeInterface. It will just call the actual natives method in
+ // NativeClass.
+ builder.addMethod(createNativeWrapperMethod(interfaceMethod, nativesMethod));
+ }
+
+ // Add the testInstance field.
+ // Holds the test natives target if it is set.
+ FieldSpec testTarget = FieldSpec.builder(nativeInterfaceType, "testInstance")
+ .addModifiers(Modifier.PRIVATE, Modifier.STATIC)
+ .build();
+ builder.addField(testTarget);
+
+ // Getter for target or testing instance if flag in GEN_JNI is set.
+ /*
+ {classname}.Natives get() {
+ if (GEN_JNI.TESTING_ENABLED) {
+ if (testInst != null) {
+ return testInst;
+ }
+ if (GEN_JNI.REQUIRE_MOCK) {
+ throw new UnsupportedOperationException($noMockExceptionString);
+ }
+ }
+ NativeLibraryLoadedStatus.checkLoaded($isMainDex)
+ return new {classname}Jni();
+ }
+ */
+ String noMockExceptionString =
+ String.format("No mock found for the native implementation for %s. "
+ + "The current configuration requires all native "
+ + "implementations to have a mock instance.",
+ nativeInterfaceType);
+
+ MethodSpec instanceGetter =
+ MethodSpec.methodBuilder("get")
+ .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
+ .returns(nativeInterfaceType)
+ .beginControlFlow("if ($T.$N)", mGenJniClassName, NATIVE_TEST_FIELD_NAME)
+ .beginControlFlow("if ($N != null)", testTarget)
+ .addStatement("return $N", testTarget)
+ .endControlFlow()
+ .beginControlFlow(
+ "if ($T.$N)", mGenJniClassName, NATIVE_REQUIRE_MOCK_FIELD_NAME)
+ .addStatement("throw new UnsupportedOperationException($S)",
+ noMockExceptionString)
+ .endControlFlow()
+ .endControlFlow()
+ .addStatement("$T.$N($L)", JNI_STATUS_CLASS_NAME, "checkLoaded", isMainDex)
+ .addStatement("return new $N()", name)
+ .build();
+
+ builder.addMethod(instanceGetter);
+
+ // Next add TEST_HOOKS to set testInstance... should look like this:
+ // JniStaticTestMocker<ClassNameJni> TEST_HOOKS = new JniStaticTestMocker<>() {
+ // @Override
+ // public static setInstanceForTesting(ClassNameJni instance) {
+ // if (!GEN_JNI.TESTING_ENABLED) {
+ // throw new RuntimeException($mocksNotEnabledExceptionString);
+ // }
+ // testInstance = instance;
+ // }
+ // }
+ String mocksNotEnabledExceptionString =
+ "Tried to set a JNI mock when mocks aren't enabled!";
+ MethodSpec testHookMockerMethod =
+ MethodSpec.methodBuilder("setInstanceForTesting")
+ .addModifiers(Modifier.PUBLIC)
+ .addAnnotation(Override.class)
+ .addParameter(nativeInterfaceType, "instance")
+ .beginControlFlow("if (!$T.$N)", mGenJniClassName, NATIVE_TEST_FIELD_NAME)
+ .addStatement(
+ "throw new RuntimeException($S)", mocksNotEnabledExceptionString)
+ .endControlFlow()
+ .addStatement("$N = instance", testTarget)
+ .build();
+
+ // Make the anonymous TEST_HOOK class.
+ ParameterizedTypeName genericMockerInterface = ParameterizedTypeName.get(
+ ClassName.get(JniStaticTestMocker.class), ClassName.get(nativeInterface));
+
+ TypeSpec testHook = TypeSpec.anonymousClassBuilder("")
+ .addSuperinterface(genericMockerInterface)
+ .addMethod(testHookMockerMethod)
+ .build();
+
+ FieldSpec testHookSpec =
+ FieldSpec.builder(genericMockerInterface, "TEST_HOOKS")
+ .addModifiers(Modifier.STATIC, Modifier.PUBLIC, Modifier.FINAL)
+ .initializer("$L", testHook.toString())
+ .build();
+
+ builder.addField(testHookSpec);
+ return builder.build();
+ }
+
+ /**
+ * Creates a wrapper method that overrides interfaceMethod and calls staticNativeMethod.
+ * @param interfaceMethod method that will be overridden in a {@link NativeMethods} annotated
+ * interface.
+ * @param staticNativeMethod method that will be called in NativeClass.
+ */
+ MethodSpec createNativeWrapperMethod(
+ ExecutableElement interfaceMethod, MethodSpec staticNativeMethod) {
+ // Method will have the same name and be public.
+ MethodSpec.Builder builder =
+ MethodSpec.methodBuilder(interfaceMethod.getSimpleName().toString())
+ .addModifiers(Modifier.PUBLIC)
+ .addAnnotation(Override.class);
+
+ // Method will need the same params and return type as the one we're overriding.
+ copyMethodParamsAndReturnType(builder, interfaceMethod);
+
+ // Add return if method return type is not void.
+ if (!interfaceMethod.getReturnType().toString().equals("void")) {
+ // Also need to cast because non-primitives are Objects in NativeClass.
+ builder.addCode("return ($T)", interfaceMethod.getReturnType());
+ }
+
+ // Make call to native function.
+ builder.addCode("$T.$N(", mGenJniClassName, staticNativeMethod);
+
+ // Add params to native call.
+ ArrayList<String> paramNames = new ArrayList<>();
+ for (VariableElement param : interfaceMethod.getParameters()) {
+ paramNames.add(param.getSimpleName().toString());
+ }
+
+ builder.addCode(String.join(", ", paramNames) + ");\n");
+ return builder.build();
+ }
+
+ boolean shouldDowncastToObjectForJni(TypeName t) {
+ if (t.isPrimitive()) {
+ return false;
+ }
+ // There are some non-primitives that should not be downcasted.
+ return !JNI_OBJECT_TYPE_EXCEPTIONS.contains(t.toString());
+ }
+
+ TypeName toTypeName(TypeMirror t, boolean useJni) {
+ if (t.getKind() == TypeKind.ARRAY) {
+ return ArrayTypeName.of(toTypeName(((ArrayType) t).getComponentType(), useJni));
+ }
+ TypeName typeName = TypeName.get(t);
+ if (useJni && shouldDowncastToObjectForJni(typeName)) {
+ return TypeName.OBJECT;
+ }
+ return typeName;
+ }
+
+ /**
+ * Since some types may decay to objects in the native method
+ * this method returns a javadoc string that contains the
+ * type information from the old method.
+ **/
+ String createNativeMethodJavadocString(ClassName outerType, ExecutableElement oldMethod) {
+ ArrayList<String> docLines = new ArrayList<>();
+
+ // Class descriptor.
+ String descriptor = String.format("%s.%s.%s", outerType.packageName(),
+ outerType.simpleName(), oldMethod.getSimpleName().toString());
+ docLines.add(descriptor);
+
+ // Parameters.
+ for (VariableElement param : oldMethod.getParameters()) {
+ TypeName paramType = TypeName.get(param.asType());
+ String paramTypeName = paramType.toString();
+ String name = param.getSimpleName().toString();
+ docLines.add(String.format("@param %s (%s)", name, paramTypeName));
+ }
+
+ // Return type.
+ docLines.add(String.format("@return (%s)", oldMethod.getReturnType().toString()));
+
+ return String.join("\n", docLines) + "\n";
+ }
+
+ void copyMethodParamsAndReturnType(
+ MethodSpec.Builder builder, ExecutableElement method, boolean useJniTypes) {
+ for (VariableElement param : method.getParameters()) {
+ builder.addParameter(createParamSpec(param, useJniTypes));
+ }
+ TypeMirror givenReturnType = method.getReturnType();
+ TypeName returnType = toTypeName(givenReturnType, useJniTypes);
+
+ builder.returns(returnType);
+ }
+
+ void copyMethodParamsAndReturnType(MethodSpec.Builder builder, ExecutableElement method) {
+ copyMethodParamsAndReturnType(builder, method, false);
+ }
+
+ ParameterSpec createParamSpec(VariableElement param, boolean useJniObjects) {
+ TypeName paramType = toTypeName(param.asType(), useJniObjects);
+ return ParameterSpec.builder(paramType, param.getSimpleName().toString())
+ .addModifiers(param.getModifiers())
+ .build();
+ }
+}