// Copyright © 2013-2017 Esko Luontola and other Retrolambda contributors
// This software is released under the Apache License 2.0.
// The license text is at http://www.apache.org/licenses/LICENSE-2.0
package net.orfjackal.retrolambda.lambdas;
import org.objectweb.asm.*;
import org.objectweb.asm.tree.*;
import static org.objectweb.asm.Opcodes.*;
public class BackportLambdaClass extends ClassVisitor {
private static final String SINGLETON_FIELD_NAME = "instance";
private static final String JAVA_LANG_OBJECT = "java/lang/Object";
private String lambdaClass;
private Type constructor;
private Handle implMethod;
private Handle accessMethod;
private LambdaFactoryMethod factoryMethod;
public BackportLambdaClass(ClassVisitor next) {
super(ASM5, next);
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
lambdaClass = name;
LambdaReifier.setLambdaClass(lambdaClass);
implMethod = LambdaReifier.getLambdaImplMethod();
accessMethod = LambdaReifier.getLambdaAccessMethod();
factoryMethod = LambdaReifier.getLambdaFactoryMethod();
if (superName.equals(LambdaNaming.MAGIC_LAMBDA_IMPL)) {
superName = JAVA_LANG_OBJECT;
}
super.visit(version, access, name, signature, superName, interfaces);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
if (name.equals("<init>")) {
constructor = Type.getMethodType(desc);
}
if (LambdaNaming.isSerializationHook(access, name, desc)) {
return null; // remove serialization hooks; we serialize lambda instances as-is
}
if (LambdaNaming.isPlatformFactoryMethod(access, name, desc, factoryMethod.getDesc())) {
return null; // remove the JVM's factory method which will not be unused
}
MethodVisitor next = super.visitMethod(access, name, desc, signature, exceptions);
next = new RemoveLambdaFormHiddenAnnotation(next);
next = new RemoveMagicLambdaConstructorCall(next);
next = new CallPrivateImplMethodsViaAccessMethods(access, name, desc, signature, exceptions, next);
return next;
}
@Override
public void visitEnd() {
if (isStateless()) {
makeSingleton();
}
generateFactoryMethod();
super.visitEnd();
}
private void makeSingleton() {
FieldVisitor fv = super.visitField(ACC_PRIVATE | ACC_STATIC | ACC_FINAL,
SINGLETON_FIELD_NAME, singletonFieldDesc(), null, null);
fv.visitEnd();
MethodVisitor mv = super.visitMethod(ACC_STATIC, "<clinit>", "()V", null, null);
mv.visitCode();
mv.visitTypeInsn(NEW, lambdaClass);
mv.visitInsn(DUP);
mv.visitMethodInsn(INVOKESPECIAL, lambdaClass, "<init>", "()V", false);
mv.visitFieldInsn(PUTSTATIC, lambdaClass, SINGLETON_FIELD_NAME, singletonFieldDesc());
mv.visitInsn(RETURN);
mv.visitMaxs(-1, -1); // rely on ClassWriter.COMPUTE_MAXS
mv.visitEnd();
}
private void generateFactoryMethod() {
MethodVisitor mv = cv.visitMethod(ACC_PUBLIC | ACC_STATIC,
factoryMethod.getName(), factoryMethod.getDesc(), null, null);
mv.visitCode();
if (isStateless()) {
mv.visitFieldInsn(GETSTATIC, lambdaClass, SINGLETON_FIELD_NAME, singletonFieldDesc());
mv.visitInsn(ARETURN);
} else {
mv.visitTypeInsn(NEW, lambdaClass);
mv.visitInsn(DUP);
int varIndex = 0;
for (Type type : constructor.getArgumentTypes()) {
mv.visitVarInsn(type.getOpcode(ILOAD), varIndex);
varIndex += type.getSize();
}
mv.visitMethodInsn(INVOKESPECIAL, lambdaClass, "<init>", constructor.getDescriptor(), false);
mv.visitInsn(ARETURN);
}
mv.visitMaxs(-1, -1); // rely on ClassWriter.COMPUTE_MAXS
mv.visitEnd();
}
private String singletonFieldDesc() {
return "L" + lambdaClass + ";";
}
private boolean isStateless() {
return constructor.getArgumentTypes().length == 0;
}
private static class RemoveMagicLambdaConstructorCall extends MethodVisitor {
public RemoveMagicLambdaConstructorCall(MethodVisitor next) {
super(ASM5, next);
}
@Override
public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
if (opcode == INVOKESPECIAL
&& owner.equals(LambdaNaming.MAGIC_LAMBDA_IMPL)
&& name.equals("<init>")
&& desc.equals("()V")) {
owner = JAVA_LANG_OBJECT;
}
super.visitMethodInsn(opcode, owner, name, desc, itf);
}
}
private class CallPrivateImplMethodsViaAccessMethods extends MethodNode {
private final MethodVisitor next;
public CallPrivateImplMethodsViaAccessMethods(int access, String name, String desc, String signature,
String[] exceptions, MethodVisitor next) {
super(ASM5, access, name, desc, signature, exceptions);
this.next = next;
}
@Override
public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
// Java 8's lambda classes get away with calling private virtual methods
// by using invokespecial because the JVM relaxes the bytecode validation
// of the lambda classes it generates. We must however call them through
// a non-private access method which we have generated.
if (owner.equals(implMethod.getOwner())
&& name.equals(implMethod.getName())
&& desc.equals(implMethod.getDesc())) {
if (implMethod.getTag() == H_NEWINVOKESPECIAL
&& accessMethod.getTag() == H_INVOKESTATIC) {
// The impl is a private constructor which is called through an access method.
// The current method already did NEW an instance, but we won't use it because
// the access method will also instantiate it. The JVM would be OK with a non-empty
// stack on ARETURN, but it causes a VerifyError on Android, so here we remove the
// unused instance from the stack.
boolean found = false;
for (int i = instructions.size() - 1; i >= 1; i--) {
AbstractInsnNode maybeNew = instructions.get(i - 1);
AbstractInsnNode maybeDup = instructions.get(i);
if (maybeNew.getOpcode() == NEW && maybeDup.getOpcode() == DUP) {
instructions.remove(maybeNew);
instructions.remove(maybeDup);
found = true;
break;
}
}
if (!found) {
throw new IllegalStateException(
"Expected to find NEW, DUP instructions preceding NEWINVOKESPECIAL. Please file this as a bug.");
}
}
super.visitMethodInsn(
Handles.getOpcode(accessMethod),
accessMethod.getOwner(),
accessMethod.getName(),
accessMethod.getDesc(),
accessMethod.getTag() == H_INVOKEINTERFACE);
} else {
super.visitMethodInsn(opcode, owner, name, desc, itf);
}
}
@Override
public void visitEnd() {
accept(next);
}
}
}