summaryrefslogtreecommitdiff
path: root/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java
diff options
context:
space:
mode:
Diffstat (limited to 'java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java')
-rw-r--r--java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java272
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
}