From d2f51aa8179cc693562aa429c282e8949c9aa3ce Mon Sep 17 00:00:00 2001 From: hoisie Date: Mon, 4 Jan 2021 10:39:53 -0800 Subject: [PATCH] Lay the groundwork to use preinstrumented android-all jars in Robolectric. PiperOrigin-RevId: 350029856 --- .../internal/bytecode/ClassDetails.java | 27 +++++++++++++++--- .../internal/bytecode/ClassInstrumentor.java | 28 +++++++++++++++---- .../InstrumentationConfiguration.java | 1 + .../internal/bytecode/MutableClass.java | 11 +++++++- .../internal/bytecode/NativeMethod.java | 15 ++++++++++ .../internal/bytecode/SandboxClassLoader.java | 15 ++++++---- 6 files changed, 81 insertions(+), 16 deletions(-) create mode 100644 sandbox/src/main/java/org/robolectric/internal/bytecode/NativeMethod.java diff --git a/sandbox/src/main/java/org/robolectric/internal/bytecode/ClassDetails.java b/sandbox/src/main/java/org/robolectric/internal/bytecode/ClassDetails.java index 6d75b4a2b04..fad5aa51af1 100644 --- a/sandbox/src/main/java/org/robolectric/internal/bytecode/ClassDetails.java +++ b/sandbox/src/main/java/org/robolectric/internal/bytecode/ClassDetails.java @@ -1,8 +1,10 @@ package org.robolectric.internal.bytecode; import java.lang.annotation.Annotation; +import java.util.Arrays; import java.util.HashSet; import java.util.Set; +import org.objectweb.asm.AnnotationVisitor; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.Opcodes; @@ -13,11 +15,17 @@ * significantly faster than a {@link org.objectweb.asm.tree.ClassNode} object. */ public class ClassDetails { + private static final String SHADOWED_OBJECT_INTERNAL_NAME = + ShadowedObject.class.getName().replace('.', '/'); + private final ClassReader classReader; private final String className; + private final byte[] classBytes; private Set annotations; + private Set interfaces; public ClassDetails(byte[] classBytes) { + this.classBytes = classBytes; this.classReader = new ClassReader(classBytes); this.className = classReader.getClassName().replace('/', '.'); } @@ -38,23 +46,34 @@ public boolean hasAnnotation(Class annotationClass) { if (annotations == null) { this.annotations = new HashSet<>(); classReader.accept( - new AnnotationVisitor(annotations), + new AnnotationCollector(annotations), ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); } String internalName = "L" + annotationClass.getName().replace('.', '/') + ";"; return this.annotations.contains(internalName); } - private static class AnnotationVisitor extends ClassVisitor { + public boolean isInstrumented() { + if (this.interfaces == null) { + this.interfaces = new HashSet<>(Arrays.asList(classReader.getInterfaces())); + } + return this.interfaces.contains(SHADOWED_OBJECT_INTERNAL_NAME); + } + + public byte[] getClassBytes() { + return classBytes; + } + + private static class AnnotationCollector extends ClassVisitor { private final Set annotations; - public AnnotationVisitor(Set annotations) { + public AnnotationCollector(Set annotations) { super(Opcodes.ASM9); this.annotations = annotations; } @Override - public org.objectweb.asm.AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { + public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { if (visible) { annotations.add(descriptor); } diff --git a/sandbox/src/main/java/org/robolectric/internal/bytecode/ClassInstrumentor.java b/sandbox/src/main/java/org/robolectric/internal/bytecode/ClassInstrumentor.java index 34aa979ac40..2b73289665c 100644 --- a/sandbox/src/main/java/org/robolectric/internal/bytecode/ClassInstrumentor.java +++ b/sandbox/src/main/java/org/robolectric/internal/bytecode/ClassInstrumentor.java @@ -2,6 +2,7 @@ import java.lang.invoke.MethodType; import java.lang.reflect.Modifier; +import java.util.ArrayList; import java.util.List; import java.util.ListIterator; import org.objectweb.asm.ClassReader; @@ -16,6 +17,7 @@ import org.objectweb.asm.commons.Method; import org.objectweb.asm.commons.Remapper; import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.AnnotationNode; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.FieldInsnNode; import org.objectweb.asm.tree.FieldNode; @@ -30,6 +32,7 @@ public abstract class ClassInstrumentor { private static final String ROBO_INIT_METHOD_NAME = "$$robo$init"; static final Type OBJECT_TYPE = Type.getType(Object.class); + static final String NATIVE_METHOD_DESCRIPTOR = Type.getDescriptor(NativeMethod.class); private static final ShadowImpl SHADOW_IMPL = new ShadowImpl(); final Decorator decorator; @@ -76,11 +79,14 @@ public String map(final String internalName) { } public byte[] instrument( - byte[] origBytes, InstrumentationConfiguration config, ClassNodeProvider classNodeProvider) { + ClassDetails classDetails, + InstrumentationConfiguration config, + ClassNodeProvider classNodeProvider) { PerfStatsCollector perfStats = PerfStatsCollector.getInstance(); MutableClass mutableClass = perfStats.measure( - "analyze class", () -> analyzeClass(origBytes, config, classNodeProvider)); + "analyze class", + () -> analyzeClass(classDetails.getClassBytes(), config, classNodeProvider)); byte[] instrumentedBytes = perfStats.measure("instrument class", () -> instrumentToBytes(mutableClass)); recordPackageStats(perfStats, mutableClass); @@ -351,7 +357,8 @@ protected void instrumentNormalMethod(MutableClass mutableClass, MethodNode meth if ((method.access & Opcodes.ACC_ABSTRACT) == 0) { method.access = method.access | Opcodes.ACC_FINAL; } - if ((method.access & Opcodes.ACC_NATIVE) != 0) { + boolean isNativeMethod = (method.access & Opcodes.ACC_NATIVE) != 0; + if (isNativeMethod) { instrumentNativeMethod(mutableClass, method); } @@ -359,8 +366,19 @@ protected void instrumentNormalMethod(MutableClass mutableClass, MethodNode meth String originalName = method.name; method.name = directMethodName(mutableClass, originalName); - MethodNode delegatorMethodNode = new MethodNode(method.access, originalName, method.desc, method.signature, exceptionArray(method)); + MethodNode delegatorMethodNode = + new MethodNode( + method.access, originalName, method.desc, method.signature, exceptionArray(method)); delegatorMethodNode.visibleAnnotations = method.visibleAnnotations; + // Add a @NativeMethod annotation to the original method to indicate that this method was + // previously native. This is useful for some ClassInstrumentor plugins that do some additional + // instrumentation on native methods. + if (isNativeMethod) { + if (delegatorMethodNode.visibleAnnotations == null) { + delegatorMethodNode.visibleAnnotations = new ArrayList<>(); + } + delegatorMethodNode.visibleAnnotations.add(new AnnotationNode(NATIVE_METHOD_DESCRIPTOR)); + } delegatorMethodNode.access &= ~(Opcodes.ACC_NATIVE | Opcodes.ACC_ABSTRACT | Opcodes.ACC_FINAL); makeMethodPrivate(method); @@ -386,7 +404,7 @@ protected void instrumentNativeMethod(MutableClass mutableClass, MethodNode meth generator.returnValue(); } - private static String directMethodName(MutableClass mutableClass, String originalName) { + protected static String directMethodName(MutableClass mutableClass, String originalName) { return SHADOW_IMPL.directMethodName(mutableClass.getName(), originalName); } diff --git a/sandbox/src/main/java/org/robolectric/internal/bytecode/InstrumentationConfiguration.java b/sandbox/src/main/java/org/robolectric/internal/bytecode/InstrumentationConfiguration.java index d86daf83307..96f4026afe0 100644 --- a/sandbox/src/main/java/org/robolectric/internal/bytecode/InstrumentationConfiguration.java +++ b/sandbox/src/main/java/org/robolectric/internal/bytecode/InstrumentationConfiguration.java @@ -88,6 +88,7 @@ public boolean shouldInstrument(ClassDetails classDetails) { && !classesToNotInstrument.contains(classDetails.getName()) && !isInPackagesToNotInstrument(classDetails.getName()) && !classMatchesExclusionRegex(classDetails.getName()) + && !classDetails.isInstrumented() && !classDetails.hasAnnotation(DoNotInstrument.class) && (isInInstrumentedPackage(classDetails.getName()) || instrumentedClasses.contains(classDetails.getName()) diff --git a/sandbox/src/main/java/org/robolectric/internal/bytecode/MutableClass.java b/sandbox/src/main/java/org/robolectric/internal/bytecode/MutableClass.java index 3e95d58c612..305431f7a46 100644 --- a/sandbox/src/main/java/org/robolectric/internal/bytecode/MutableClass.java +++ b/sandbox/src/main/java/org/robolectric/internal/bytecode/MutableClass.java @@ -1,6 +1,7 @@ package org.robolectric.internal.bytecode; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; import java.util.ArrayList; import java.util.List; import org.objectweb.asm.Opcodes; @@ -19,7 +20,9 @@ public class MutableClass { final Type classType; final ImmutableSet foundMethods; - MutableClass(ClassNode classNode, InstrumentationConfiguration config, + public MutableClass( + ClassNode classNode, + InstrumentationConfiguration config, ClassNodeProvider classNodeProvider) { this.classNode = classNode; this.config = config; @@ -55,6 +58,12 @@ public void addMethod(MethodNode methodNode) { classNode.methods.add(methodNode); } + public void removeMethod(String name, String desc) { + Iterables.removeIf( + classNode.methods, + methodNode -> name.equals(methodNode.name) && desc.equals(methodNode.desc)); + } + public List getFields() { return classNode.fields; } diff --git a/sandbox/src/main/java/org/robolectric/internal/bytecode/NativeMethod.java b/sandbox/src/main/java/org/robolectric/internal/bytecode/NativeMethod.java new file mode 100644 index 00000000000..516e368d251 --- /dev/null +++ b/sandbox/src/main/java/org/robolectric/internal/bytecode/NativeMethod.java @@ -0,0 +1,15 @@ +package org.robolectric.internal.bytecode; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Added to instrumented methods to indicate that the original was a native method. This can be used + * by tools that do further processing on Robolectric instrumented classes related to native + * methods. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface NativeMethod {} diff --git a/sandbox/src/main/java/org/robolectric/internal/bytecode/SandboxClassLoader.java b/sandbox/src/main/java/org/robolectric/internal/bytecode/SandboxClassLoader.java index bc19e6f08ef..2c449451c7a 100644 --- a/sandbox/src/main/java/org/robolectric/internal/bytecode/SandboxClassLoader.java +++ b/sandbox/src/main/java/org/robolectric/internal/bytecode/SandboxClassLoader.java @@ -142,11 +142,11 @@ protected Class maybeInstrumentClass(String className) throws ClassNotFoundEx try { final byte[] bytes; ClassDetails classDetails = new ClassDetails(origClassBytes); - if (config.shouldInstrument(classDetails)) { - bytes = classInstrumentor.instrument(origClassBytes, config, classNodeProvider); + if (shouldInstrument(classDetails)) { + bytes = classInstrumentor.instrument(classDetails, config, classNodeProvider); maybeDumpClassBytes(classDetails, bytes); } else { - bytes = postProcessUninstrumentedClass(classDetails, origClassBytes); + bytes = postProcessUninstrumentedClass(classDetails); } ensurePackage(className); return defineClass(className, bytes, 0, bytes.length); @@ -171,9 +171,12 @@ private void maybeDumpClassBytes(ClassDetails classDetails, byte[] classBytes) { } } - protected byte[] postProcessUninstrumentedClass( - ClassDetails classDetails, byte[] origClassBytes) { - return origClassBytes; + protected boolean shouldInstrument(ClassDetails classDetails) { + return config.shouldInstrument(classDetails); + } + + protected byte[] postProcessUninstrumentedClass(ClassDetails classDetails) { + return classDetails.getClassBytes(); } protected byte[] getByteCode(String className) throws ClassNotFoundException {