aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/tools/android/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java
diff options
context:
space:
mode:
authorGravatar cnsun <cnsun@google.com>2017-06-28 18:05:48 +0200
committerGravatar Marcel Hlopko <hlopko@google.com>2017-06-29 09:31:57 +0200
commit8740ca6bd0f4156aaf663c482bc3e9c7ebb2c556 (patch)
treef2a05eb23192d8c236dbfe6da5e9ada5d9763a95 /src/tools/android/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java
parentc251b05375808fe2e22a1931853603e76043c932 (diff)
Access interface constants to explicitly trigger the execution of interface
initializers. The problem is that when we desugar default methods, the static intializer of interface will not be executed. The JVM spec says that if an interface has default methods, then when it is loaded, it will also be initialized. After we desugar such an interface, its default methods are removed, and when we load the interface, the <clinit> will not be executed. This CL checks whether an interface has default methods and fields. If yes (needs to be initialized when the interface is loaded), it injects field access code to access the interface fields in the <clinit> of the companion class. We also create a constant $$CONSTANT$$ in the companion class. Then for all the classes that implement the interface, we inject GETSTATIC in the class <clinit> to the $$CONSTANT$$ of the companion class of the interface. This indirection is to avoid the IllegalAccessError when the interface is package private. Note that accessing an arbitrary interface field does not guarantee the interface will be initialized. We need to access the field that is initialized in the interface static initializer. RELNOTES: None PiperOrigin-RevId: 160414671
Diffstat (limited to 'src/tools/android/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java')
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java209
1 files changed, 206 insertions, 3 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..d7d46a1919 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 =
+ collectOrderedCompanionsToTriggerInterfaceClinit(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> collectOrderedCompanionsToTriggerInterfaceClinit(
+ ImmutableList<String> interfaces) {
+ ImmutableList.Builder<String> companionCollector = ImmutableList.builder();
+ HashSet<String> visitedInterfaces = new HashSet<>();
+ for (String anInterface : interfaces) {
+ collectOrderedCompanionsToTriggerInterfaceClinit(
+ anInterface, visitedInterfaces, companionCollector);
+ }
+ return companionCollector.build();
+ }
+
+ private void collectOrderedCompanionsToTriggerInterfaceClinit(
+ 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) {
+ collectOrderedCompanionsToTriggerInterfaceClinit(
+ 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;