package org.robolectric.internal.bytecode;
import javax.annotation.Nonnull;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.FieldVisitor;
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.GeneratorAdapter;
import org.objectweb.asm.commons.JSRInlinerAdapter;
import org.objectweb.asm.commons.Method;
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.LdcInsnNode;
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.Logger;
import org.robolectric.util.ReflectionHelpers;
import org.robolectric.util.Util;
import java.io.IOException;
import java.io.InputStream;
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.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import static java.lang.invoke.MethodType.methodType;
import static org.objectweb.asm.Type.ARRAY;
import static org.objectweb.asm.Type.OBJECT;
import static org.objectweb.asm.Type.VOID;
import static org.robolectric.util.ReflectionHelpers.ClassParameter.from;
/**
* Class loader that modifies the bytecode of Android classes to insert calls to Robolectric's shadow classes.
*/
public class SandboxClassLoader extends URLClassLoader implements Opcodes {
private final URLClassLoader systemClassLoader;
private final URLClassLoader urls;
private final InstrumentationConfiguration config;
private final Map<String, String> classesToRemap;
private final Set<MethodRef> methodsToIntercept;
public SandboxClassLoader(InstrumentationConfiguration config) {
this(((URLClassLoader) ClassLoader.getSystemClassLoader()), config);
}
public SandboxClassLoader(URLClassLoader systemClassLoader, InstrumentationConfiguration config, URL... urls) {
super(systemClassLoader.getURLs(), systemClassLoader.getParent());
this.systemClassLoader = systemClassLoader;
this.config = config;
this.urls = new URLClassLoader(urls, null);
classesToRemap = convertToSlashes(config.classNameTranslations());
methodsToIntercept = convertToSlashes(config.methodsToIntercept());
for (URL url : urls) {
Logger.debug("Loading classes from: %s", url);
}
}
@Override
public URL getResource(String name) {
URL fromParent = super.getResource(name);
if (fromParent != null) {
return fromParent;
}
return urls.getResource(name);
}
private InputStream getClassBytesAsStreamPreferringLocalUrls(String resName) {
InputStream fromUrlsClassLoader = urls.getResourceAsStream(resName);
if (fromUrlsClassLoader != null) {
return fromUrlsClassLoader;
}
return super.getResourceAsStream(resName);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
if (config.shouldAcquire(name)) {
return maybeInstrumentClass(name);
} else {
return systemClassLoader.loadClass(name);
}
}
protected Class<?> maybeInstrumentClass(String className) throws ClassNotFoundException {
final byte[] origClassBytes = getByteCode(className);
ClassNode classNode = new ClassNode(Opcodes.ASM4) {
@Override
public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
desc = remapParamType(desc);
return super.visitField(access, name, desc, signature, value);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor methodVisitor = super.visitMethod(access, name, remapParams(desc), signature, exceptions);
return new JSRInlinerAdapter(methodVisitor, access, name, desc, signature, exceptions);
}
};
final ClassReader classReader = new ClassReader(origClassBytes);
classReader.accept(classNode, 0);
classNode.interfaces.add(Type.getInternalName(ShadowedObject.class));
try {
byte[] bytes;
ClassInfo classInfo = new ClassInfo(className, classNode);
if (config.shouldInstrument(classInfo)) {
bytes = getInstrumentedBytes(classNode, config.containsStubs(classInfo));
} else {
bytes = origClassBytes;
}
ensurePackage(className);
return defineClass(className, bytes, 0, bytes.length);
} catch (Exception e) {
throw new ClassNotFoundException("couldn't load " + className, e);
} catch (OutOfMemoryError e) {
System.err.println("[ERROR] couldn't load " + className + " in " + this);
throw e;
}
}
@Override
protected Package getPackage(String name) {
Package aPackage = super.getPackage(name);
if (aPackage != null) {
return aPackage;
}
return ReflectionHelpers.callInstanceMethod(systemClassLoader, "getPackage",
from(String.class, name));
}
protected byte[] getByteCode(String className) throws ClassNotFoundException {
String classFilename = className.replace('.', '/') + ".class";
try (InputStream classBytesStream = getClassBytesAsStreamPreferringLocalUrls(classFilename)) {
if (classBytesStream == null) throw new ClassNotFoundException(className);
return Util.readBytes(classBytesStream);
} catch (IOException e) {
throw new ClassNotFoundException("couldn't load " + className, e);
}
}
private void ensurePackage(final String className) {
int lastDotIndex = className.lastIndexOf('.');
if (lastDotIndex != -1) {
String pckgName = className.substring(0, lastDotIndex);
Package pckg = getPackage(pckgName);
if (pckg == null) {
definePackage(pckgName, null, null, null, null, null, null, null);
}
}
}
private String remapParams(String desc) {
StringBuilder buf = new StringBuilder();
buf.append("(");
for (Type type : Type.getArgumentTypes(desc)) {
buf.append(remapParamType(type));
}
buf.append(")");
buf.append(remapParamType(Type.getReturnType(desc)));
return buf.toString();
}
// remap Landroid/Foo; to Landroid/Bar;
private String remapParamType(String desc) {
return remapParamType(Type.getType(desc));
}
private String remapParamType(Type type) {
String remappedName;
String internalName;
switch (type.getSort()) {
case ARRAY:
internalName = type.getInternalName();
int count = 0;
while (internalName.charAt(count) == '[') count++;
remappedName = remapParamType(internalName.substring(count));
if (remappedName != null) {
return Type.getObjectType(internalName.substring(0, count) + remappedName).getDescriptor();
}
break;
case OBJECT:
internalName = type.getInternalName();
remappedName = classesToRemap.get(internalName);
if (remappedName != null) {
return Type.getObjectType(remappedName).getDescriptor();
}
break;
default:
break;
}
return type.getDescriptor();
}
// remap android/Foo to android/Bar
private String remapType(String value) {
String remappedValue = classesToRemap.get(value);
if (remappedValue != null) {
value = remappedValue;
}
return value;
}
private byte[] getInstrumentedBytes(ClassNode classNode, boolean containsStubs) throws ClassNotFoundException {
if (InvokeDynamic.ENABLED) {
new InvokeDynamicClassInstrumentor(classNode, containsStubs).instrument();
} else {
new OldClassInstrumentor(classNode, containsStubs).instrument();
}
ClassWriter writer = new InstrumentingClassWriter(classNode);
classNode.accept(writer);
return writer.toByteArray();
}
private Map<String, String> convertToSlashes(Map<String, String> map) {
HashMap<String, String> newMap = new HashMap<>();
for (Map.Entry<String, String> entry : map.entrySet()) {
String key = internalize(entry.getKey());
String value = internalize(entry.getValue());
newMap.put(key, value);
newMap.put("L" + key + ";", "L" + value + ";"); // also the param reference form
}
return newMap;
}
private Set<MethodRef> convertToSlashes(Set<MethodRef> methodRefs) {
HashSet<MethodRef> transformed = new HashSet<>();
for (MethodRef methodRef : methodRefs) {
transformed.add(new MethodRef(internalize(methodRef.className), methodRef.methodName));
}
return transformed;
}
private String internalize(String className) {
return className.replace('.', '/');
}
public static void box(final Type type, ListIterator<AbstractInsnNode> instructions) {
if (type.getSort() == Type.OBJECT || type.getSort() == Type.ARRAY) {
return;
}
if (type == Type.VOID_TYPE) {
instructions.add(new InsnNode(ACONST_NULL));
} else {
Type boxed = getBoxedType(type);
instructions.add(new TypeInsnNode(NEW, boxed.getInternalName()));
if (type.getSize() == 2) {
// Pp -> Ppo -> oPpo -> ooPpo -> ooPp -> o
instructions.add(new InsnNode(DUP_X2));
instructions.add(new InsnNode(DUP_X2));
instructions.add(new InsnNode(POP));
} else {
// p -> po -> opo -> oop -> o
instructions.add(new InsnNode(DUP_X1));
instructions.add(new InsnNode(SWAP));
}
instructions.add(new MethodInsnNode(INVOKESPECIAL, boxed.getInternalName(), "<init>", "(" + type.getDescriptor() + ")V"));
}
}
private static Type getBoxedType(final Type type) {
switch (type.getSort()) {
case Type.BYTE:
return Type.getObjectType("java/lang/Byte");
case Type.BOOLEAN:
return Type.getObjectType("java/lang/Boolean");
case Type.SHORT:
return Type.getObjectType("java/lang/Short");
case Type.CHAR:
return Type.getObjectType("java/lang/Character");
case Type.INT:
return Type.getObjectType("java/lang/Integer");
case Type.FLOAT:
return Type.getObjectType("java/lang/Float");
case Type.LONG:
return Type.getObjectType("java/lang/Long");
case Type.DOUBLE:
return Type.getObjectType("java/lang/Double");
}
return type;
}
private boolean shouldIntercept(MethodInsnNode targetMethod) {
if (targetMethod.name.equals("<init>")) return false; // sorry, can't strip out calls to super() in constructor
return methodsToIntercept.contains(new MethodRef(targetMethod.owner, targetMethod.name))
|| methodsToIntercept.contains(new MethodRef(targetMethod.owner, "*"));
}
abstract class ClassInstrumentor {
private static final String ROBO_INIT_METHOD_NAME = "$$robo$init";
static final String GET_ROBO_DATA_SIGNATURE = "()Ljava/lang/Object;";
final Type OBJECT_TYPE = Type.getType(Object.class);
private final String OBJECT_DESC = Type.getDescriptor(Object.class);
final ClassNode classNode;
private final boolean containsStubs;
final String internalClassName;
private final String className;
final Type classType;
public ClassInstrumentor(ClassNode classNode, boolean containsStubs) {
this.classNode = classNode;
this.containsStubs = containsStubs;
this.internalClassName = classNode.name;
this.className = classNode.name.replace('/', '.');
this.classType = Type.getObjectType(internalClassName);
}
//todo javadoc. Extract blocks to separate methods.
public void instrument() {
makeClassPublic(classNode);
classNode.access = classNode.access & ~ACC_FINAL;
// Need Java version >=7 to allow invokedynamic
classNode.version = Math.max(classNode.version, V1_7);
classNode.fields.add(0, new FieldNode(ACC_PUBLIC | ACC_FINAL,
ShadowConstants.CLASS_HANDLER_DATA_FIELD_NAME, OBJECT_DESC, OBJECT_DESC, null));
Set<String> foundMethods = instrumentMethods();
// If there is no constructor, adds one
addNoArgsConstructor(foundMethods);
addDirectCallConstructor();
// Do not override final #equals, #hashCode, and #toString for all classes
instrumentInheritedObjectMethod(classNode, foundMethods, "equals", "(Ljava/lang/Object;)Z");
instrumentInheritedObjectMethod(classNode, foundMethods, "hashCode", "()I");
instrumentInheritedObjectMethod(classNode, foundMethods, "toString", "()Ljava/lang/String;");
addRoboInitMethod();
addRoboGetDataMethod();
doSpecialHandling();
}
@Nonnull
private Set<String> instrumentMethods() {
Set<String> foundMethods = new HashSet<>();
List<MethodNode> methods = new ArrayList<>(classNode.methods);
for (MethodNode method : methods) {
foundMethods.add(method.name + method.desc);
filterSpecialMethods(method);
if (method.name.equals("<clinit>")) {
method.name = ShadowConstants.STATIC_INITIALIZER_METHOD_NAME;
classNode.methods.add(generateStaticInitializerNotifierMethod());
} else if (method.name.equals("<init>")) {
instrumentConstructor(method);
} else if (!isSyntheticAccessorMethod(method) && !Modifier.isAbstract(method.access)) {
instrumentNormalMethod(method);
}
}
return foundMethods;
}
private void addNoArgsConstructor(Set<String> foundMethods) {
if (!foundMethods.contains("<init>()V")) {
MethodNode defaultConstructor = new MethodNode(ACC_PUBLIC, "<init>", "()V", "()V", null);
RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(defaultConstructor);
generator.loadThis();
generator.visitMethodInsn(INVOKESPECIAL, classNode.superName, "<init>", "()V");
generator.loadThis();
generator.invokeVirtual(classType, new Method(ROBO_INIT_METHOD_NAME, "()V"));
generator.returnValue();
classNode.methods.add(defaultConstructor);
}
}
abstract protected void addDirectCallConstructor();
private void addRoboInitMethod() {
MethodNode initMethodNode = new MethodNode(ACC_PROTECTED, ROBO_INIT_METHOD_NAME, "()V", null, null);
RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(initMethodNode);
Label alreadyInitialized = new Label();
generator.loadThis(); // this
generator.getField(classType, ShadowConstants.CLASS_HANDLER_DATA_FIELD_NAME, OBJECT_TYPE); // contents of __robo_data__
generator.ifNonNull(alreadyInitialized);
generator.loadThis(); // this
generator.loadThis(); // this, this
writeCallToInitializing(generator);
// this, __robo_data__
generator.putField(classType, ShadowConstants.CLASS_HANDLER_DATA_FIELD_NAME, OBJECT_TYPE);
generator.mark(alreadyInitialized);
generator.returnValue();
classNode.methods.add(initMethodNode);
}
abstract protected void writeCallToInitializing(RobolectricGeneratorAdapter generator);
private void addRoboGetDataMethod() {
MethodNode initMethodNode = new MethodNode(ACC_PUBLIC, ShadowConstants.GET_ROBO_DATA_METHOD_NAME, GET_ROBO_DATA_SIGNATURE, null, null);
RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(initMethodNode);
generator.loadThis(); // this
generator.getField(classType, ShadowConstants.CLASS_HANDLER_DATA_FIELD_NAME, OBJECT_TYPE); // contents of __robo_data__
generator.returnValue();
generator.endMethod();
classNode.methods.add(initMethodNode);
}
private void doSpecialHandling() {
if (className.equals("android.os.Build$VERSION")) {
for (Object field : classNode.fields) {
FieldNode fieldNode = (FieldNode) field;
fieldNode.access &= ~(Modifier.FINAL);
}
}
}
/**
* Checks if the given method in the class if overriding, at some point of it's
* inheritance tree, a final method
*/
private boolean isOverridingFinalMethod(ClassNode classNode, String methodName, String methodSignature) {
while (true) {
List<MethodNode> methods = new ArrayList<>(classNode.methods);
for (MethodNode method : methods) {
if (method.name.equals(methodName) && method.desc.equals(methodSignature)) {
if ((method.access & ACC_FINAL) != 0) {
return true;
}
}
}
if (classNode.superName == null) {
return false;
}
try {
byte[] byteCode = getByteCode(classNode.superName);
ClassReader classReader = new ClassReader(byteCode);
classNode = new ClassNode();
classReader.accept(classNode, 0);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
private boolean isSyntheticAccessorMethod(MethodNode method) {
return (method.access & ACC_SYNTHETIC) != 0;
}
/**
* To be used to instrument methods inherited from the Object class,
* such as hashCode, equals, and toString.
* Adds the methods directly to the class.
*/
private void instrumentInheritedObjectMethod(ClassNode classNode, Set<String> foundMethods, final String methodName, String methodDesc) {
// Won't instrument if method is overriding a final method
if (isOverridingFinalMethod(classNode, methodName, methodDesc)) {
return;
}
// if the class doesn't directly override the method, it adds it as a direct invocation and instruments it
if (!foundMethods.contains(methodName + methodDesc)) {
MethodNode methodNode = new MethodNode(ACC_PUBLIC, methodName, methodDesc, null, null);
RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(methodNode);
generator.invokeMethod("java/lang/Object", methodNode);
generator.returnValue();
generator.endMethod();
this.classNode.methods.add(methodNode);
instrumentNormalMethod(methodNode);
}
}
private void instrumentConstructor(MethodNode method) {
makeMethodPrivate(method);
if (containsStubs) {
method.instructions.clear();
RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(method);
generator.loadThis();
generator.visitMethodInsn(INVOKESPECIAL, classNode.superName, "<init>", "()V");
generator.returnValue();
generator.endMethod();
}
InsnList removedInstructions = extractCallToSuperConstructor(method);
method.name = new ShadowImpl().directMethodName(ShadowConstants.CONSTRUCTOR_METHOD_NAME);
classNode.methods.add(redirectorMethod(method, ShadowConstants.CONSTRUCTOR_METHOD_NAME));
String[] exceptions = exceptionArray(method);
MethodNode methodNode = new MethodNode(method.access, "<init>", method.desc, method.signature, exceptions);
makeMethodPublic(methodNode);
RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(methodNode);
methodNode.instructions = removedInstructions;
generator.loadThis();
generator.invokeVirtual(classType, new Method(ROBO_INIT_METHOD_NAME, "()V"));
generateShadowCall(method, ShadowConstants.CONSTRUCTOR_METHOD_NAME, generator);
generator.endMethod();
classNode.methods.add(methodNode);
}
private InsnList extractCallToSuperConstructor(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 ALOAD:
VarInsnNode vnode = (VarInsnNode) node;
if (vnode.var == 0) {
startIndex = i;
}
break;
case INVOKESPECIAL:
MethodInsnNode mnode = (MethodInsnNode) node;
if (mnode.owner.equals(internalClassName) || mnode.owner.equals(classNode.superName)) {
assert mnode.name.equals("<init>");
// 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 ATHROW:
ctor.visitCode();
ctor.visitInsn(RETURN);
ctor.visitEnd();
return removedInstructions;
}
}
throw new RuntimeException("huh? " + ctor.name + ctor.desc);
}
//TODO javadocs
private void instrumentNormalMethod(MethodNode method) {
// if not abstract, set a final modifier
if ((method.access & ACC_ABSTRACT) == 0) {
method.access = method.access | ACC_FINAL;
}
// if a native method, remove native modifier and force return a default value
if ((method.access & ACC_NATIVE) != 0) {
method.access = method.access & ~ACC_NATIVE;
RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(method);
Type returnType = generator.getReturnType();
generator.pushDefaultReturnValueToStack(returnType);
generator.returnValue();
}
// todo figure out
String originalName = method.name;
method.name = new ShadowImpl().directMethodName(originalName);
MethodNode delegatorMethodNode = new MethodNode(method.access, originalName, method.desc, method.signature, exceptionArray(method));
delegatorMethodNode.visibleAnnotations = method.visibleAnnotations;
delegatorMethodNode.access &= ~(ACC_NATIVE | ACC_ABSTRACT | ACC_FINAL);
makeMethodPrivate(method);
RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(delegatorMethodNode);
generateShadowCall(method, originalName, generator);
generator.endMethod();
classNode.methods.add(delegatorMethodNode);
}
//todo rename
private MethodNode redirectorMethod(MethodNode method, String newName) {
MethodNode redirector = new MethodNode(ASM4, newName, method.desc, method.signature, exceptionArray(method));
redirector.access = method.access & ~(ACC_NATIVE | ACC_ABSTRACT | ACC_FINAL);
makeMethodPrivate(redirector);
RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(redirector);
generator.invokeMethod(internalClassName, method);
generator.returnValue();
return redirector;
}
private String[] exceptionArray(MethodNode method) {
return ((List<String>) method.exceptions).toArray(new String[method.exceptions.size()]);
}
/**
* Filters methods that might need special treatment because of various reasons
*/
private void filterSpecialMethods(MethodNode callingMethod) {
ListIterator<AbstractInsnNode> instructions = callingMethod.instructions.iterator();
while (instructions.hasNext()) {
AbstractInsnNode node = instructions.next();
switch (node.getOpcode()) {
case NEW:
TypeInsnNode newInsnNode = (TypeInsnNode) node;
newInsnNode.desc = remapType(newInsnNode.desc);
break;
case GETFIELD:
/* falls through */
case PUTFIELD:
/* falls through */
case GETSTATIC:
/* falls through */
case PUTSTATIC:
FieldInsnNode fieldInsnNode = (FieldInsnNode) node;
fieldInsnNode.desc = remapType(fieldInsnNode.desc); // todo test
break;
case INVOKESTATIC:
/* falls through */
case INVOKEINTERFACE:
/* falls through */
case INVOKESPECIAL:
/* falls through */
case INVOKEVIRTUAL:
MethodInsnNode targetMethod = (MethodInsnNode) node;
targetMethod.desc = remapParams(targetMethod.desc);
if (isGregorianCalendarBooleanConstructor(targetMethod)) {
replaceGregorianCalendarBooleanConstructor(instructions, targetMethod);
} else if (shouldIntercept(targetMethod)) {
interceptInvokeVirtualMethod(instructions, targetMethod);
}
break;
case INVOKEDYNAMIC:
/* no unusual behavior */
break;
default:
break;
}
}
}
/**
* Verifies if the @targetMethod is a <init>(boolean) constructor for {@link java.util.GregorianCalendar}
*/
private boolean isGregorianCalendarBooleanConstructor(MethodInsnNode targetMethod) {
return targetMethod.owner.equals("java/util/GregorianCalendar") &&
targetMethod.name.equals("<init>") &&
targetMethod.desc.equals("(Z)V");
}
/**
* Replaces the void <init> (boolean) constructor for a call to the void <init> (int, int, int) one
*/
private 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(POP));
// Add parameters values for calling GregorianCalendar(int, int, int)
instructions.add(new InsnNode(ICONST_0));
instructions.add(new InsnNode(ICONST_0));
instructions.add(new InsnNode(ICONST_0));
// Call GregorianCalendar(int, int, int)
instructions.add(new MethodInsnNode(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+)
*/
abstract protected void interceptInvokeVirtualMethod(ListIterator<AbstractInsnNode> instructions, MethodInsnNode targetMethod);
/**
* Replaces protected and private class modifiers with public
*/
private void makeClassPublic(ClassNode clazz) {
clazz.access = (clazz.access | ACC_PUBLIC) & ~(ACC_PROTECTED | ACC_PRIVATE);
}
/**
* Replaces protected and private method modifiers with public
*/
private void makeMethodPublic(MethodNode method) {
method.access = (method.access | ACC_PUBLIC) & ~(ACC_PROTECTED | ACC_PRIVATE);
}
/**
* Replaces protected and public class modifiers with private
*/
private void makeMethodPrivate(MethodNode method) {
method.access = (method.access | ACC_PRIVATE) & ~(ACC_PUBLIC | ACC_PROTECTED);
}
private MethodNode generateStaticInitializerNotifierMethod() {
MethodNode methodNode = new MethodNode(ACC_STATIC, "<clinit>", "()V", "()V", null);
RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(methodNode);
generator.push(classType);
generator.invokeStatic(Type.getType(RobolectricInternals.class), new Method("classInitializing", "(Ljava/lang/Class;)V"));
generator.returnValue();
generator.endMethod();
return methodNode;
}
// todo javadocs
protected abstract void generateShadowCall(MethodNode originalMethod, String originalMethodName, RobolectricGeneratorAdapter generator);
int getTag(MethodNode m) {
return Modifier.isStatic(m.access) ? H_INVOKESTATIC : H_INVOKESPECIAL;
}
}
/**
* ClassWriter implementation that verifies classes by comparing type information obtained
* from loading the classes as resources. This was taken from the ASM ClassWriter unit tests.
*/
private class InstrumentingClassWriter extends ClassWriter {
/**
* Preserve stack map frames for V51 and newer bytecode. This fixes class verification errors
* for JDK7 and JDK8. The option to disable bytecode verification was removed in JDK8.
*
* Don't bother for V50 and earlier bytecode, because it doesn't contain stack map frames, and
* also because ASM's stack map frame handling doesn't support the JSR and RET instructions
* present in legacy bytecode.
*/
public InstrumentingClassWriter(ClassNode classNode) {
super(classNode.version >= 51 ? ClassWriter.COMPUTE_FRAMES : ClassWriter.COMPUTE_MAXS);
}
@Override
public int newNameType(String name, String desc) {
return super.newNameType(name, desc.charAt(0) == ')' ? remapParams(desc) : remapParamType(desc));
}
@Override
public int newClass(String value) {
value = remapType(value);
return super.newClass(value);
}
@Override
protected String getCommonSuperClass(final String type1, final String type2) {
try {
ClassReader info1 = typeInfo(type1);
ClassReader info2 = typeInfo(type2);
if ((info1.getAccess() & Opcodes.ACC_INTERFACE) != 0) {
if (typeImplements(type2, info2, type1)) {
return type1;
}
if ((info2.getAccess() & Opcodes.ACC_INTERFACE) != 0) {
if (typeImplements(type1, info1, type2)) {
return type2;
}
}
return "java/lang/Object";
}
if ((info2.getAccess() & Opcodes.ACC_INTERFACE) != 0) {
if (typeImplements(type1, info1, type2)) {
return type2;
} else {
return "java/lang/Object";
}
}
StringBuilder b1 = typeAncestors(type1, info1);
StringBuilder b2 = typeAncestors(type2, info2);
String result = "java/lang/Object";
int end1 = b1.length();
int end2 = b2.length();
while (true) {
int start1 = b1.lastIndexOf(";", end1 - 1);
int start2 = b2.lastIndexOf(";", end2 - 1);
if (start1 != -1 && start2 != -1
&& end1 - start1 == end2 - start2) {
String p1 = b1.substring(start1 + 1, end1);
String p2 = b2.substring(start2 + 1, end2);
if (p1.equals(p2)) {
result = p1;
end1 = start1;
end2 = start2;
} else {
return result;
}
} else {
return result;
}
}
} catch (IOException e) {
return "java/lang/Object"; // Handle classes that may be obfuscated
}
}
private StringBuilder typeAncestors(String type, ClassReader info) throws IOException {
StringBuilder b = new StringBuilder();
while (!"java/lang/Object".equals(type)) {
b.append(';').append(type);
type = info.getSuperName();
info = typeInfo(type);
}
return b;
}
private boolean typeImplements(String type, ClassReader info, String itf) throws IOException {
while (!"java/lang/Object".equals(type)) {
String[] itfs = info.getInterfaces();
for (String itf2 : itfs) {
if (itf2.equals(itf)) {
return true;
}
}
for (String itf1 : itfs) {
if (typeImplements(itf1, typeInfo(itf1), itf)) {
return true;
}
}
type = info.getSuperName();
info = typeInfo(type);
}
return false;
}
private ClassReader typeInfo(final String type) throws IOException {
try (InputStream is = getClassBytesAsStreamPreferringLocalUrls(type + ".class")) {
return new ClassReader(is);
}
}
}
/**
* GeneratorAdapter implementation specific to generate code for Robolectric purposes
*/
private static class RobolectricGeneratorAdapter extends GeneratorAdapter {
private final boolean isStatic;
private final String desc;
public RobolectricGeneratorAdapter(MethodNode methodNode) {
super(Opcodes.ASM4, methodNode, methodNode.access, methodNode.name, methodNode.desc);
this.isStatic = Modifier.isStatic(methodNode.access);
this.desc = methodNode.desc;
}
public void loadThisOrNull() {
if (isStatic) {
loadNull();
} else {
loadThis();
}
}
public boolean isStatic() {
return isStatic;
}
public void loadNull() {
visitInsn(ACONST_NULL);
}
public Type getReturnType() {
return Type.getReturnType(desc);
}
/**
* Forces a return of a default value, depending on the method's return type
*
* @param type The method's return type
*/
public void pushDefaultReturnValueToStack(Type type) {
if (type.equals(Type.BOOLEAN_TYPE)) {
push(false);
} else if (type.equals(Type.INT_TYPE) || type.equals(Type.SHORT_TYPE) || type.equals(Type.BYTE_TYPE) || type.equals(Type.CHAR_TYPE)) {
push(0);
} else if (type.equals(Type.LONG_TYPE)) {
push(0L);
} else if (type.equals(Type.FLOAT_TYPE)) {
push(0f);
} else if (type.equals(Type.DOUBLE_TYPE)) {
push(0d);
} else if (type.getSort() == ARRAY || type.getSort() == OBJECT) {
loadNull();
}
}
private void invokeMethod(String internalClassName, MethodNode method) {
invokeMethod(internalClassName, method.name, method.desc);
}
private void invokeMethod(String internalClassName, String methodName, String methodDesc) {
if (isStatic()) {
loadArgs(); // this, [args]
visitMethodInsn(INVOKESTATIC, internalClassName, methodName, methodDesc);
} else {
loadThisOrNull(); // this
loadArgs(); // this, [args]
visitMethodInsn(INVOKESPECIAL, internalClassName, methodName, methodDesc);
}
}
public TryCatch tryStart(Type exceptionType) {
return new TryCatch(this, exceptionType);
}
}
/**
* 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);
}
}
public class OldClassInstrumentor extends SandboxClassLoader.ClassInstrumentor {
private final Type PLAN_TYPE = Type.getType(ClassHandler.Plan.class);
private final Type THROWABLE_TYPE = Type.getType(Throwable.class);
private final Method INITIALIZING_METHOD = new Method("initializing", "(Ljava/lang/Object;)Ljava/lang/Object;");
private final Method METHOD_INVOKED_METHOD = new Method("methodInvoked", "(Ljava/lang/String;ZLjava/lang/Class;)L" + PLAN_TYPE.getInternalName() + ";");
private final Method PLAN_RUN_METHOD = new Method("run", OBJECT_TYPE, new Type[]{OBJECT_TYPE, OBJECT_TYPE, Type.getType(Object[].class)});
private final Method HANDLE_EXCEPTION_METHOD = new Method("cleanStackTrace", THROWABLE_TYPE, new Type[]{THROWABLE_TYPE});
private final String DIRECT_OBJECT_MARKER_TYPE_DESC = Type.getObjectType(DirectObjectMarker.class.getName().replace('.', '/')).getDescriptor();
private final Type ROBOLECTRIC_INTERNALS_TYPE = Type.getType(RobolectricInternals.class);
public OldClassInstrumentor(ClassNode classNode, boolean containsStubs) {
super(classNode, containsStubs);
}
@Override
protected void addDirectCallConstructor() {
MethodNode directCallConstructor = new MethodNode(ACC_PUBLIC,
"<init>", "(" + DIRECT_OBJECT_MARKER_TYPE_DESC + classType.getDescriptor() + ")V", null, null);
RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(directCallConstructor);
generator.loadThis();
if (classNode.superName.equals("java/lang/Object")) {
generator.visitMethodInsn(INVOKESPECIAL, classNode.superName, "<init>", "()V");
} else {
generator.loadArgs();
generator.visitMethodInsn(INVOKESPECIAL, classNode.superName,
"<init>", "(" + DIRECT_OBJECT_MARKER_TYPE_DESC + "L" + classNode.superName + ";)V");
}
generator.loadThis();
generator.loadArg(1);
generator.putField(classType, ShadowConstants.CLASS_HANDLER_DATA_FIELD_NAME, OBJECT_TYPE);
generator.returnValue();
classNode.methods.add(directCallConstructor);
}
@Override
protected void writeCallToInitializing(RobolectricGeneratorAdapter generator) {
generator.invokeStatic(ROBOLECTRIC_INTERNALS_TYPE, INITIALIZING_METHOD);
}
@Override
protected void generateShadowCall(MethodNode originalMethod, String originalMethodName, RobolectricGeneratorAdapter generator) {
generateCallToClassHandler(originalMethod, originalMethodName, generator);
}
//TODO clean up & javadocs
private void generateCallToClassHandler(MethodNode originalMethod, String originalMethodName, RobolectricGeneratorAdapter generator) {
int planLocalVar = generator.newLocal(PLAN_TYPE);
int exceptionLocalVar = generator.newLocal(THROWABLE_TYPE);
Label directCall = new Label();
Label doReturn = new Label();
boolean isNormalInstanceMethod = !generator.isStatic && !originalMethodName.equals(ShadowConstants.CONSTRUCTOR_METHOD_NAME);
// maybe perform proxy call...
if (isNormalInstanceMethod) {
Label notInstanceOfThis = new Label();
generator.loadThis(); // this
generator.getField(classType, ShadowConstants.CLASS_HANDLER_DATA_FIELD_NAME, OBJECT_TYPE); // contents of __robo_data__
generator.instanceOf(classType); // __robo_data__, is instance of same class?
generator.visitJumpInsn(IFEQ, notInstanceOfThis); // jump if no (is not instance)
TryCatch tryCatchForProxyCall = generator.tryStart(THROWABLE_TYPE);
generator.loadThis(); // this
generator.getField(classType, ShadowConstants.CLASS_HANDLER_DATA_FIELD_NAME, OBJECT_TYPE); // contents of __robo_data__
generator.checkCast(classType); // __robo_data__ but cast to my class
generator.loadArgs(); // __robo_data__ instance, [args]
generator.visitMethodInsn(INVOKESPECIAL, internalClassName, originalMethod.name, originalMethod.desc);
tryCatchForProxyCall.end();
generator.returnValue();
// catch(Throwable)
tryCatchForProxyCall.handler();
generator.storeLocal(exceptionLocalVar);
generator.loadLocal(exceptionLocalVar);
generator.invokeStatic(ROBOLECTRIC_INTERNALS_TYPE, HANDLE_EXCEPTION_METHOD);
generator.throwException();
// callClassHandler...
generator.mark(notInstanceOfThis);
}
// prepare for call to classHandler.methodInvoked(String signature, boolean isStatic)
generator.push(classType.getInternalName() + "/" + originalMethodName + originalMethod.desc);
generator.push(generator.isStatic());
generator.push(classType); // my class
generator.invokeStatic(ROBOLECTRIC_INTERNALS_TYPE, METHOD_INVOKED_METHOD);
generator.storeLocal(planLocalVar);
generator.loadLocal(planLocalVar); // plan
generator.ifNull(directCall);
// prepare for call to plan.run(Object instance, Object[] params)
TryCatch tryCatchForHandler = generator.tryStart(THROWABLE_TYPE);
generator.loadLocal(planLocalVar); // plan
generator.loadThisOrNull(); // instance
if (generator.isStatic()) { // roboData
generator.loadNull();
} else {
generator.loadThis();
generator.invokeVirtual(classType, new Method(ShadowConstants.GET_ROBO_DATA_METHOD_NAME, GET_ROBO_DATA_SIGNATURE));
}
generator.loadArgArray(); // params
generator.invokeInterface(PLAN_TYPE, PLAN_RUN_METHOD);
Type returnType = generator.getReturnType();
int sort = returnType.getSort();
switch (sort) {
case VOID:
generator.pop();
break;
case OBJECT:
/* falls through */
case ARRAY:
generator.checkCast(returnType);
break;
default:
int unboxLocalVar = generator.newLocal(OBJECT_TYPE);
generator.storeLocal(unboxLocalVar);
generator.loadLocal(unboxLocalVar);
Label notNull = generator.newLabel();
Label afterward = generator.newLabel();
generator.ifNonNull(notNull);
generator.pushDefaultReturnValueToStack(returnType); // return zero, false, whatever
generator.goTo(afterward);
generator.mark(notNull);
generator.loadLocal(unboxLocalVar);
generator.unbox(returnType);
generator.mark(afterward);
break;
}
tryCatchForHandler.end();
generator.goTo(doReturn);
// catch(Throwable)
tryCatchForHandler.handler();
generator.storeLocal(exceptionLocalVar);
generator.loadLocal(exceptionLocalVar);
generator.invokeStatic(ROBOLECTRIC_INTERNALS_TYPE, HANDLE_EXCEPTION_METHOD);
generator.throwException();
if (!originalMethod.name.equals("<init>")) {
generator.mark(directCall);
TryCatch tryCatchForDirect = generator.tryStart(THROWABLE_TYPE);
generator.invokeMethod(classType.getInternalName(), originalMethod.name, originalMethod.desc);
tryCatchForDirect.end();
generator.returnValue();
// catch(Throwable)
tryCatchForDirect.handler();
generator.storeLocal(exceptionLocalVar);
generator.loadLocal(exceptionLocalVar);
generator.invokeStatic(ROBOLECTRIC_INTERNALS_TYPE, HANDLE_EXCEPTION_METHOD);
generator.throwException();
}
generator.mark(doReturn);
generator.returnValue();
}
/**
* 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+)
*/
@Override
protected void interceptInvokeVirtualMethod(ListIterator<AbstractInsnNode> instructions, MethodInsnNode targetMethod) {
interceptInvokeVirtualMethodWithoutInvokeDynamic(instructions, targetMethod);
}
/**
* Intercepts the method without using the invokedynamic bytecode instruction.
* Should be called through interceptInvokeVirtualMethod, not directly
*/
private void interceptInvokeVirtualMethodWithoutInvokeDynamic(ListIterator<AbstractInsnNode> instructions, MethodInsnNode targetMethod) {
boolean isStatic = targetMethod.getOpcode() == INVOKESTATIC;
instructions.remove(); // remove the method invocation
Type[] argumentTypes = Type.getArgumentTypes(targetMethod.desc);
instructions.add(new LdcInsnNode(argumentTypes.length));
instructions.add(new TypeInsnNode(ANEWARRAY, "java/lang/Object"));
// first, move any arguments into an Object[] in reverse order
for (int i = argumentTypes.length - 1; i >= 0; i--) {
Type type = argumentTypes[i];
int argWidth = type.getSize();
if (argWidth == 1) { // A B C []
instructions.add(new InsnNode(DUP_X1)); // A B [] C []
instructions.add(new InsnNode(SWAP)); // A B [] [] C
instructions.add(new LdcInsnNode(i)); // A B [] [] C 2
instructions.add(new InsnNode(SWAP)); // A B [] [] 2 C
box(type, instructions); // A B [] [] 2 (C)
instructions.add(new InsnNode(AASTORE)); // A B [(C)]
} else if (argWidth == 2) { // A B _C_ []
instructions.add(new InsnNode(DUP_X2)); // A B [] _C_ []
instructions.add(new InsnNode(DUP_X2)); // A B [] [] _C_ []
instructions.add(new InsnNode(POP)); // A B [] [] _C_
box(type, instructions); // A B [] [] (C)
instructions.add(new LdcInsnNode(i)); // A B [] [] (C) 2
instructions.add(new InsnNode(SWAP)); // A B [] [] 2 (C)
instructions.add(new InsnNode(AASTORE)); // A B [(C)]
}
}
if (isStatic) { // []
instructions.add(new InsnNode(Opcodes.ACONST_NULL)); // [] null
instructions.add(new InsnNode(Opcodes.SWAP)); // null []
}
// instance []
instructions.add(new LdcInsnNode(targetMethod.owner + "/" + targetMethod.name + targetMethod.desc)); // target method signature
// instance [] signature
instructions.add(new InsnNode(DUP_X2)); // signature instance [] signature
instructions.add(new InsnNode(POP)); // signature instance []
instructions.add(new LdcInsnNode(classType)); // signature instance [] class
instructions.add(new MethodInsnNode(INVOKESTATIC,
Type.getType(RobolectricInternals.class).getInternalName(), "intercept",
"(Ljava/lang/String;Ljava/lang/Object;[Ljava/lang/Object;Ljava/lang/Class;)Ljava/lang/Object;"));
final Type returnType = Type.getReturnType(targetMethod.desc);
switch (returnType.getSort()) {
case ARRAY:
/* falls through */
case OBJECT:
instructions.add(new TypeInsnNode(CHECKCAST, remapType(returnType.getInternalName())));
break;
case VOID:
instructions.add(new InsnNode(POP));
break;
case Type.LONG:
instructions.add(new TypeInsnNode(CHECKCAST, Type.getInternalName(Long.class)));
instructions.add(new MethodInsnNode(INVOKEVIRTUAL, Type.getInternalName(Long.class), "longValue", Type.getMethodDescriptor(Type.LONG_TYPE), false));
break;
case Type.FLOAT:
instructions.add(new TypeInsnNode(CHECKCAST, Type.getInternalName(Float.class)));
instructions.add(new MethodInsnNode(INVOKEVIRTUAL, Type.getInternalName(Float.class), "floatValue", Type.getMethodDescriptor(Type.FLOAT_TYPE), false));
break;
case Type.DOUBLE:
instructions.add(new TypeInsnNode(CHECKCAST, Type.getInternalName(Double.class)));
instructions.add(new MethodInsnNode(INVOKEVIRTUAL, Type.getInternalName(Double.class), "doubleValue", Type.getMethodDescriptor(Type.DOUBLE_TYPE), false));
break;
case Type.BOOLEAN:
instructions.add(new TypeInsnNode(CHECKCAST, Type.getInternalName(Boolean.class)));
instructions.add(new MethodInsnNode(INVOKEVIRTUAL, Type.getInternalName(Boolean.class), "booleanValue", Type.getMethodDescriptor(Type.BOOLEAN_TYPE), false));
break;
case Type.INT:
instructions.add(new TypeInsnNode(CHECKCAST, Type.getInternalName(Integer.class)));
instructions.add(new MethodInsnNode(INVOKEVIRTUAL, Type.getInternalName(Integer.class), "intValue", Type.getMethodDescriptor(Type.INT_TYPE), false));
break;
case Type.SHORT:
instructions.add(new TypeInsnNode(CHECKCAST, Type.getInternalName(Short.class)));
instructions.add(new MethodInsnNode(INVOKEVIRTUAL, Type.getInternalName(Short.class), "shortValue", Type.getMethodDescriptor(Type.SHORT_TYPE), false));
break;
case Type.BYTE:
instructions.add(new TypeInsnNode(CHECKCAST, Type.getInternalName(Byte.class)));
instructions.add(new MethodInsnNode(INVOKEVIRTUAL, Type.getInternalName(Byte.class), "byteValue", Type.getMethodDescriptor(Type.BYTE_TYPE), false));
break;
default:
throw new RuntimeException("Not implemented: " + getClass().getName() + " cannot intercept methods with return type " + returnType.getClassName());
}
}
}
public class InvokeDynamicClassInstrumentor extends SandboxClassLoader.ClassInstrumentor {
private final Handle BOOTSTRAP_INIT;
private final Handle BOOTSTRAP;
private final Handle BOOTSTRAP_STATIC;
private final Handle BOOTSTRAP_INTRINSIC;
public InvokeDynamicClassInstrumentor(ClassNode classNode, boolean containsStubs) {
super(classNode, containsStubs);
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(H_INVOKESTATIC, className, "bootstrapInit", bootstrap.toMethodDescriptorString());
BOOTSTRAP = new Handle(H_INVOKESTATIC, className, "bootstrap", bootstrapMethod);
BOOTSTRAP_STATIC = new Handle(H_INVOKESTATIC, className, "bootstrapStatic", bootstrapMethod);
BOOTSTRAP_INTRINSIC = new Handle(H_INVOKESTATIC, className, "bootstrapIntrinsic", bootstrapIntrinsic);
}
@Override
protected void addDirectCallConstructor() {
// not needed, for reasons.
}
@Override
protected void writeCallToInitializing(RobolectricGeneratorAdapter generator) {
generator.invokeDynamic("initializing", Type.getMethodDescriptor(OBJECT_TYPE, classType), BOOTSTRAP_INIT);
}
@Override
protected void generateShadowCall(MethodNode originalMethod, String originalMethodName, RobolectricGeneratorAdapter generator) {
generateInvokeDynamic(originalMethod, originalMethodName, generator);
}
// todo javadocs
private void generateInvokeDynamic(MethodNode originalMethod, String originalMethodName, RobolectricGeneratorAdapter generator) {
Handle original =
new Handle(getTag(originalMethod), classType.getInternalName(), originalMethod.name,
originalMethod.desc);
if (generator.isStatic()) {
generator.loadArgs();
generator.invokeDynamic(originalMethodName, originalMethod.desc, BOOTSTRAP_STATIC, original);
} else {
String desc = "(" + classType.getDescriptor() + originalMethod.desc.substring(1);
generator.loadThis();
generator.loadArgs();
generator.invokeDynamic(originalMethodName, desc, BOOTSTRAP, original);
}
generator.returnValue();
}
@Override
protected void interceptInvokeVirtualMethod(ListIterator<AbstractInsnNode> instructions, MethodInsnNode targetMethod) {
interceptInvokeVirtualMethodWithInvokeDynamic(instructions, targetMethod);
}
/**
* Intercepts the method using the invokedynamic bytecode instruction available in Java 7+.
* Should be called through interceptInvokeVirtualMethod, not directly
*/
private void interceptInvokeVirtualMethodWithInvokeDynamic(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() != INVOKESTATIC) {
String thisType = type.getDescriptor();
description = "(" + thisType + description.substring(1, description.length());
}
instructions.add(new InvokeDynamicInsnNode(targetMethod.name, description, BOOTSTRAP_INTRINSIC, owner));
}
}
}