blob: 1ba7356b69a315a406faeb95fb7d8c6afe714936 [file] [log] [blame]
package org.robolectric.internal.bytecode;
import static java.lang.invoke.MethodType.methodType;
import java.lang.invoke.CallSite;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Modifier;
import java.util.List;
import java.util.ListIterator;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Handle;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.ClassRemapper;
import org.objectweb.asm.commons.GeneratorAdapter;
import org.objectweb.asm.commons.JSRInlinerAdapter;
import org.objectweb.asm.commons.Method;
import org.objectweb.asm.commons.Remapper;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldInsnNode;
import org.objectweb.asm.tree.FieldNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.InsnNode;
import org.objectweb.asm.tree.InvokeDynamicInsnNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.TypeInsnNode;
import org.objectweb.asm.tree.VarInsnNode;
import org.robolectric.util.PerfStatsCollector;
/**
* Instruments (i.e. modifies the bytecode) of classes to place the scaffolding necessary to use
* Robolectric's shadows.
*/
public class ClassInstrumentor {
private static final Handle BOOTSTRAP_INIT;
private static final Handle BOOTSTRAP;
private static final Handle BOOTSTRAP_STATIC;
private static final Handle BOOTSTRAP_INTRINSIC;
private static final String ROBO_INIT_METHOD_NAME = "$$robo$init";
static final Type OBJECT_TYPE = Type.getType(Object.class);
private static final ShadowImpl SHADOW_IMPL = new ShadowImpl();
final Decorator decorator;
static {
String className = Type.getInternalName(InvokeDynamicSupport.class);
MethodType bootstrap =
methodType(CallSite.class, MethodHandles.Lookup.class, String.class, MethodType.class);
String bootstrapMethod =
bootstrap.appendParameterTypes(MethodHandle.class).toMethodDescriptorString();
String bootstrapIntrinsic =
bootstrap.appendParameterTypes(String.class).toMethodDescriptorString();
BOOTSTRAP_INIT =
new Handle(
Opcodes.H_INVOKESTATIC,
className,
"bootstrapInit",
bootstrap.toMethodDescriptorString(),
false);
BOOTSTRAP = new Handle(Opcodes.H_INVOKESTATIC, className, "bootstrap", bootstrapMethod, false);
BOOTSTRAP_STATIC =
new Handle(Opcodes.H_INVOKESTATIC, className, "bootstrapStatic", bootstrapMethod, false);
BOOTSTRAP_INTRINSIC =
new Handle(
Opcodes.H_INVOKESTATIC, className, "bootstrapIntrinsic", bootstrapIntrinsic, false);
}
public ClassInstrumentor() {
this(new ShadowDecorator());
}
protected ClassInstrumentor(Decorator decorator) {
this.decorator = decorator;
}
private MutableClass analyzeClass(
byte[] origClassBytes,
final InstrumentationConfiguration config,
ClassNodeProvider classNodeProvider) {
ClassNode classNode =
new ClassNode(Opcodes.ASM4) {
@Override
public MethodVisitor visitMethod(
int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor methodVisitor =
super.visitMethod(access, name, config.remapParams(desc), signature, exceptions);
return new JSRInlinerAdapter(methodVisitor, access, name, desc, signature, exceptions);
}
};
final ClassReader classReader = new ClassReader(origClassBytes);
classReader.accept(classNode, 0);
return new MutableClass(classNode, config, classNodeProvider);
}
byte[] instrumentToBytes(MutableClass mutableClass) {
instrument(mutableClass);
ClassNode classNode = mutableClass.classNode;
ClassWriter writer = new InstrumentingClassWriter(mutableClass.classNodeProvider, classNode);
Remapper remapper =
new Remapper() {
@Override
public String map(final String internalName) {
return mutableClass.config.mappedTypeName(internalName);
}
};
ClassRemapper visitor = new ClassRemapper(writer, remapper);
classNode.accept(visitor);
return writer.toByteArray();
}
public byte[] instrument(
ClassDetails classDetails,
InstrumentationConfiguration config,
ClassNodeProvider classNodeProvider) {
PerfStatsCollector perfStats = PerfStatsCollector.getInstance();
MutableClass mutableClass =
perfStats.measure(
"analyze class",
() -> analyzeClass(classDetails.getClassBytes(), config, classNodeProvider));
byte[] instrumentedBytes =
perfStats.measure("instrument class", () -> instrumentToBytes(mutableClass));
recordPackageStats(perfStats, mutableClass);
return instrumentedBytes;
}
private void recordPackageStats(PerfStatsCollector perfStats, MutableClass mutableClass) {
String className = mutableClass.getName();
for (int i = className.indexOf('.'); i != -1; i = className.indexOf('.', i + 1)) {
perfStats.incrementCount("instrument package " + className.substring(0, i));
}
}
public void instrument(MutableClass mutableClass) {
try {
// Need Java version >=7 to allow invokedynamic
mutableClass.classNode.version = Math.max(mutableClass.classNode.version, Opcodes.V1_7);
if (mutableClass.getName().equals("android.util.SparseArray")) {
addSetToSparseArray(mutableClass);
}
instrumentMethods(mutableClass);
if (mutableClass.isInterface()) {
mutableClass.addInterface(Type.getInternalName(InstrumentedInterface.class));
} else {
makeClassPublic(mutableClass.classNode);
if ((mutableClass.classNode.access & Opcodes.ACC_FINAL) == Opcodes.ACC_FINAL) {
mutableClass
.classNode
.visitAnnotation("Lcom/google/errorprone/annotations/DoNotMock;", true)
.visit(
"value",
"This class is final. Consider using the real thing, or "
+ "adding/enhancing a Robolectric shadow for it.");
}
mutableClass.classNode.access = mutableClass.classNode.access & ~Opcodes.ACC_FINAL;
// If there is no constructor, adds one
addNoArgsConstructor(mutableClass);
addDirectCallConstructor(mutableClass);
addRoboInitMethod(mutableClass);
removeFinalFromFields(mutableClass);
decorator.decorate(mutableClass);
}
} catch (Exception e) {
throw new RuntimeException("failed to instrument " + mutableClass.getName(), e);
}
}
// See https://github.com/robolectric/robolectric/issues/6840
// Adds Set(int, object) to android.util.SparseArray.
private void addSetToSparseArray(MutableClass mutableClass) {
for (MethodNode method : mutableClass.getMethods()) {
if ("set".equals(method.name)) {
return;
}
}
MethodNode setFunction =
new MethodNode(
Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC,
"set",
"(ILjava/lang/Object;)V",
"(ITE;)V",
null);
RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(setFunction);
generator.loadThis();
generator.loadArg(0);
generator.loadArg(1);
generator.invokeVirtual(mutableClass.classType, new Method("put", "(ILjava/lang/Object;)V"));
generator.returnValue();
mutableClass.addMethod(setFunction);
}
private void instrumentMethods(MutableClass mutableClass) {
if (mutableClass.isInterface()) {
for (MethodNode method : mutableClass.getMethods()) {
rewriteMethodBody(mutableClass, method);
}
} else {
for (MethodNode method : mutableClass.getMethods()) {
rewriteMethodBody(mutableClass, method);
if (method.name.equals("<clinit>")) {
method.name = ShadowConstants.STATIC_INITIALIZER_METHOD_NAME;
mutableClass.addMethod(generateStaticInitializerNotifierMethod(mutableClass));
} else if (method.name.equals("<init>")) {
instrumentConstructor(mutableClass, method);
} else if (!isSyntheticAccessorMethod(method) && !Modifier.isAbstract(method.access)) {
instrumentNormalMethod(mutableClass, method);
}
}
}
}
private static void addNoArgsConstructor(MutableClass mutableClass) {
if (!mutableClass.foundMethods.contains("<init>()V")) {
MethodNode defaultConstructor =
new MethodNode(Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC, "<init>", "()V", "()V", null);
RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(defaultConstructor);
generator.loadThis();
generator.visitMethodInsn(
Opcodes.INVOKESPECIAL, mutableClass.classNode.superName, "<init>", "()V", false);
generator.loadThis();
generator.invokeVirtual(mutableClass.classType, new Method(ROBO_INIT_METHOD_NAME, "()V"));
generator.returnValue();
mutableClass.addMethod(defaultConstructor);
}
}
protected void addDirectCallConstructor(MutableClass mutableClass) {}
/**
* Generates code like this:
*
* <pre>
* protected void $$robo$init() {
* if (__robo_data__ == null) {
* __robo_data__ = RobolectricInternals.initializing(this);
* }
* }
* </pre>
*/
private void addRoboInitMethod(MutableClass mutableClass) {
MethodNode initMethodNode =
new MethodNode(
Opcodes.ACC_PROTECTED | Opcodes.ACC_SYNTHETIC,
ROBO_INIT_METHOD_NAME,
"()V",
null,
null);
RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(initMethodNode);
Label alreadyInitialized = new Label();
generator.loadThis(); // this
generator.getField(
mutableClass.classType,
ShadowConstants.CLASS_HANDLER_DATA_FIELD_NAME,
OBJECT_TYPE); // contents of __robo_data__
generator.ifNonNull(alreadyInitialized);
generator.loadThis(); // this
generator.loadThis(); // this, this
writeCallToInitializing(mutableClass, generator);
// this, __robo_data__
generator.putField(
mutableClass.classType, ShadowConstants.CLASS_HANDLER_DATA_FIELD_NAME, OBJECT_TYPE);
generator.mark(alreadyInitialized);
generator.returnValue();
mutableClass.addMethod(initMethodNode);
}
protected void writeCallToInitializing(
MutableClass mutableClass, RobolectricGeneratorAdapter generator) {
generator.invokeDynamic(
"initializing",
Type.getMethodDescriptor(OBJECT_TYPE, mutableClass.classType),
BOOTSTRAP_INIT);
}
private static void removeFinalFromFields(MutableClass mutableClass) {
for (FieldNode fieldNode : mutableClass.getFields()) {
fieldNode.access &= ~Modifier.FINAL;
}
}
private static boolean isSyntheticAccessorMethod(MethodNode method) {
return (method.access & Opcodes.ACC_SYNTHETIC) != 0;
}
/**
* Constructors are instrumented as follows: TODO(slliu): Fill in constructor instrumentation
* directions
*
* @param method the constructor to instrument
*/
private void instrumentConstructor(MutableClass mutableClass, MethodNode method) {
makeMethodPrivate(method);
InsnList callSuper = extractCallToSuperConstructor(mutableClass, method);
method.name = directMethodName(mutableClass, ShadowConstants.CONSTRUCTOR_METHOD_NAME);
mutableClass.addMethod(
redirectorMethod(mutableClass, method, ShadowConstants.CONSTRUCTOR_METHOD_NAME));
String[] exceptions = exceptionArray(method);
MethodNode initMethodNode =
new MethodNode(method.access, "<init>", method.desc, method.signature, exceptions);
makeMethodPublic(initMethodNode);
RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(initMethodNode);
initMethodNode.instructions = callSuper;
generator.loadThis();
generator.invokeVirtual(mutableClass.classType, new Method(ROBO_INIT_METHOD_NAME, "()V"));
generateClassHandlerCall(
mutableClass, method, ShadowConstants.CONSTRUCTOR_METHOD_NAME, generator);
generator.endMethod();
mutableClass.addMethod(initMethodNode);
}
private static InsnList extractCallToSuperConstructor(
MutableClass mutableClass, MethodNode ctor) {
InsnList removedInstructions = new InsnList();
int startIndex = 0;
AbstractInsnNode[] insns = ctor.instructions.toArray();
for (int i = 0; i < insns.length; i++) {
AbstractInsnNode node = insns[i];
switch (node.getOpcode()) {
case Opcodes.ALOAD:
VarInsnNode vnode = (VarInsnNode) node;
if (vnode.var == 0) {
startIndex = i;
}
break;
case Opcodes.PUTFIELD:
FieldInsnNode pnode = (FieldInsnNode) node;
if (pnode.owner.equals(mutableClass.internalClassName) && pnode.name.equals("this$0")) {
// remove all instructions in the range startIndex..i, from aload_0 to putfield this$0
while (startIndex <= i) {
ctor.instructions.remove(insns[startIndex]);
removedInstructions.add(insns[startIndex]);
startIndex++;
}
}
break;
case Opcodes.INVOKESPECIAL:
MethodInsnNode mnode = (MethodInsnNode) node;
if (mnode.owner.equals(mutableClass.internalClassName)
|| mnode.owner.equals(mutableClass.classNode.superName)) {
if (!"<init>".equals(mnode.name)) {
throw new AssertionError("Invalid MethodInsnNode name");
}
// remove all instructions in the range startIndex..i, from aload_0 to invokespecial
// <init>
while (startIndex <= i) {
ctor.instructions.remove(insns[startIndex]);
removedInstructions.add(insns[startIndex]);
startIndex++;
}
return removedInstructions;
}
break;
case Opcodes.ATHROW:
ctor.visitCode();
ctor.visitInsn(Opcodes.RETURN);
ctor.visitEnd();
return removedInstructions;
default:
// nothing to do
}
}
throw new RuntimeException("huh? " + ctor.name + ctor.desc);
}
/**
* Instruments a normal method
*
* <ul>
* <li>Rename the method from {@code methodName} to {@code $$robo$$methodName}.
* <li>Make it private so we can invoke it directly without subclass overrides taking
* precedence.
* <li>Remove {@code final} modifiers, if present.
* <li>Create a delegator method named {@code methodName} which delegates to the {@link
* ClassHandler}.
* </ul>
*/
protected void instrumentNormalMethod(MutableClass mutableClass, MethodNode method) {
// if not abstract, set a final modifier
if ((method.access & Opcodes.ACC_ABSTRACT) == 0) {
method.access = method.access | Opcodes.ACC_FINAL;
}
boolean isNativeMethod = (method.access & Opcodes.ACC_NATIVE) != 0;
if (isNativeMethod) {
instrumentNativeMethod(mutableClass, method);
}
// todo figure out
String originalName = method.name;
method.name = directMethodName(mutableClass, originalName);
MethodNode delegatorMethodNode =
new MethodNode(
method.access, originalName, method.desc, method.signature, exceptionArray(method));
delegatorMethodNode.visibleAnnotations = method.visibleAnnotations;
delegatorMethodNode.access &= ~(Opcodes.ACC_NATIVE | Opcodes.ACC_ABSTRACT | Opcodes.ACC_FINAL);
makeMethodPrivate(method);
RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(delegatorMethodNode);
generateClassHandlerCall(mutableClass, method, originalName, generator);
generator.endMethod();
mutableClass.addMethod(delegatorMethodNode);
}
/**
* Creates native stub which returns the default return value.
*
* @param mutableClass Class to be instrumented
* @param method Method to be instrumented, must be native
*/
protected void instrumentNativeMethod(MutableClass mutableClass, MethodNode method) {
method.access = method.access & ~Opcodes.ACC_NATIVE;
RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(method);
Type returnType = generator.getReturnType();
generator.pushDefaultReturnValueToStack(returnType);
generator.returnValue();
}
protected static String directMethodName(MutableClass mutableClass, String originalName) {
return SHADOW_IMPL.directMethodName(mutableClass.getName(), originalName);
}
// todo rename
private MethodNode redirectorMethod(
MutableClass mutableClass, MethodNode method, String newName) {
MethodNode redirector =
new MethodNode(
Opcodes.ASM4, newName, method.desc, method.signature, exceptionArray(method));
redirector.access =
method.access & ~(Opcodes.ACC_NATIVE | Opcodes.ACC_ABSTRACT | Opcodes.ACC_FINAL);
makeMethodPrivate(redirector);
RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(redirector);
generator.invokeMethod(mutableClass.internalClassName, method);
generator.returnValue();
return redirector;
}
protected String[] exceptionArray(MethodNode method) {
List<String> exceptions = method.exceptions;
return exceptions.toArray(new String[exceptions.size()]);
}
/** Filters methods that might need special treatment because of various reasons */
private void rewriteMethodBody(MutableClass mutableClass, MethodNode callingMethod) {
ListIterator<AbstractInsnNode> instructions = callingMethod.instructions.iterator();
while (instructions.hasNext()) {
AbstractInsnNode node = instructions.next();
switch (node.getOpcode()) {
case Opcodes.NEW:
TypeInsnNode newInsnNode = (TypeInsnNode) node;
newInsnNode.desc = mutableClass.config.mappedTypeName(newInsnNode.desc);
break;
case Opcodes.GETFIELD:
/* falls through */
case Opcodes.PUTFIELD:
/* falls through */
case Opcodes.GETSTATIC:
/* falls through */
case Opcodes.PUTSTATIC:
FieldInsnNode fieldInsnNode = (FieldInsnNode) node;
fieldInsnNode.desc = mutableClass.config.mappedTypeName(fieldInsnNode.desc); // todo test
break;
case Opcodes.INVOKESTATIC:
/* falls through */
case Opcodes.INVOKEINTERFACE:
/* falls through */
case Opcodes.INVOKESPECIAL:
/* falls through */
case Opcodes.INVOKEVIRTUAL:
MethodInsnNode targetMethod = (MethodInsnNode) node;
targetMethod.desc = mutableClass.config.remapParams(targetMethod.desc);
if (isGregorianCalendarBooleanConstructor(targetMethod)) {
replaceGregorianCalendarBooleanConstructor(instructions, targetMethod);
} else if (mutableClass.config.shouldIntercept(targetMethod)) {
interceptInvokeVirtualMethod(mutableClass, instructions, targetMethod);
}
break;
case Opcodes.INVOKEDYNAMIC:
/* no unusual behavior */
break;
default:
break;
}
}
}
/**
* Verifies if the @targetMethod is a {@code <init>(boolean)} constructor for {@link
* java.util.GregorianCalendar}.
*/
private static boolean isGregorianCalendarBooleanConstructor(MethodInsnNode targetMethod) {
return targetMethod.owner.equals("java/util/GregorianCalendar")
&& targetMethod.name.equals("<init>")
&& targetMethod.desc.equals("(Z)V");
}
/**
* Replaces the void {@code <init>(boolean)} constructor for a call to the {@code void <init>(int,
* int, int)} one.
*/
private static void replaceGregorianCalendarBooleanConstructor(
ListIterator<AbstractInsnNode> instructions, MethodInsnNode targetMethod) {
// Remove the call to GregorianCalendar(boolean)
instructions.remove();
// Discard the already-pushed parameter for GregorianCalendar(boolean)
instructions.add(new InsnNode(Opcodes.POP));
// Add parameters values for calling GregorianCalendar(int, int, int)
instructions.add(new InsnNode(Opcodes.ICONST_0));
instructions.add(new InsnNode(Opcodes.ICONST_0));
instructions.add(new InsnNode(Opcodes.ICONST_0));
// Call GregorianCalendar(int, int, int)
instructions.add(
new MethodInsnNode(
Opcodes.INVOKESPECIAL,
targetMethod.owner,
targetMethod.name,
"(III)V",
targetMethod.itf));
}
/**
* Decides to call through the appropriate method to intercept the method with an INVOKEVIRTUAL
* Opcode, depending if the invokedynamic bytecode instruction is available (Java 7+).
*/
protected void interceptInvokeVirtualMethod(
MutableClass mutableClass,
ListIterator<AbstractInsnNode> instructions,
MethodInsnNode targetMethod) {
instructions.remove(); // remove the method invocation
Type type = Type.getObjectType(targetMethod.owner);
String description = targetMethod.desc;
String owner = type.getClassName();
if (targetMethod.getOpcode() != Opcodes.INVOKESTATIC) {
String thisType = type.getDescriptor();
description = "(" + thisType + description.substring(1);
}
instructions.add(
new InvokeDynamicInsnNode(targetMethod.name, description, BOOTSTRAP_INTRINSIC, owner));
}
/** Replaces protected and private class modifiers with public. */
private static void makeClassPublic(ClassNode clazz) {
clazz.access =
(clazz.access | Opcodes.ACC_PUBLIC) & ~(Opcodes.ACC_PROTECTED | Opcodes.ACC_PRIVATE);
}
/** Replaces protected and private method modifiers with public. */
protected void makeMethodPublic(MethodNode method) {
method.access =
(method.access | Opcodes.ACC_PUBLIC) & ~(Opcodes.ACC_PROTECTED | Opcodes.ACC_PRIVATE);
}
/** Replaces protected and public class modifiers with private. */
protected void makeMethodPrivate(MethodNode method) {
method.access =
(method.access | Opcodes.ACC_PRIVATE) & ~(Opcodes.ACC_PUBLIC | Opcodes.ACC_PROTECTED);
}
private static MethodNode generateStaticInitializerNotifierMethod(MutableClass mutableClass) {
MethodNode methodNode = new MethodNode(Opcodes.ACC_STATIC, "<clinit>", "()V", "()V", null);
RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(methodNode);
generator.push(mutableClass.classType);
generator.invokeStatic(
Type.getType(RobolectricInternals.class),
new Method("classInitializing", "(Ljava/lang/Class;)V"));
generator.returnValue();
generator.endMethod();
return methodNode;
}
// todo javadocs
protected void generateClassHandlerCall(
MutableClass mutableClass,
MethodNode originalMethod,
String originalMethodName,
RobolectricGeneratorAdapter generator) {
Handle original =
new Handle(
getTag(originalMethod),
mutableClass.classType.getInternalName(),
originalMethod.name,
originalMethod.desc,
getTag(originalMethod) == Opcodes.H_INVOKEINTERFACE);
if (generator.isStatic()) {
generator.loadArgs();
generator.invokeDynamic(originalMethodName, originalMethod.desc, BOOTSTRAP_STATIC, original);
} else {
String desc = "(" + mutableClass.classType.getDescriptor() + originalMethod.desc.substring(1);
generator.loadThis();
generator.loadArgs();
generator.invokeDynamic(originalMethodName, desc, BOOTSTRAP, original);
}
generator.returnValue();
}
int getTag(MethodNode m) {
return Modifier.isStatic(m.access) ? Opcodes.H_INVOKESTATIC : Opcodes.H_INVOKESPECIAL;
}
public interface Decorator {
void decorate(MutableClass mutableClass);
}
/**
* Provides try/catch code generation with a {@link org.objectweb.asm.commons.GeneratorAdapter}.
*/
static class TryCatch {
private final Label start;
private final Label end;
private final Label handler;
private final GeneratorAdapter generatorAdapter;
TryCatch(GeneratorAdapter generatorAdapter, Type type) {
this.generatorAdapter = generatorAdapter;
this.start = generatorAdapter.mark();
this.end = new Label();
this.handler = new Label();
generatorAdapter.visitTryCatchBlock(start, end, handler, type.getInternalName());
}
void end() {
generatorAdapter.mark(end);
}
void handler() {
generatorAdapter.mark(handler);
}
}
}