// Copyright 2016 The Bazel Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package com.google.devtools.build.android.desugar; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import java.util.HashSet; import java.util.LinkedHashSet; import org.objectweb.asm.AnnotationVisitor; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.FieldVisitor; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.MethodNode; import org.objectweb.asm.tree.TypeInsnNode; /** * Visitor intended to fix up lambda classes to match assumptions made in {@link LambdaDesugaring}. * Specifically this includes fixing visibilities and generating any missing factory methods. * * <p>Each instance can only visit one class. This is because the signature of the needed factory * method is passed into the constructor. */ class LambdaClassFixer extends ClassVisitor { /** Magic method name used by {@link java.lang.invoke.LambdaMetafactory}. */ public static final String FACTORY_METHOD_NAME = "get$Lambda"; /** Field name we'll use to hold singleton instances where possible. */ public static final String SINGLETON_FIELD_NAME = "$instance"; private final LambdaInfo lambdaInfo; private final ClassReaderFactory factory; private final ImmutableSet<String> interfaceLambdaMethods; private final boolean allowDefaultMethods; private final boolean copyBridgeMethods; private final HashSet<String> implementedMethods = new HashSet<>(); private final LinkedHashSet<String> methodsToMoveIn = new LinkedHashSet<>(); private String originalInternalName; private ImmutableList<String> interfaces; private boolean hasState; private boolean hasFactory; private String desc; private String signature; public LambdaClassFixer(ClassVisitor dest, LambdaInfo lambdaInfo, ClassReaderFactory factory, ImmutableSet<String> interfaceLambdaMethods, boolean allowDefaultMethods, boolean copyBridgeMethods) { super(Opcodes.ASM5, dest); checkArgument(!allowDefaultMethods || interfaceLambdaMethods.isEmpty()); checkArgument(allowDefaultMethods || copyBridgeMethods); this.lambdaInfo = lambdaInfo; this.factory = factory; this.interfaceLambdaMethods = interfaceLambdaMethods; this.allowDefaultMethods = allowDefaultMethods; this.copyBridgeMethods = copyBridgeMethods; } @Override public void visit( int version, int access, String name, String signature, String superName, String[] interfaces) { checkArgument(BitFlags.noneSet(access, Opcodes.ACC_INTERFACE), "Not a class: %s", name); checkState(this.originalInternalName == null, "not intended for reuse but reused for %s", name); originalInternalName = name; hasState = false; hasFactory = false; desc = null; this.signature = null; this.interfaces = ImmutableList.copyOf(interfaces); // Rename to desired name super.visit(version, access, getInternalName(), signature, superName, interfaces); } @Override public FieldVisitor visitField( int access, String name, String desc, String signature, Object value) { hasState = true; return super.visitField(access, name, desc, signature, value); } @Override public MethodVisitor visitMethod( int access, String name, String desc, String signature, String[] exceptions) { if (name.equals("writeReplace") && BitFlags.noneSet(access, Opcodes.ACC_STATIC) && desc.equals("()Ljava/lang/Object;")) { // Lambda serialization hooks use java/lang/invoke/SerializedLambda, which isn't available on // Android. Since Jack doesn't do anything special for serializable lambdas we just drop these // serialization hooks. // https://docs.oracle.com/javase/8/docs/platform/serialization/spec/output.html#a5324 gives // details on the role and signature of this method. return null; } if (BitFlags.noneSet(access, Opcodes.ACC_ABSTRACT | Opcodes.ACC_STATIC)) { // Keep track of instance methods implemented in this class for later. Since this visitor // is intended for lambda classes, no need to look at the superclass. implementedMethods.add(name + ":" + desc); } if (FACTORY_METHOD_NAME.equals(name)) { hasFactory = true; if (!lambdaInfo.needFactory()) { return null; // drop generated factory method if we won't call it } access &= ~Opcodes.ACC_PRIVATE; // make factory method accessible } else if ("<init>".equals(name)) { this.desc = desc; this.signature = signature; if (!lambdaInfo.needFactory() && !desc.startsWith("()")) { access &= ~Opcodes.ACC_PRIVATE; // make constructor accessible if we'll call it directly } } MethodVisitor methodVisitor = new LambdaClassMethodRewriter(super.visitMethod(access, name, desc, signature, exceptions)); if (!lambdaInfo.bridgeMethod().equals(lambdaInfo.methodReference())) { // Skip UseBridgeMethod unless we actually need it methodVisitor = new UseBridgeMethod(methodVisitor, lambdaInfo, access, name, desc, signature, exceptions); } if (!FACTORY_METHOD_NAME.equals(name) && !"<init>".equals(name)) { methodVisitor = new LambdaClassInvokeSpecialRewriter(methodVisitor); } return methodVisitor; } @Override public void visitEnd() { checkState(!hasState || hasFactory, "Expected factory method for capturing lambda %s", getInternalName()); if (!hasState) { checkState(signature == null, "Didn't expect generic constructor signature %s %s", getInternalName(), signature); checkState(lambdaInfo.factoryMethodDesc().startsWith("()"), "Expected 0-arg factory method for %s but found %s", getInternalName(), lambdaInfo.factoryMethodDesc()); // Since this is a stateless class we populate and use a static singleton field "$instance". // Field is package-private so we can read it from the class that had the invokedynamic. String singletonFieldDesc = lambdaInfo.factoryMethodDesc().substring("()".length()); super.visitField( Opcodes.ACC_STATIC | Opcodes.ACC_FINAL, SINGLETON_FIELD_NAME, singletonFieldDesc, (String) null, (Object) null) .visitEnd(); MethodVisitor codeBuilder = super.visitMethod( Opcodes.ACC_STATIC, "<clinit>", "()V", (String) null, new String[0]); codeBuilder.visitTypeInsn(Opcodes.NEW, getInternalName()); codeBuilder.visitInsn(Opcodes.DUP); codeBuilder.visitMethodInsn(Opcodes.INVOKESPECIAL, getInternalName(), "<init>", checkNotNull(desc, "didn't see a constructor for %s", getInternalName()), /*itf*/ false); codeBuilder.visitFieldInsn( Opcodes.PUTSTATIC, getInternalName(), SINGLETON_FIELD_NAME, singletonFieldDesc); codeBuilder.visitInsn(Opcodes.RETURN); codeBuilder.visitMaxs(2, 0); // two values are pushed onto the stack codeBuilder.visitEnd(); } copyRewrittenLambdaMethods(); if (copyBridgeMethods) { copyBridgeMethods(interfaces); } super.visitEnd(); } private String getInternalName() { return lambdaInfo.desiredInternalName(); } private void copyRewrittenLambdaMethods() { for (String rewritten : methodsToMoveIn) { String interfaceInternalName = rewritten.substring(0, rewritten.indexOf('#')); String methodName = rewritten.substring(interfaceInternalName.length() + 1); ClassReader bytecode = checkNotNull(factory.readIfKnown(interfaceInternalName), "Couldn't load interface with lambda method %s", rewritten); CopyOneMethod copier = new CopyOneMethod(methodName); // TODO(kmb): Set source file attribute for lambda classes so lambda debug info makes sense bytecode.accept(copier, ClassReader.SKIP_DEBUG); checkState(copier.copied(), "Didn't find %s", rewritten); } } private void copyBridgeMethods(ImmutableList<String> interfaces) { for (String implemented : interfaces) { ClassReader bytecode = factory.readIfKnown(implemented); if (bytecode != null) { // Don't copy line numbers and local variable tables. They would be misleading or wrong // and other methods in generated lambda classes don't have debug info either. bytecode.accept(new CopyBridgeMethods(), ClassReader.SKIP_DEBUG); } // else the interface is defined in a different Jar, which we can ignore here } } /** * Rewriter for methods in generated lambda classes. */ private class LambdaClassMethodRewriter extends MethodVisitor { public LambdaClassMethodRewriter(MethodVisitor dest) { super(Opcodes.ASM5, dest); } @Override public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) { String method = owner + "#" + name; if (interfaceLambdaMethods.contains(method)) { // Rewrite invocations of lambda methods in interfaces to anticipate the lambda method being // moved into the lambda class (i.e., the class being visited here). checkArgument(opcode == Opcodes.INVOKESTATIC, "Cannot move instance method %s", method); owner = getInternalName(); itf = false; // owner was interface but is now a class methodsToMoveIn.add(method); } else if (originalInternalName.equals(owner)) { // Reflect renaming of lambda classes owner = getInternalName(); } if (name.startsWith("lambda$")) { // Reflect renaming of lambda$ instance methods in LambdaDesugaring. Do this even if we'll // move the method into the lambda class we're processing so the renaming done in // LambdaDesugaring doesn't kick in if the class were desugared a second time. name = LambdaDesugaring.uniqueInPackage(owner, name); } super.visitMethodInsn(opcode, owner, name, desc, itf); } @Override public void visitTypeInsn(int opcode, String type) { if (originalInternalName.equals(type)) { // Reflect renaming of lambda classes type = getInternalName(); } super.visitTypeInsn(opcode, type); } @Override public void visitFieldInsn(int opcode, String owner, String name, String desc) { if (originalInternalName.equals(owner)) { // Reflect renaming of lambda classes owner = getInternalName(); } super.visitFieldInsn(opcode, owner, name, desc); } @Override public AnnotationVisitor visitAnnotation(String desc, boolean visible) { // Drop annotation that's part of the generated lambda class that's not available on Android. // Proguard complains about this otherwise. if ("Ljava/lang/invoke/LambdaForm$Hidden;".equals(desc)) { return null; } return super.visitAnnotation(desc, visible); } } /** Rewriter for invokespecial in generated lambda classes. */ private static class LambdaClassInvokeSpecialRewriter extends MethodVisitor { public LambdaClassInvokeSpecialRewriter(MethodVisitor dest) { super(Opcodes.ASM5, dest); } @Override public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) { if (opcode == Opcodes.INVOKESPECIAL && name.startsWith("lambda$")) { opcode = itf ? Opcodes.INVOKEINTERFACE : Opcodes.INVOKEVIRTUAL; } super.visitMethodInsn(opcode, owner, name, desc, itf); } } /** * Visitor that copies bridge methods from the visited interface into the class visited by the * surrounding {@link LambdaClassFixer}. Descends recursively into interfaces extended by the * visited interface. */ private class CopyBridgeMethods extends ClassVisitor { @SuppressWarnings("hiding") private ImmutableList<String> interfaces; public CopyBridgeMethods() { // No delegate visitor; instead we'll add methods to the outer class's delegate where needed super(Opcodes.ASM5); } @Override public void visit( int version, int access, String name, String signature, String superName, String[] interfaces) { checkArgument(BitFlags.isSet(access, Opcodes.ACC_INTERFACE)); checkState(this.interfaces == null); this.interfaces = ImmutableList.copyOf(interfaces); } @Override public MethodVisitor visitMethod( int access, String name, String desc, String signature, String[] exceptions) { if ((access & (Opcodes.ACC_BRIDGE | Opcodes.ACC_ABSTRACT | Opcodes.ACC_STATIC)) == Opcodes.ACC_BRIDGE) { // Only copy bridge methods--hand-written default methods are not supported--and only if // we haven't seen the method already. if (implementedMethods.add(name + ":" + desc)) { MethodVisitor result = LambdaClassFixer.super.visitMethod(access, name, desc, signature, exceptions); return allowDefaultMethods ? result : new AvoidJacocoInit(result); } } return null; } @Override public void visitEnd() { copyBridgeMethods(this.interfaces); } } private class CopyOneMethod extends ClassVisitor { private final String methodName; private int copied = 0; public CopyOneMethod(String methodName) { // No delegate visitor; instead we'll add methods to the outer class's delegate where needed super(Opcodes.ASM5); checkState(!allowDefaultMethods, "Couldn't copy interface lambda bodies"); this.methodName = methodName; } public boolean copied() { return copied > 0; } @Override public void visit( int version, int access, String name, String signature, String superName, String[] interfaces) { checkArgument(BitFlags.isSet(access, Opcodes.ACC_INTERFACE)); } @Override public MethodVisitor visitMethod( int access, String name, String desc, String signature, String[] exceptions) { if (name.equals(methodName)) { checkState(copied == 0, "Found unexpected second method %s with descriptor %s", name, desc); ++copied; // Rename for consistency with what we do in LambdaClassMethodRewriter name = LambdaDesugaring.uniqueInPackage(getInternalName(), name); return new AvoidJacocoInit( LambdaClassFixer.super.visitMethod(access, name, desc, signature, exceptions)); } return null; } } /** * Method visitor that rewrites {@code $jacocoInit()} calls to equivalent field accesses. * * <p>This class should only be used to visit interface methods and assumes that the code in * {@code $jacocoInit()} is always executed in the interface's static initializer, which is the * case in the absence of hand-written static or default interface methods (which * {@link Java7Compatibility} makes sure of). */ private static class AvoidJacocoInit extends MethodVisitor { public AvoidJacocoInit(MethodVisitor dest) { super(Opcodes.ASM5, dest); } @Override public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) { if (opcode == Opcodes.INVOKESTATIC && "$jacocoInit".equals(name)) { // Rewrite $jacocoInit() calls to just read the $jacocoData field super.visitFieldInsn(Opcodes.GETSTATIC, owner, "$jacocoData", "[Z"); } else { super.visitMethodInsn(opcode, owner, name, desc, itf); } } } private static class UseBridgeMethod extends MethodNode { private final MethodVisitor dest; private final LambdaInfo lambdaInfo; public UseBridgeMethod(MethodVisitor dest, LambdaInfo lambdaInfo, int access, String name, String desc, String signature, String[] exceptions) { super(Opcodes.ASM5, access, name, desc, signature, exceptions); this.dest = dest; this.lambdaInfo = lambdaInfo; } @Override public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) { if (owner.equals(lambdaInfo.methodReference().getOwner()) && name.equals(lambdaInfo.methodReference().getName()) && desc.equals(lambdaInfo.methodReference().getDesc())) { if (lambdaInfo.methodReference().getTag() == Opcodes.H_NEWINVOKESPECIAL && lambdaInfo.bridgeMethod().getTag() != Opcodes.H_NEWINVOKESPECIAL) { // We're changing a constructor call to a factory method call, so we unfortunately need // to go find the NEW/DUP pair preceding the constructor call and remove it removeLastAllocation(); } super.visitMethodInsn( LambdaDesugaring.invokeOpcode(lambdaInfo.bridgeMethod()), lambdaInfo.bridgeMethod().getOwner(), lambdaInfo.bridgeMethod().getName(), lambdaInfo.bridgeMethod().getDesc(), lambdaInfo.bridgeMethod().isInterface()); } else { super.visitMethodInsn(opcode, owner, name, desc, itf); } } private void removeLastAllocation() { AbstractInsnNode insn = instructions.getLast(); while (insn != null && insn.getPrevious() != null) { AbstractInsnNode prev = insn.getPrevious(); if (prev.getOpcode() == Opcodes.NEW && insn.getOpcode() == Opcodes.DUP && ((TypeInsnNode) prev).desc.equals(lambdaInfo.methodReference().getOwner())) { instructions.remove(prev); instructions.remove(insn); return; } insn = prev; } throw new IllegalStateException( "Couldn't find allocation to rewrite ::new reference " + lambdaInfo.methodReference()); } @Override public void visitEnd() { accept(dest); } } }