// Copyright 2017 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.devtools.build.android.desugar.io.BitFlags; import com.google.devtools.build.android.desugar.io.FieldInfo; import java.lang.reflect.Method; import javax.annotation.Nullable; import org.objectweb.asm.AnnotationVisitor; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.FieldVisitor; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; import org.objectweb.asm.TypePath; /** * Visitor that moves methods with bodies from interfaces into a companion class and rewrites call * sites accordingly (which is only needed for static interface methods). Default methods are kept * as abstract methods with all their annotations. * *

Any necessary companion classes will be added to the given {@link GeneratedClassStore}. It's * the caller's responsibility to write those out. * *

Relies on {@link DefaultMethodClassFixer} to stub in method bodies for moved default methods. * Assumes that lambdas are already desugared. Ignores bridge methods, which are handled specially. */ class InterfaceDesugaring extends ClassVisitor { static final String COMPANION_METHOD_TO_TRIGGER_INTERFACE_CLINIT_NAME = "$$triggerInterfaceInit"; static final String COMPANION_METHOD_TO_TRIGGER_INTERFACE_CLINIT_DESC = "()V"; static final String INTERFACE_STATIC_COMPANION_METHOD_SUFFIX = "$$STATIC$$"; private final ClassVsInterface interfaceCache; private final DependencyCollector depsCollector; private final CoreLibrarySupport coreLibrarySupport; private final ClassReaderFactory bootclasspath; private final ClassLoader targetLoader; private final GeneratedClassStore store; private final boolean legacyJaCoCo; private String internalName; private int bytecodeVersion; private int accessFlags; private int numberOfDefaultMethods; @Nullable private ClassVisitor companion; @Nullable private FieldInfo interfaceFieldToAccessInCompanionMethodToTriggerInterfaceClinit; public InterfaceDesugaring( ClassVisitor dest, ClassVsInterface interfaceCache, DependencyCollector depsCollector, @Nullable CoreLibrarySupport coreLibrarySupport, ClassReaderFactory bootclasspath, ClassLoader targetLoader, GeneratedClassStore store, boolean legacyJaCoCo) { super(Opcodes.ASM6, dest); this.interfaceCache = interfaceCache; this.depsCollector = depsCollector; this.coreLibrarySupport = coreLibrarySupport; this.bootclasspath = bootclasspath; this.targetLoader = targetLoader; this.store = store; this.legacyJaCoCo = legacyJaCoCo; } @Override public void visit( int version, int access, String name, String signature, String superName, String[] interfaces) { companion = null; numberOfDefaultMethods = 0; internalName = name; bytecodeVersion = version; accessFlags = access; if (isInterface()) { interfaceCache.addKnownInterfaces(name); // Record interface hierarchy. This helps avoid parsing .class files when double-checking // desugaring results later using collected dependency information. depsCollector.recordExtendedInterfaces(name, interfaces); } else { interfaceCache.addKnownClass(name); } interfaceCache.addKnownClass(superName).addKnownInterfaces(interfaces); super.visit(version, access, name, signature, superName, interfaces); } @Override public void visitEnd() { if (companion != null) { // Record classes with default methods. This increases precision when double-checking // desugaring results later, without parsing .class files again, compared to just looking // for companion classes in a given desugared Jar which may only contain static methods. depsCollector.recordDefaultMethods(internalName, numberOfDefaultMethods); // Emit a method to access the fields of the interfaces that need initialization. emitInterfaceFieldAccessInCompanionMethodToTriggerInterfaceClinit(); companion.visitEnd(); } super.visitEnd(); } private void emitInterfaceFieldAccessInCompanionMethodToTriggerInterfaceClinit() { if (companion == null || interfaceFieldToAccessInCompanionMethodToTriggerInterfaceClinit == null) { return; } // Create a method to access the interface fields MethodVisitor visitor = checkNotNull( companion.visitMethod( Opcodes.ACC_STATIC | Opcodes.ACC_PUBLIC, COMPANION_METHOD_TO_TRIGGER_INTERFACE_CLINIT_NAME, COMPANION_METHOD_TO_TRIGGER_INTERFACE_CLINIT_DESC, null, null), "Cannot get a method visitor to write out %s to the companion class.", COMPANION_METHOD_TO_TRIGGER_INTERFACE_CLINIT_NAME); // Visit the interface field to triger of the interface. visitor.visitFieldInsn( Opcodes.GETSTATIC, interfaceFieldToAccessInCompanionMethodToTriggerInterfaceClinit.owner(), interfaceFieldToAccessInCompanionMethodToTriggerInterfaceClinit.name(), interfaceFieldToAccessInCompanionMethodToTriggerInterfaceClinit.desc()); Type fieldType = Type.getType(interfaceFieldToAccessInCompanionMethodToTriggerInterfaceClinit.desc()); if (fieldType.getSort() == Type.LONG || fieldType.getSort() == Type.DOUBLE) { visitor.visitInsn(Opcodes.POP2); } else { visitor.visitInsn(Opcodes.POP); } visitor.visitInsn(Opcodes.RETURN); } @Override public FieldVisitor visitField( int access, String name, String desc, String signature, Object value) { if (legacyJaCoCo && isInterface() && BitFlags.isSet(access, Opcodes.ACC_FINAL) && "$jacocoData".equals(name)) { // Move $jacocoData field to companion class and remove final modifier. We'll rewrite field // accesses accordingly. Code generated by older JaCoCo versions tried to assign to this // final field in methods, and interface fields have to be private, so we move the field // to a class, which ends up looking pretty similar to what JaCoCo generates for classes. access &= ~Opcodes.ACC_FINAL; return companion().visitField(access, name, desc, signature, value); } else { return super.visitField(access, name, desc, signature, value); } } @Override public MethodVisitor visitMethod( int access, String name, String desc, String signature, String[] exceptions) { String codeOwner = internalName; MethodVisitor result; if (isInterface() && isStaticInitializer(name)) { result = new InterfaceFieldWriteCollector( super.visitMethod(access, name, desc, signature, exceptions)); if (result != null && legacyJaCoCo) { result = new MoveJacocoFieldAccess(result); } } else if (isInterface() && BitFlags.noneSet(access, Opcodes.ACC_ABSTRACT | Opcodes.ACC_BRIDGE)) { checkArgument(BitFlags.noneSet(access, Opcodes.ACC_NATIVE), "Forbidden per JLS ch 9.4"); boolean isLambdaBody = name.startsWith("lambda$") && BitFlags.isSynthetic(access); if (isLambdaBody) { access &= ~Opcodes.ACC_PUBLIC; // undo visibility change from LambdaDesugaring } name = normalizeInterfaceMethodName(name, isLambdaBody, BitFlags.isStatic(access)); codeOwner = getCompanionClassName(internalName); if (BitFlags.isStatic(access)) { // Completely move static interface methods, which requires rewriting call sites result = companion() .visitMethod(access & ~Opcodes.ACC_PRIVATE, name, desc, signature, exceptions); } else { MethodVisitor abstractDest; if (isLambdaBody) { // Completely move lambda bodies, which requires rewriting call sites access &= ~Opcodes.ACC_PRIVATE; abstractDest = null; } else { // Make default methods abstract but move their implementation into a static method with // corresponding signature. Doesn't require callsite rewriting but implementing classes // may need to implement default methods explicitly. checkArgument( BitFlags.noneSet(access, Opcodes.ACC_PRIVATE), "Unexpected private interface method %s.%s : %s", name, internalName, desc); ++numberOfDefaultMethods; if (coreLibrarySupport != null) { coreLibrarySupport.registerIfEmulatedCoreInterface( access, internalName, name, desc, exceptions); } abstractDest = super.visitMethod(access | Opcodes.ACC_ABSTRACT, name, desc, signature, exceptions); } // TODO(b/37110951): adjust signature with explicit receiver type, which may be generic MethodVisitor codeDest = companion() .visitMethod( access | Opcodes.ACC_STATIC, name, companionDefaultMethodDescriptor(internalName, desc), (String) null, // drop signature, since given one doesn't include the new param exceptions); result = abstractDest != null ? new MultiplexAnnotations(codeDest, abstractDest) : codeDest; } if (result != null && legacyJaCoCo) { result = new MoveJacocoFieldAccess(result); } } else { result = super.visitMethod(access, name, desc, signature, exceptions); } return result != null ? new InterfaceInvocationRewriter( result, isInterface() ? internalName : null, bootclasspath, targetLoader, depsCollector, codeOwner) : null; } @Override public void visitOuterClass(String owner, String name, String desc) { // Proguard gets grumpy if an outer method doesn't exist, which can be the result of moving // interface methods to companion classes (b/68260836). In that case (for which we need to // figure out if "owner" is an interface) need to adjust the outer method information. if (name != null && interfaceCache.isOuterInterface(owner, internalName)) { // Just drop outer method info. That's unfortunate, but the only alternative would be to // change the outer method to point to the companion class, which would mean the // reflection methods that use this information would return a companion ($$CC) class name // as well as a possibly-modified method name and signature, so it seems better to return // the correct original interface name and no method information. Doing this also saves // us from doing even more work to figure out whether the method is static and a lambda // method, which we'd need to known to adjust name and descriptor correctly. name = null; desc = null; } // otherwise there's no enclosing method that could've been moved, or owner is a class super.visitOuterClass(owner, name, desc); } private boolean isInterface() { return BitFlags.isInterface(accessFlags); } private static boolean isStaticInitializer(String methodName) { return "".equals(methodName); } static String normalizeInterfaceMethodName(String name, boolean isLambda, boolean isStatic) { if (isLambda) { // Rename lambda method to reflect the new owner. Not doing so confuses LambdaDesugaring // if it's run over this class again. LambdaDesugaring has already renamed the method from // its original name to include the interface name at this point. return name + DependencyCollector.INTERFACE_COMPANION_SUFFIX; } else if (isStatic) { return name + INTERFACE_STATIC_COMPANION_METHOD_SUFFIX; } else { return name; } } static String getCompanionClassName(String interfaceName) { return interfaceName + DependencyCollector.INTERFACE_COMPANION_SUFFIX; } /** * Returns the descriptor of a static method for an instance method with the given receiver and * description, simply by pre-pending the given descriptor's parameter list with the given * receiver type. */ static String companionDefaultMethodDescriptor(String interfaceName, String desc) { Type type = Type.getMethodType(desc); Type[] companionArgs = new Type[type.getArgumentTypes().length + 1]; companionArgs[0] = Type.getObjectType(interfaceName); System.arraycopy(type.getArgumentTypes(), 0, companionArgs, 1, type.getArgumentTypes().length); return Type.getMethodDescriptor(type.getReturnType(), companionArgs); } private ClassVisitor companion() { if (companion == null) { checkState(isInterface()); String companionName = getCompanionClassName(internalName); companion = store.add(companionName); companion.visit( bytecodeVersion, // Companion class must be public so moved methods can be called from anywhere (accessFlags | Opcodes.ACC_SYNTHETIC | Opcodes.ACC_PUBLIC) & ~Opcodes.ACC_INTERFACE, companionName, (String) null, // signature "java/lang/Object", new String[0]); } return companion; } /** * Interface field scanner to get the first field of the current interface that is written in the * initializer. */ private class InterfaceFieldWriteCollector extends MethodVisitor { public InterfaceFieldWriteCollector(MethodVisitor mv) { super(Opcodes.ASM6, mv); } @Override public void visitFieldInsn(int opcode, String owner, String name, String desc) { if (interfaceFieldToAccessInCompanionMethodToTriggerInterfaceClinit == null && opcode == Opcodes.PUTSTATIC && owner.equals(internalName)) { // It is possible that an interface initializer can sets fields of other classes. // (b/64290760), so we test whether the owner is the same as the internalName. interfaceFieldToAccessInCompanionMethodToTriggerInterfaceClinit = FieldInfo.create(owner, name, desc); } super.visitFieldInsn(opcode, owner, name, desc); } } /** * Rewriter for calls to static interface methods and super calls to default methods, unless * they're part of the bootclasspath, as well as all lambda body methods. Keeps calls to interface * methods declared in the bootclasspath as-is (but note that these would presumably fail on * devices without those methods). */ static class InterfaceInvocationRewriter extends MethodVisitor { /** * If we're visiting a method declared in an interface, the internal name of that interface. * That lets us rewrite invocations of other methods within that interface even if the bytecode * fails to indicate them as interface method invocations, as older versions of JaCoCo failed to * do (b/62623509). */ @Nullable private final String interfaceName; private final ClassReaderFactory bootclasspath; private final ClassLoader targetLoader; private final DependencyCollector depsCollector; /** Internal name that'll be used to record any dependencies on interface methods. */ private final String declaringClass; public InterfaceInvocationRewriter( MethodVisitor dest, @Nullable String knownInterfaceName, ClassReaderFactory bootclasspath, ClassLoader targetLoader, DependencyCollector depsCollector, String declaringClass) { super(Opcodes.ASM6, dest); this.interfaceName = knownInterfaceName; this.bootclasspath = bootclasspath; this.targetLoader = targetLoader; this.depsCollector = depsCollector; this.declaringClass = declaringClass; } @Override public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) { // Assume that any static interface methods on the classpath are moved if ((itf || owner.equals(interfaceName)) && !bootclasspath.isKnown(owner)) { boolean isLambda = name.startsWith("lambda$"); name = normalizeInterfaceMethodName(name, isLambda, opcode == Opcodes.INVOKESTATIC); if (isLambda) { // Redirect lambda invocations to completely remove all lambda methods from interfaces. checkArgument(!owner.endsWith(DependencyCollector.INTERFACE_COMPANION_SUFFIX), "shouldn't consider %s an interface", owner); if (opcode == Opcodes.INVOKEINTERFACE) { opcode = Opcodes.INVOKESTATIC; desc = companionDefaultMethodDescriptor(owner, desc); } else { checkArgument( opcode == Opcodes.INVOKESTATIC, "Unexpected opcode %s to invoke %s.%s", opcode, owner, name); } // Reflect that InterfaceDesugaring moves and renames the lambda body method owner += DependencyCollector.INTERFACE_COMPANION_SUFFIX; itf = false; // Record dependency on companion class depsCollector.assumeCompanionClass(declaringClass, owner); String expectedLambdaMethodName = LambdaDesugaring.uniqueInPackage(owner, name); checkState( name.equals(expectedLambdaMethodName), "Unexpected lambda body method name for %s: real=%s, expected=%s", owner, name, expectedLambdaMethodName); } else if ((opcode == Opcodes.INVOKESTATIC || opcode == Opcodes.INVOKESPECIAL)) { checkArgument(!owner.endsWith(DependencyCollector.INTERFACE_COMPANION_SUFFIX), "shouldn't consider %s an interface", owner); if (opcode == Opcodes.INVOKESPECIAL) { // Turn Interface.super.m() into DefiningInterface$$CC.m(receiver). Note that owner // always refers to the current type's immediate super-interface, but the default method // may be inherited by that interface, so we have to figure out where the method is // defined and invoke it in the corresponding companion class (b/73355452). Note that // we're always dealing with interfaces here, and all interface methods are public, // so using Class.getMethods should suffice to find inherited methods. Also note this // can only be a default method invocation, no abstract method invocation. owner = findDefaultMethod(owner, name, desc) .getDeclaringClass().getName().replace('.', '/'); opcode = Opcodes.INVOKESTATIC; desc = companionDefaultMethodDescriptor(owner, desc); } owner += DependencyCollector.INTERFACE_COMPANION_SUFFIX; itf = false; // Record dependency on companion class depsCollector.assumeCompanionClass(declaringClass, owner); } } super.visitMethodInsn(opcode, owner, name, desc, itf); } private Method findDefaultMethod(String owner, String name, String desc) { try { Class clazz = targetLoader.loadClass(owner.replace('/', '.')); // otherwise getting public methods with getMethods() below isn't enough checkArgument(clazz.isInterface(), "Not an interface: %s", owner); for (Method m : clazz.getMethods()) { if (m.getName().equals(name) && Type.getMethodDescriptor(m).equals(desc)) { checkState(m.isDefault(), "Found non-default method: %s", m); return m; } } } catch (ClassNotFoundException e) { throw new IllegalStateException("Couldn't load " + owner, e); } throw new IllegalArgumentException("Method not found: " + owner + "." + name + desc); } } /** * Method visitor intended for interface method bodies that rewrites jacoco field accesses to * expect the field in the companion class, to work around problematic bytecode emitted by older * JaCoCo versions (b/62623509). */ private static class MoveJacocoFieldAccess extends MethodVisitor { public MoveJacocoFieldAccess(MethodVisitor mv) { super(Opcodes.ASM6, mv); } @Override public void visitFieldInsn(int opcode, String owner, String name, String desc) { if ("$jacocoData".equals(name)) { checkState(!owner.endsWith(DependencyCollector.INTERFACE_COMPANION_SUFFIX), "Expected interface: %s", owner); owner = getCompanionClassName(owner); } super.visitFieldInsn(opcode, owner, name, desc); } } /** * Method visitor that behaves like a passthrough but additionally duplicates all annotations into * a second given {@link MethodVisitor}. */ private static class MultiplexAnnotations extends MethodVisitor { private final MethodVisitor annotationOnlyDest; public MultiplexAnnotations(@Nullable MethodVisitor dest, MethodVisitor annotationOnlyDest) { super(Opcodes.ASM6, dest); this.annotationOnlyDest = annotationOnlyDest; } @Override public void visitParameter(String name, int access) { super.visitParameter(name, access); annotationOnlyDest.visitParameter(name, access); } @Override public AnnotationVisitor visitAnnotation(String desc, boolean visible) { AnnotationVisitor dest = super.visitAnnotation(desc, visible); AnnotationVisitor annoDest = annotationOnlyDest.visitAnnotation(desc, visible); return new MultiplexAnnotationVisitor(dest, annoDest); } @Override public AnnotationVisitor visitTypeAnnotation( int typeRef, TypePath typePath, String desc, boolean visible) { AnnotationVisitor dest = super.visitTypeAnnotation(typeRef, typePath, desc, visible); AnnotationVisitor annoDest = annotationOnlyDest.visitTypeAnnotation(typeRef, typePath, desc, visible); return new MultiplexAnnotationVisitor(dest, annoDest); } @Override public AnnotationVisitor visitParameterAnnotation(int parameter, String desc, boolean visible) { AnnotationVisitor dest = super.visitParameterAnnotation(parameter, desc, visible); AnnotationVisitor annoDest = annotationOnlyDest.visitParameterAnnotation(parameter, desc, visible); return new MultiplexAnnotationVisitor(dest, annoDest); } } /** * Annotation visitor that recursively passes the visited annotations to any number of given * {@link AnnotationVisitor}s. */ private static class MultiplexAnnotationVisitor extends AnnotationVisitor { private final AnnotationVisitor[] moreDestinations; public MultiplexAnnotationVisitor( @Nullable AnnotationVisitor dest, AnnotationVisitor... moreDestinations) { super(Opcodes.ASM6, dest); this.moreDestinations = moreDestinations; } @Override public void visit(String name, Object value) { super.visit(name, value); for (AnnotationVisitor dest : moreDestinations) { dest.visit(name, value); } } @Override public void visitEnum(String name, String desc, String value) { super.visitEnum(name, desc, value); for (AnnotationVisitor dest : moreDestinations) { dest.visitEnum(name, desc, value); } } @Override public AnnotationVisitor visitAnnotation(String name, String desc) { AnnotationVisitor[] subVisitors = new AnnotationVisitor[moreDestinations.length]; AnnotationVisitor dest = super.visitAnnotation(name, desc); for (int i = 0; i < subVisitors.length; ++i) { subVisitors[i] = moreDestinations[i].visitAnnotation(name, desc); } return new MultiplexAnnotationVisitor(dest, subVisitors); } @Override public AnnotationVisitor visitArray(String name) { AnnotationVisitor[] subVisitors = new AnnotationVisitor[moreDestinations.length]; AnnotationVisitor dest = super.visitArray(name); for (int i = 0; i < subVisitors.length; ++i) { subVisitors[i] = moreDestinations[i].visitArray(name); } return new MultiplexAnnotationVisitor(dest, subVisitors); } @Override public void visitEnd() { super.visitEnd(); for (AnnotationVisitor dest : moreDestinations) { dest.visitEnd(); } } } }