diff options
3 files changed, 286 insertions, 9 deletions
diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java b/src/tools/android/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java index 2563c7a9ab..68f9756735 100644 --- a/src/tools/android/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java +++ b/src/tools/android/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java @@ -20,6 +20,7 @@ import static com.google.common.base.Preconditions.checkState; import com.google.common.collect.ImmutableList; import java.util.Comparator; import java.util.HashSet; +import java.util.Iterator; import java.util.Set; import java.util.TreeSet; import org.objectweb.asm.ClassReader; @@ -27,6 +28,11 @@ import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.InsnList; +import org.objectweb.asm.tree.InsnNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; /** * Fixer of classes that extend interfaces with default methods to declare any missing methods @@ -43,6 +49,8 @@ public class DefaultMethodClassFixer extends ClassVisitor { private String internalName; private ImmutableList<String> directInterfaces; private String superName; + /** This method node caches <clinit>, and flushes out in {@code visitEnd()}; */ + private MethodNode clInitMethodNode; public DefaultMethodClassFixer( ClassVisitor dest, @@ -82,10 +90,88 @@ public class DefaultMethodClassFixer extends ClassVisitor { // figure out what methods they declare before stubbing in any missing default methods. recordInheritedMethods(); stubMissingDefaultAndBridgeMethods(); + // Check whether there are interfaces with default methods and <clinit>. If yes, the following + // method call will return a list of interface fields to access in the <clinit> to trigger + // the initialization of these interfaces. + ImmutableList<String> companionsToTriggerInterfaceClinit = + computeOrderedCompanionsToTriggerInterfaceClinit(directInterfaces); + if (!companionsToTriggerInterfaceClinit.isEmpty()) { + if (clInitMethodNode == null) { + clInitMethodNode = new MethodNode(Opcodes.ACC_STATIC, "<clinit>", "()V", null, null); + } + desugarClinitToTriggerInterfaceInitializers(companionsToTriggerInterfaceClinit); + } + } + if (clInitMethodNode != null && super.cv != null) { // Write <clinit> to the chained visitor. + clInitMethodNode.accept(super.cv); } super.visitEnd(); } + private boolean isClinitAlreadyDesugared( + ImmutableList<String> companionsToAccessToTriggerInterfaceClinit) { + InsnList instructions = clInitMethodNode.instructions; + if (instructions.size() <= companionsToAccessToTriggerInterfaceClinit.size()) { + // The <clinit> must end with RETURN, so if the instruction count is less than or equal to + // the companion class count, this <clinit> has not been desugared. + return false; + } + Iterator<AbstractInsnNode> iterator = instructions.iterator(); + for (String companion : companionsToAccessToTriggerInterfaceClinit) { + if (!iterator.hasNext()) { + return false; + } + AbstractInsnNode first = iterator.next(); + if (!(first instanceof MethodInsnNode)) { + return false; + } + MethodInsnNode methodInsnNode = (MethodInsnNode) first; + if (methodInsnNode.getOpcode() != Opcodes.INVOKESTATIC + || !methodInsnNode.owner.equals(companion) + || !methodInsnNode.name.equals( + InterfaceDesugaring.COMPANION_METHOD_TO_TRIGGER_INTERFACE_CLINIT_NAME)) { + return false; + } + checkState( + methodInsnNode.desc.equals( + InterfaceDesugaring.COMPANION_METHOD_TO_TRIGGER_INTERFACE_CLINIT_DESC), + "Inconsistent method desc: %s vs %s", + methodInsnNode.desc, + InterfaceDesugaring.COMPANION_METHOD_TO_TRIGGER_INTERFACE_CLINIT_DESC); + + if (!iterator.hasNext()) { + return false; + } + AbstractInsnNode second = iterator.next(); + if (second.getOpcode() != Opcodes.POP) { + return false; + } + } + return true; + } + + private void desugarClinitToTriggerInterfaceInitializers( + ImmutableList<String> companionsToTriggerInterfaceClinit) { + if (isClinitAlreadyDesugared(companionsToTriggerInterfaceClinit)) { + return; + } + InsnList desugarInsts = new InsnList(); + for (String companionClass : companionsToTriggerInterfaceClinit) { + desugarInsts.add( + new MethodInsnNode( + Opcodes.INVOKESTATIC, + companionClass, + InterfaceDesugaring.COMPANION_METHOD_TO_TRIGGER_INTERFACE_CLINIT_NAME, + InterfaceDesugaring.COMPANION_METHOD_TO_TRIGGER_INTERFACE_CLINIT_DESC, + false)); + } + if (clInitMethodNode.instructions.size() == 0) { + clInitMethodNode.instructions.insert(new InsnNode(Opcodes.RETURN)); + } + clInitMethodNode.instructions.insertBefore( + clInitMethodNode.instructions.getFirst(), desugarInsts); + } + @Override public MethodVisitor visitMethod( int access, String name, String desc, String signature, String[] exceptions) { @@ -93,6 +179,11 @@ public class DefaultMethodClassFixer extends ClassVisitor { if (!isInterface) { recordIfInstanceMethod(access, name, desc); } + if ("<clinit>".equals(name)) { + checkState(clInitMethodNode == null, "This class fixer has been used. "); + clInitMethodNode = new MethodNode(access, name, desc, signature, exceptions); + return clInitMethodNode; + } return super.visitMethod(access, name, desc, signature, exceptions); } @@ -165,6 +256,55 @@ public class DefaultMethodClassFixer extends ClassVisitor { } /** + * Starting from the given interfaces, this method scans the interface hierarchy, finds the + * interfaces that have default methods and <clinit>, and returns the companion class names of + * these interfaces. + * + * <p>Note that the returned companion classes are ordered in the order of the interface + * initialization, which is consistent with the JVM behavior. For example, "class A implements I1, + * I2", the returned list would be [I1$$CC, I2$$CC], not [I2$$CC, I1$$CC]. + */ + private ImmutableList<String> computeOrderedCompanionsToTriggerInterfaceClinit( + ImmutableList<String> interfaces) { + ImmutableList.Builder<String> companionCollector = ImmutableList.builder(); + HashSet<String> visitedInterfaces = new HashSet<>(); + for (String anInterface : interfaces) { + computeOrderedCompanionsToTriggerInterfaceClinit( + anInterface, visitedInterfaces, companionCollector); + } + return companionCollector.build(); + } + + private void computeOrderedCompanionsToTriggerInterfaceClinit( + String anInterface, + HashSet<String> visitedInterfaces, + ImmutableList.Builder<String> companionCollector) { + if (!visitedInterfaces.add(anInterface)) { + return; + } + ClassReader bytecode = classpath.readIfKnown(anInterface); + if (bytecode == null || bootclasspath.isKnown(anInterface)) { + return; + } + String[] parentInterfaces = bytecode.getInterfaces(); + if (parentInterfaces != null && parentInterfaces.length > 0) { + for (String parentInterface : parentInterfaces) { + computeOrderedCompanionsToTriggerInterfaceClinit( + parentInterface, visitedInterfaces, companionCollector); + } + } + InterfaceInitializationNecessityDetector necessityDetector = + new InterfaceInitializationNecessityDetector(bytecode.getClassName()); + bytecode.accept(necessityDetector, ClassReader.SKIP_DEBUG); + if (necessityDetector.needsToInitialize()) { + // If we need to initialize this interface, we initialize its companion class, and its + // companion class will initialize the interface then. This desigin decision is made to avoid + // access issue, e.g., package-private interfaces. + companionCollector.add(InterfaceDesugaring.getCompanionClassName(anInterface)); + } + } + + /** * Recursively searches the given interfaces for default methods not implemented by this class * directly. If this method returns true we need to think about stubbing missing default methods. */ @@ -198,10 +338,13 @@ public class DefaultMethodClassFixer extends ClassVisitor { // Note that an exception is that, if a bridge method is for a default interface method, javac // will NOT generate the bridge method in the implementing class. So we need extra logic to // handle these bridge methods. + return isNonBridgeDefaultMethod(access) && !instanceMethods.contains(name + ":" + desc); + } + + private static boolean isNonBridgeDefaultMethod(int access) { return BitFlags.noneSet( - access, - Opcodes.ACC_ABSTRACT | Opcodes.ACC_STATIC | Opcodes.ACC_BRIDGE | Opcodes.ACC_PRIVATE) - && !instanceMethods.contains(name + ":" + desc); + access, + Opcodes.ACC_ABSTRACT | Opcodes.ACC_STATIC | Opcodes.ACC_BRIDGE | Opcodes.ACC_PRIVATE); } /** @@ -379,6 +522,66 @@ public class DefaultMethodClassFixer extends ClassVisitor { } } + /** + * Detector to determine whether an interface needs to be initialized when it is loaded. + * + * <p>If the interface has a default method, and its <clinit> initializes any of its fields, then + * this interface needs to be initialized. + */ + private static class InterfaceInitializationNecessityDetector extends ClassVisitor { + + private final String internalName; + private boolean hasFieldInitializedInClinit; + private boolean hasDefaultMethods; + + public InterfaceInitializationNecessityDetector(String internalName) { + super(Opcodes.ASM5); + this.internalName = internalName; + } + + public boolean needsToInitialize() { + return hasDefaultMethods && hasFieldInitializedInClinit; + } + + @Override + public void visit( + int version, + int access, + String name, + String signature, + String superName, + String[] interfaces) { + super.visit(version, access, name, signature, superName, interfaces); + checkState( + internalName.equals(name), + "Inconsistent internal names: expected=%s, real=%s", + internalName, + name); + checkArgument( + BitFlags.isSet(access, Opcodes.ACC_INTERFACE), + "This class visitor is only used for interfaces."); + } + + @Override + public MethodVisitor visitMethod( + int access, String name, String desc, String signature, String[] exceptions) { + if (!hasDefaultMethods) { + hasDefaultMethods = isNonBridgeDefaultMethod(access); + } + if ("<clinit>".equals(name)) { + return new MethodVisitor(Opcodes.ASM5) { + @Override + public void visitFieldInsn(int opcode, String owner, String name, String desc) { + if (opcode == Opcodes.PUTSTATIC && internalName.equals(owner)) { + hasFieldInitializedInClinit = true; + } + } + }; + } + return null; // Do not care about the code. + } + } + /** Comparator for interfaces that compares by whether interfaces extend one another. */ enum InterfaceComparator implements Comparator<Class<?>> { INSTANCE; diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/Desugar.java b/src/tools/android/java/com/google/devtools/build/android/desugar/Desugar.java index bbba22e149..74dfa176a1 100644 --- a/src/tools/android/java/com/google/devtools/build/android/desugar/Desugar.java +++ b/src/tools/android/java/com/google/devtools/build/android/desugar/Desugar.java @@ -20,6 +20,7 @@ import static com.google.devtools.build.android.desugar.LambdaClassMaker.LAMBDA_ import static java.nio.charset.StandardCharsets.ISO_8859_1; import com.google.auto.value.AutoValue; +import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; @@ -625,7 +626,8 @@ class Desugar { return ioPairListbuilder.build(); } - private static class ThrowingClassLoader extends ClassLoader { + @VisibleForTesting + static class ThrowingClassLoader extends ClassLoader { @Override protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { if (name.startsWith("java.")) { @@ -700,7 +702,8 @@ class Desugar { * closer. */ @SuppressWarnings("MustBeClosedChecker") - private static ImmutableList<InputFileProvider> toRegisteredInputFileProvider( + @VisibleForTesting + static ImmutableList<InputFileProvider> toRegisteredInputFileProvider( Closer closer, List<Path> paths) throws IOException { ImmutableList.Builder<InputFileProvider> builder = new ImmutableList.Builder<>(); for (Path path : paths) { diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java b/src/tools/android/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java index adcd1e04af..c32ca9ab98 100644 --- a/src/tools/android/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java +++ b/src/tools/android/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java @@ -14,6 +14,7 @@ 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 javax.annotation.Nullable; @@ -37,6 +38,9 @@ import org.objectweb.asm.TypePath; */ 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 COMPANION_SUFFIX = "$$CC"; static final String INTERFACE_STATIC_COMPANION_METHOD_SUFFIX = "$$STATIC$$"; @@ -47,6 +51,7 @@ class InterfaceDesugaring extends ClassVisitor { private int bytecodeVersion; private int accessFlags; @Nullable private ClassVisitor companion; + @Nullable private FieldInfo interfaceFieldToAccessInCompanionMethodToTriggerInterfaceClinit; public InterfaceDesugaring( ClassVisitor dest, ClassReaderFactory bootclasspath, GeneratedClassStore store) { @@ -73,18 +78,50 @@ class InterfaceDesugaring extends ClassVisitor { @Override public void visitEnd() { if (companion != null) { + // 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 <clinit> of the interface. + visitor.visitFieldInsn( + Opcodes.GETSTATIC, + interfaceFieldToAccessInCompanionMethodToTriggerInterfaceClinit.owner(), + interfaceFieldToAccessInCompanionMethodToTriggerInterfaceClinit.name(), + interfaceFieldToAccessInCompanionMethodToTriggerInterfaceClinit.desc()); + visitor.visitInsn(Opcodes.POP); + visitor.visitInsn(Opcodes.RETURN); + } + @Override public MethodVisitor visitMethod( int access, String name, String desc, String signature, String[] exceptions) { MethodVisitor result; - if (BitFlags.isSet(accessFlags, Opcodes.ACC_INTERFACE) - && BitFlags.noneSet(access, Opcodes.ACC_ABSTRACT | Opcodes.ACC_BRIDGE) - && !"<clinit>".equals(name)) { + if (isStaticInitializer(name)) { + result = + new InterfaceFieldWriteCollector( + super.visitMethod(access, name, desc, signature, exceptions)); + } else if (BitFlags.isSet(accessFlags, Opcodes.ACC_INTERFACE) + && BitFlags.noneSet(access, Opcodes.ACC_ABSTRACT | Opcodes.ACC_BRIDGE)) { checkArgument(BitFlags.noneSet(access, Opcodes.ACC_NATIVE), "Forbidden per JLS ch 9.4"); boolean isLambdaBody = @@ -140,6 +177,10 @@ class InterfaceDesugaring extends ClassVisitor { : null; } + private static boolean isStaticInitializer(String methodName) { + return "<clinit>".equals(methodName); + } + private static String normalizeInterfaceMethodName( String name, boolean isLambda, boolean isStatic) { String suffix; @@ -156,6 +197,10 @@ class InterfaceDesugaring extends ClassVisitor { return name + suffix; } + static String getCompanionClassName(String interfaceName) { + return interfaceName + 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 @@ -172,7 +217,7 @@ class InterfaceDesugaring extends ClassVisitor { private ClassVisitor companion() { if (companion == null) { checkState(BitFlags.isSet(accessFlags, Opcodes.ACC_INTERFACE)); - String companionName = internalName + COMPANION_SUFFIX; + String companionName = getCompanionClassName(internalName); companion = store.add(companionName); companion.visit( @@ -188,6 +233,32 @@ class InterfaceDesugaring extends ClassVisitor { } /** + * Interface field scanner to get the field of the current interface that is written in the + * initializer. + */ + private class InterfaceFieldWriteCollector extends MethodVisitor { + + public InterfaceFieldWriteCollector(MethodVisitor mv) { + super(Opcodes.ASM5, mv); + } + + @Override + public void visitFieldInsn(int opcode, String owner, String name, String desc) { + if (interfaceFieldToAccessInCompanionMethodToTriggerInterfaceClinit == null + && opcode == Opcodes.PUTSTATIC) { + checkState( + owner.equals(internalName), + "Expect only the fields in this interface to be initialized. owner=%s, expected=%s", + owner, + 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 |