// 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.common.collect.ImmutableList; import java.util.Comparator; import java.util.HashSet; import java.util.Set; import java.util.TreeSet; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; /** * Fixer of classes that extend interfaces with default methods to declare any missing methods * explicitly and call the corresponding companion method generated by {@link InterfaceDesugaring}. */ public class DefaultMethodClassFixer extends ClassVisitor { private final ClassReaderFactory classpath; private final ClassReaderFactory bootclasspath; private final ClassLoader targetLoader; private final HashSet<String> instanceMethods = new HashSet<>(); private boolean isInterface; private String internalName; private ImmutableList<String> directInterfaces; private String superName; public DefaultMethodClassFixer(ClassVisitor dest, ClassReaderFactory classpath, ClassReaderFactory bootclasspath, ClassLoader targetLoader) { super(Opcodes.ASM5, dest); this.classpath = classpath; this.bootclasspath = bootclasspath; this.targetLoader = targetLoader; } @Override public void visit( int version, int access, String name, String signature, String superName, String[] interfaces) { checkState(this.directInterfaces == null); isInterface = BitFlags.isSet(access, Opcodes.ACC_INTERFACE); internalName = name; checkArgument(superName != null || "java/lang/Object".equals(name), // ASM promises this "Type without superclass: %s", name); this.directInterfaces = ImmutableList.copyOf(interfaces); this.superName = superName; super.visit(version, access, name, signature, superName, interfaces); } @Override public void visitEnd() { if (!isInterface && 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(); stubMissingDefaultMethods(); } super.visitEnd(); } @Override public MethodVisitor visitMethod( int access, String name, String desc, String signature, String[] exceptions) { // Keep track of instance methods implemented in this class for later. if (!isInterface) { recordIfInstanceMethod(access, name, desc); } return super.visitMethod(access, name, desc, signature, exceptions); } private void stubMissingDefaultMethods() { TreeSet<Class<?>> allInterfaces = new TreeSet<>(InterfaceComparator.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 // it's convenient to rely on Class.isAssignableFrom to compute subtype relationships, and // we'd still have to insist that all transitively implemented interfaces can be loaded. // We don't load the visited class, however, in case it's a generated lambda class. Class<?> itf = loadFromInternal(direct); collectInterfaces(itf, allInterfaces); } Class<?> superclass = loadFromInternal(superName); 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. continue; } stubMissingDefaultMethods(interfaceToVisit.getName().replace('.', '/')); } } private Class<?> loadFromInternal(String internalName) { try { return targetLoader.loadClass(internalName.replace('/', '.')); } catch (ClassNotFoundException e) { throw new IllegalStateException( "Couldn't load " + internalName + ", is the classpath complete?", e); } } private void collectInterfaces(Class<?> itf, Set<Class<?>> dest) { checkArgument(itf.isInterface()); if (!dest.add(itf)) { return; } for (Class<?> implemented : itf.getInterfaces()) { collectInterfaces(implemented, dest); } } private void recordInheritedMethods() { InstanceMethodRecorder recorder = new InstanceMethodRecorder(); String internalName = superName; while (internalName != null) { ClassReader bytecode = bootclasspath.readIfKnown(internalName); if (bytecode == null) { bytecode = checkNotNull(classpath.readIfKnown(internalName), "Superclass not found: %s", internalName); } bytecode.accept(recorder, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG); internalName = bytecode.getSuperName(); } } private void recordIfInstanceMethod(int access, String name, String desc) { if (BitFlags.noneSet(access, Opcodes.ACC_STATIC)) { // Record all declared instance methods, including abstract, bridge, and native methods, as // they all take precedence over default methods. instanceMethods.add(name + ":" + desc); } } /** * 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. */ private boolean defaultMethodsDefined(ImmutableList<String> interfaces) { for (String implemented : interfaces) { ClassReader bytecode = classpath.readIfKnown(implemented); if (bytecode != null && !bootclasspath.isKnown(implemented)) { // 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; } } // Else interface isn't on the classpath, which indicates incomplete classpaths. For now // we'll just assume the missing interfaces don't declare default methods but if they do // we'll end up with concrete classes that don't implement an abstract method, which can // cause runtime failures. The classpath needs to be fixed in this case. } return false; } /** * Returns {@code true} for non-bridge default methods not in {@link #instanceMethods}. */ private boolean shouldStub(int access, String name, String desc) { // Ignore private methods, which technically aren't default methods and can only be called from // other methods defined in the interface. This also ignores lambda body methods, which is fine // as we don't want or need to stub those. Also ignore bridge methods as javac adds them to // concrete classes as needed anyway and we handle them separately for generated lambda classes. return BitFlags.noneSet(access, Opcodes.ACC_ABSTRACT | Opcodes.ACC_STATIC | Opcodes.ACC_BRIDGE | Opcodes.ACC_PRIVATE) && !instanceMethods.contains(name + ":" + desc); } private void stubMissingDefaultMethods(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); // We can skip code attributes as we just need to find default methods to stub. bytecode.accept(new DefaultMethodStubber(), ClassReader.SKIP_CODE | 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 String interfaceName; public DefaultMethodStubber() { super(Opcodes.ASM5); } @Override public void visit( int version, int access, String name, String signature, String superName, String[] interfaces) { checkArgument(BitFlags.isSet(access, Opcodes.ACC_INTERFACE)); checkState(interfaceName == null); interfaceName = name; } @Override public MethodVisitor visitMethod( int access, String name, String desc, String signature, String[] exceptions) { if (shouldStub(access, name, desc)) { // Remember we stubbed this method in case it's also defined by subsequently visited // interfaces. javac would force the method to be defined explicitly if there any two // definitions conflict, but see stubMissingDefaultMethods() for how we deal with default // methods redefined in interfaces extending another. recordIfInstanceMethod(access, name, desc); // 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 // ACC_SYNTHETIC modifier, so don't. // Signatures can be wrong, e.g., when type variables are introduced, instantiated, or // refined in the class we're processing, so drop them. MethodVisitor stubMethod = DefaultMethodClassFixer.this.visitMethod(access, name, desc, (String) null, exceptions); int slot = 0; stubMethod.visitVarInsn(Opcodes.ALOAD, slot++); // load the receiver Type neededType = Type.getMethodType(desc); for (Type arg : neededType.getArgumentTypes()) { stubMethod.visitVarInsn(arg.getOpcode(Opcodes.ILOAD), slot); slot += arg.getSize(); } stubMethod.visitMethodInsn( Opcodes.INVOKESTATIC, interfaceName + InterfaceDesugaring.COMPANION_SUFFIX, name, InterfaceDesugaring.companionDefaultMethodDescriptor(interfaceName, 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; // we don't care about the actual code in these methods } } /** * Visitor for interfaces that recursively searches interfaces for default method declarations. */ private class DefaultMethodFinder extends ClassVisitor { @SuppressWarnings("hiding") private ImmutableList<String> interfaces; private boolean found; public DefaultMethodFinder() { super(Opcodes.ASM5); } @Override public void visit( int version, int access, String name, String signature, String superName, String[] interfaces) { checkArgument(BitFlags.isSet(access, Opcodes.ACC_INTERFACE)); checkState(this.interfaces == null); this.interfaces = ImmutableList.copyOf(interfaces); } public boolean foundDefaultMethods() { return found; } @Override public void visitEnd() { if (!found) { found = defaultMethodsDefined(this.interfaces); } } @Override public MethodVisitor visitMethod( int access, String name, String desc, String signature, String[] exceptions) { if (!found && shouldStub(access, name, desc)) { // Found a default method we're not ignoring (instanceMethods at this point contains methods // the top-level visited class implements itself). found = true; } return null; // we don't care about the actual code in these methods } } private class InstanceMethodRecorder extends ClassVisitor { public InstanceMethodRecorder() { super(Opcodes.ASM5); } @Override public void visit( int version, int access, String name, String signature, String superName, String[] interfaces) { checkArgument(BitFlags.noneSet(access, Opcodes.ACC_INTERFACE)); super.visit(version, access, name, signature, superName, interfaces); } @Override public MethodVisitor visitMethod( int access, String name, String desc, String signature, String[] exceptions) { recordIfInstanceMethod(access, name, desc); return null; } } /** Comparator for interfaces that compares by whether interfaces extend one another. */ enum InterfaceComparator implements Comparator<Class<?>> { INSTANCE; @Override public int compare(Class<?> o1, Class<?> o2) { checkArgument(o1.isInterface()); checkArgument(o2.isInterface()); if (o1 == o2) { return 0; } if (o1.isAssignableFrom(o2)) { // o1 is supertype of o2 return 1; // we want o1 to come after o2 } if (o2.isAssignableFrom(o1)) { // o2 is supertype of o1 return -1; // we want o2 to come after o1 } // o1 and o2 aren't comparable so arbitrarily impose lexicographical ordering return o1.getName().compareTo(o2.getName()); } } }