diff options
Diffstat (limited to 'java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java')
-rw-r--r-- | java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java | 272 |
1 files changed, 212 insertions, 60 deletions
diff --git a/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java b/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java index 1aaf0b6..1de48bf 100644 --- a/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java +++ b/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java @@ -18,11 +18,15 @@ 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.devtools.build.android.desugar.io.BitFlags; +import java.lang.reflect.Method; +import java.util.Arrays; import java.util.Comparator; import java.util.HashSet; import java.util.Iterator; import java.util.Set; import java.util.TreeSet; +import javax.annotation.Nullable; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.MethodVisitor; @@ -44,6 +48,7 @@ public class DefaultMethodClassFixer extends ClassVisitor { private final ClassReaderFactory bootclasspath; private final ClassLoader targetLoader; private final DependencyCollector depsCollector; + @Nullable private final CoreLibrarySupport coreLibrarySupport; private final HashSet<String> instanceMethods = new HashSet<>(); private boolean isInterface; @@ -57,10 +62,12 @@ public class DefaultMethodClassFixer extends ClassVisitor { ClassVisitor dest, ClassReaderFactory classpath, DependencyCollector depsCollector, + @Nullable CoreLibrarySupport coreLibrarySupport, ClassReaderFactory bootclasspath, ClassLoader targetLoader) { super(Opcodes.ASM6, dest); this.classpath = classpath; + this.coreLibrarySupport = coreLibrarySupport; this.bootclasspath = bootclasspath; this.targetLoader = targetLoader; this.depsCollector = depsCollector; @@ -88,7 +95,9 @@ public class DefaultMethodClassFixer extends ClassVisitor { @Override public void visitEnd() { - if (!isInterface && defaultMethodsDefined(directInterfaces)) { + if (!isInterface + && (mayNeedInterfaceStubsForEmulatedSuperclass() + || defaultMethodsDefined(directInterfaces))) { // Inherited methods take precedence over default methods, so visit all superclasses and // figure out what methods they declare before stubbing in any missing default methods. recordInheritedMethods(); @@ -190,8 +199,14 @@ public class DefaultMethodClassFixer extends ClassVisitor { return super.visitMethod(access, name, desc, signature, exceptions); } + private boolean mayNeedInterfaceStubsForEmulatedSuperclass() { + return coreLibrarySupport != null + && !coreLibrarySupport.isEmulatedCoreClassOrInterface(internalName) + && coreLibrarySupport.isEmulatedCoreClassOrInterface(superName); + } + private void stubMissingDefaultAndBridgeMethods() { - TreeSet<Class<?>> allInterfaces = new TreeSet<>(InterfaceComparator.INSTANCE); + TreeSet<Class<?>> allInterfaces = new TreeSet<>(SubtypeComparator.INSTANCE); for (String direct : directInterfaces) { // Loading ensures all transitively implemented interfaces can be loaded, which is necessary // to produce correct default method stubs in all cases. We could do without classloading but @@ -203,19 +218,55 @@ public class DefaultMethodClassFixer extends ClassVisitor { } Class<?> superclass = loadFromInternal(superName); + boolean mayNeedStubsForSuperclass = mayNeedInterfaceStubsForEmulatedSuperclass(); + if (mayNeedStubsForSuperclass) { + // Collect interfaces inherited from emulated superclasses as well, to handle things like + // extending AbstractList without explicitly implementing List. + for (Class<?> clazz = superclass; clazz != null; clazz = clazz.getSuperclass()) { + for (Class<?> itf : superclass.getInterfaces()) { + collectInterfaces(itf, allInterfaces); + } + } + } for (Class<?> interfaceToVisit : allInterfaces) { // if J extends I, J is allowed to redefine I's default methods. The comparator we used // above makes sure we visit J before I in that case so we can use J's definition. - if (superclass != null && interfaceToVisit.isAssignableFrom(superclass)) { - // superclass already implements this interface, so we must skip it. The superclass will - // be similarly rewritten or comes from the bootclasspath; either way we don't need to and - // shouldn't stub default methods for this interface. + if (!mayNeedStubsForSuperclass && interfaceToVisit.isAssignableFrom(superclass)) { + // superclass is also rewritten and already implements this interface, so we _must_ skip it. continue; } - stubMissingDefaultAndBridgeMethods(interfaceToVisit.getName().replace('.', '/')); + stubMissingDefaultAndBridgeMethods( + interfaceToVisit.getName().replace('.', '/'), mayNeedStubsForSuperclass); } } + private void stubMissingDefaultAndBridgeMethods( + String implemented, boolean mayNeedStubsForSuperclass) { + ClassReader bytecode; + boolean isBootclasspath; + if (bootclasspath.isKnown(implemented)) { + if (coreLibrarySupport != null + && (coreLibrarySupport.isRenamedCoreLibrary(implemented) + || coreLibrarySupport.isEmulatedCoreClassOrInterface(implemented))) { + bytecode = checkNotNull(bootclasspath.readIfKnown(implemented), implemented); + isBootclasspath = true; + } else { + // Default methods from interfaces on the bootclasspath that we're not renaming or emulating + // are assumed available at runtime, so just ignore them. + return; + } + } else { + bytecode = + checkNotNull( + classpath.readIfKnown(implemented), + "Couldn't find interface %s implemented by %s", implemented, internalName); + isBootclasspath = false; + } + bytecode.accept( + new DefaultMethodStubber(isBootclasspath, mayNeedStubsForSuperclass), + ClassReader.SKIP_DEBUG); + } + private Class<?> loadFromInternal(String internalName) { try { return targetLoader.loadClass(internalName.replace('/', '.')); @@ -236,7 +287,8 @@ public class DefaultMethodClassFixer extends ClassVisitor { } private void recordInheritedMethods() { - InstanceMethodRecorder recorder = new InstanceMethodRecorder(); + InstanceMethodRecorder recorder = + new InstanceMethodRecorder(mayNeedInterfaceStubsForEmulatedSuperclass()); String internalName = superName; while (internalName != null) { ClassReader bytecode = bootclasspath.readIfKnown(internalName); @@ -313,26 +365,38 @@ public class DefaultMethodClassFixer extends ClassVisitor { */ private boolean defaultMethodsDefined(ImmutableList<String> interfaces) { for (String implemented : interfaces) { + ClassReader bytecode; if (bootclasspath.isKnown(implemented)) { - continue; - } - ClassReader bytecode = classpath.readIfKnown(implemented); - if (bytecode == null) { - // Interface isn't on the classpath, which indicates incomplete classpaths. Record missing - // dependency so we can check it later. If we don't check then we may get runtime failures - // or wrong behavior from default methods that should've been stubbed in. - // TODO(kmb): Print a warning so people can start fixing their deps? - depsCollector.missingImplementedInterface(internalName, implemented); + if (coreLibrarySupport != null + && coreLibrarySupport.isEmulatedCoreClassOrInterface(implemented)) { + return true; // need to stub in emulated interface methods such as Collection.stream() + } else if (coreLibrarySupport != null + && coreLibrarySupport.isRenamedCoreLibrary(implemented)) { + // Check default methods of renamed interfaces + bytecode = checkNotNull(bootclasspath.readIfKnown(implemented), implemented); + } else { + continue; + } } else { - // Class in classpath and bootclasspath is a bad idea but in any event, assume the - // bootclasspath will take precedence like in a classloader. - // We can skip code attributes as we just need to find default methods to stub. - DefaultMethodFinder finder = new DefaultMethodFinder(); - bytecode.accept(finder, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG); - if (finder.foundDefaultMethods()) { - return true; + bytecode = classpath.readIfKnown(implemented); + if (bytecode == null) { + // Interface isn't on the classpath, which indicates incomplete classpaths. Record missing + // dependency so we can check it later. If we don't check then we may get runtime + // failures or wrong behavior from default methods that should've been stubbed in. + // TODO(kmb): Print a warning so people can start fixing their deps? + depsCollector.missingImplementedInterface(internalName, implemented); + continue; } } + + // Class in classpath and bootclasspath is a bad idea but in any event, assume the + // bootclasspath will take precedence like in a classloader. + // We can skip code attributes as we just need to find default methods to stub. + DefaultMethodFinder finder = new DefaultMethodFinder(); + bytecode.accept(finder, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG); + if (finder.foundDefaultMethods()) { + return true; + } } return false; } @@ -365,30 +429,22 @@ public class DefaultMethodClassFixer extends ClassVisitor { && !instanceMethods.contains(name + ":" + desc); } - private void stubMissingDefaultAndBridgeMethods(String implemented) { - if (bootclasspath.isKnown(implemented)) { - // Default methods on the bootclasspath will be available at runtime, so just ignore them. - return; - } - ClassReader bytecode = - checkNotNull( - classpath.readIfKnown(implemented), - "Couldn't find interface %s implemented by %s", - implemented, - internalName); - bytecode.accept(new DefaultMethodStubber(), ClassReader.SKIP_DEBUG); - } - /** * Visitor for interfaces that produces delegates in the class visited by the outer {@link * DefaultMethodClassFixer} for every default method encountered. */ private class DefaultMethodStubber extends ClassVisitor { + private final boolean isBootclasspathInterface; + private final boolean mayNeedStubsForSuperclass; + private String stubbedInterfaceName; - public DefaultMethodStubber() { + public DefaultMethodStubber( + boolean isBootclasspathInterface, boolean mayNeedStubsForSuperclass) { super(Opcodes.ASM6); + this.isBootclasspathInterface = isBootclasspathInterface; + this.mayNeedStubsForSuperclass = mayNeedStubsForSuperclass; } @Override @@ -413,8 +469,11 @@ public class DefaultMethodClassFixer extends ClassVisitor { // definitions conflict, but see stubMissingDefaultMethods() for how we deal with default // methods redefined in interfaces extending another. recordIfInstanceMethod(access, name, desc); - depsCollector.assumeCompanionClass( - internalName, InterfaceDesugaring.getCompanionClassName(stubbedInterfaceName)); + if (!isBootclasspathInterface) { + // Don't record these dependencies, as we can't check them + depsCollector.assumeCompanionClass( + internalName, InterfaceDesugaring.getCompanionClassName(stubbedInterfaceName)); + } // Add this method to the class we're desugaring and stub in a body to call the default // implementation in the interface's companion class. ijar omits these methods when setting @@ -424,6 +483,21 @@ public class DefaultMethodClassFixer extends ClassVisitor { MethodVisitor stubMethod = DefaultMethodClassFixer.this.visitMethod(access, name, desc, (String) null, exceptions); + String receiverName = stubbedInterfaceName; + String owner = InterfaceDesugaring.getCompanionClassName(stubbedInterfaceName); + if (mayNeedStubsForSuperclass) { + // Reflect what CoreLibraryInvocationRewriter would do if it encountered a super-call to a + // moved implementation of an emulated method. Equivalent to emitting the invokespecial + // super call here and relying on CoreLibraryInvocationRewriter for the rest + Class<?> emulatedImplementation = + coreLibrarySupport.getCoreInterfaceRewritingTarget( + Opcodes.INVOKESPECIAL, superName, name, desc, /*itf=*/ false); + if (emulatedImplementation != null && !emulatedImplementation.isInterface()) { + receiverName = emulatedImplementation.getName().replace('.', '/'); + owner = checkNotNull(coreLibrarySupport.getMoveTarget(receiverName, name)); + } + } + int slot = 0; stubMethod.visitVarInsn(Opcodes.ALOAD, slot++); // load the receiver Type neededType = Type.getMethodType(desc); @@ -433,30 +507,92 @@ public class DefaultMethodClassFixer extends ClassVisitor { } stubMethod.visitMethodInsn( Opcodes.INVOKESTATIC, - InterfaceDesugaring.getCompanionClassName(stubbedInterfaceName), + owner, name, - InterfaceDesugaring.companionDefaultMethodDescriptor(stubbedInterfaceName, desc), - /*itf*/ false); + InterfaceDesugaring.companionDefaultMethodDescriptor(receiverName, desc), + /*itf=*/ false); stubMethod.visitInsn(neededType.getReturnType().getOpcode(Opcodes.IRETURN)); stubMethod.visitMaxs(0, 0); // rely on class writer to compute these stubMethod.visitEnd(); - return null; + return null; // don't visit the visited interface's default method } else if (shouldStubAsBridgeDefaultMethod(access, name, desc)) { recordIfInstanceMethod(access, name, desc); - // For bridges we just copy their bodies instead of going through the companion class. - // Meanwhile, we also need to desugar the copied method bodies, so that any calls to - // interface methods are correctly handled. - return new InterfaceDesugaring.InterfaceInvocationRewriter( - DefaultMethodClassFixer.this.visitMethod(access, name, desc, (String) null, exceptions), - stubbedInterfaceName, - bootclasspath, - depsCollector, - internalName); + MethodVisitor stubMethod = + DefaultMethodClassFixer.this.visitMethod(access, name, desc, (String) null, exceptions); + // If we're visiting a bootclasspath interface then we most likely don't have the code. + // That means we can't just copy the method bodies as we're trying to do below. + if (isBootclasspathInterface) { + // Synthesize a "bridge" method that calls the true implementation + Method bridged = findBridgedMethod(name, desc); + checkState(bridged != null, + "TODO: Can't stub core interface bridge method %s.%s %s in %s", + stubbedInterfaceName, name, desc, internalName); + + int slot = 0; + stubMethod.visitVarInsn(Opcodes.ALOAD, slot++); // load the receiver + Type neededType = Type.getType(bridged); + for (Type arg : neededType.getArgumentTypes()) { + // TODO(b/73586397): insert downcasts if necessary + stubMethod.visitVarInsn(arg.getOpcode(Opcodes.ILOAD), slot); + slot += arg.getSize(); + } + // Just call the bridged method directly on the visited class using invokevirtual + stubMethod.visitMethodInsn( + Opcodes.INVOKEVIRTUAL, + internalName, + name, + neededType.getDescriptor(), + /*itf=*/ false); + stubMethod.visitInsn(neededType.getReturnType().getOpcode(Opcodes.IRETURN)); + + stubMethod.visitMaxs(0, 0); // rely on class writer to compute these + stubMethod.visitEnd(); + return null; // don't visit the visited interface's bridge method + } else { + // For bridges we just copy their bodies instead of going through the companion class. + // Meanwhile, we also need to desugar the copied method bodies, so that any calls to + // interface methods are correctly handled. + return new InterfaceDesugaring.InterfaceInvocationRewriter( + stubMethod, + stubbedInterfaceName, + bootclasspath, + targetLoader, + depsCollector, + internalName); + } } else { - return null; // we don't care about the actual code in these methods + return null; // not a default or bridge method or the class already defines this method. } } + + /** + * Returns a non-bridge interface method with given name that a method with the given descriptor + * can bridge to, if any such method can be found. + */ + @Nullable + private Method findBridgedMethod(String name, String desc) { + Type[] paramTypes = Type.getArgumentTypes(desc); + Class<?> itf = loadFromInternal(stubbedInterfaceName); + checkArgument(itf.isInterface(), "Should be an interface: %s", stubbedInterfaceName); + Method result = null; + for (Method m : itf.getDeclaredMethods()) { + if (m.isBridge()) { + continue; + } + if (!m.getName().equals(name)) { + continue; + } + // For now, only support specialized return types (which don't require casts) + // TODO(b/73586397): Make this work for other kinds of bridges in core library interfaces + if (Arrays.equals(paramTypes, Type.getArgumentTypes(m))) { + checkState(result == null, + "Found multiple bridge target %s and %s for descriptor %s", result, m, desc); + return result = m; + } + } + return result; + } } /** @@ -510,8 +646,13 @@ public class DefaultMethodClassFixer extends ClassVisitor { private class InstanceMethodRecorder extends ClassVisitor { - public InstanceMethodRecorder() { + private final boolean ignoreEmulatedMethods; + + private String className; + + public InstanceMethodRecorder(boolean ignoreEmulatedMethods) { super(Opcodes.ASM6); + this.ignoreEmulatedMethods = ignoreEmulatedMethods; } @Override @@ -523,12 +664,23 @@ public class DefaultMethodClassFixer extends ClassVisitor { String superName, String[] interfaces) { checkArgument(BitFlags.noneSet(access, Opcodes.ACC_INTERFACE)); + className = name; // updated every time we start visiting another superclass super.visit(version, access, name, signature, superName, interfaces); } @Override public MethodVisitor visitMethod( int access, String name, String desc, String signature, String[] exceptions) { + if (ignoreEmulatedMethods + && BitFlags.noneSet(access, Opcodes.ACC_STATIC) // short-circuit + && coreLibrarySupport.getCoreInterfaceRewritingTarget( + Opcodes.INVOKEVIRTUAL, className, name, desc, /*itf=*/ false) + != null) { + // *don't* record emulated core library method implementations in immediate subclasses of + // emulated core library clasess so that they can be stubbed (since the inherited + // implementation may be missing at runtime). + return null; + } recordIfInstanceMethod(access, name, desc); return null; } @@ -594,17 +746,17 @@ public class DefaultMethodClassFixer extends ClassVisitor { } } - /** Comparator for interfaces that compares by whether interfaces extend one another. */ - enum InterfaceComparator implements Comparator<Class<?>> { + /** Comparator for classes and interfaces that compares by whether subtyping relationship. */ + enum SubtypeComparator implements Comparator<Class<?>> { + /** Orders subtypes before supertypes and breaks ties lexicographically. */ INSTANCE; @Override public int compare(Class<?> o1, Class<?> o2) { - checkArgument(o1.isInterface()); - checkArgument(o2.isInterface()); if (o1 == o2) { return 0; } + // order subtypes before supertypes if (o1.isAssignableFrom(o2)) { // o1 is supertype of o2 return 1; // we want o1 to come after o2 } |