From ebd299084e0e4e37fef104619361c8e3c5ef8557 Mon Sep 17 00:00:00 2001 From: kmb Date: Thu, 20 Apr 2017 20:35:04 +0200 Subject: Default and static interface desugaring RELNOTES: n/a PiperOrigin-RevId: 153735445 --- .../build/android/desugar/ClassReaderFactory.java | 10 +- .../android/desugar/DefaultMethodClassFixer.java | 225 +++++++++++++++ .../devtools/build/android/desugar/Desugar.java | 148 +++++++--- .../build/android/desugar/GeneratedClassStore.java | 49 ++++ .../build/android/desugar/InterfaceDesugaring.java | 312 +++++++++++++++++++++ .../build/android/desugar/Java7Compatibility.java | 2 +- .../build/android/desugar/LambdaClassFixer.java | 15 +- 7 files changed, 716 insertions(+), 45 deletions(-) create mode 100644 src/tools/android/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java create mode 100644 src/tools/android/java/com/google/devtools/build/android/desugar/GeneratedClassStore.java create mode 100644 src/tools/android/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java (limited to 'src') diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/ClassReaderFactory.java b/src/tools/android/java/com/google/devtools/build/android/desugar/ClassReaderFactory.java index 2b44d7641a..bae5251d0b 100644 --- a/src/tools/android/java/com/google/devtools/build/android/desugar/ClassReaderFactory.java +++ b/src/tools/android/java/com/google/devtools/build/android/desugar/ClassReaderFactory.java @@ -28,7 +28,7 @@ class ClassReaderFactory { } /** - * Returns a reader for the given/internal/Class$Name if the class is defined in the wrapped Jar + * Returns a reader for the given/internal/Class$Name if the class is defined in the wrapped input * and {@code null} otherwise. For simplicity this method turns checked into runtime exceptions * under the assumption that all classes have already been read once when this method is called. */ @@ -50,4 +50,12 @@ class ClassReaderFactory { return null; } + + /** + * Returns {@code true} if the given given/internal/Class$Name is defined in the wrapped input. + */ + public boolean isKnown(String internalClassName) { + String filename = rewriter.unprefix(internalClassName) + ".class"; + return indexedInputs.getInputFileProvider(filename) != null; + } } 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 new file mode 100644 index 0000000000..a9e86a1575 --- /dev/null +++ b/src/tools/android/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java @@ -0,0 +1,225 @@ +// 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.HashSet; +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 HashSet instanceMethods = new HashSet<>(); + private final HashSet seenInterfaces = new HashSet<>(); + + private boolean isInterface; + private ImmutableList interfaces; + private String superName; + + public DefaultMethodClassFixer(ClassVisitor dest, ClassReaderFactory classpath, + ClassReaderFactory bootclasspath) { + super(Opcodes.ASM5, dest); + this.classpath = classpath; + this.bootclasspath = bootclasspath; + } + + @Override + public void visit( + int version, + int access, + String name, + String signature, + String superName, + String[] interfaces) { + checkState(this.interfaces == null); + isInterface = BitFlags.isSet(access, Opcodes.ACC_INTERFACE); + checkArgument(superName != null || "java/lang/Object".equals(name), // ASM promises this + "Type without superclass: %s", name); + this.interfaces = ImmutableList.copyOf(interfaces); + this.superName = superName; + super.visit(version, access, name, signature, superName, interfaces); + } + + @Override + public void visitEnd() { + if (!isInterface && !interfaces.isEmpty()) { + // 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(interfaces); + } + 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 && !interfaces.isEmpty()) { + recordIfInstanceMethod(access, name, desc); + } + return super.visitMethod(access, name, desc, signature, exceptions); + } + + 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), "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); + } + } + + private void stubMissingDefaultMethods(ImmutableList interfaces) { + for (String implemented : interfaces) { + if (!seenInterfaces.add(implemented)) { + // Skip: a superclass already implements this interface, or we've seen it here + continue; + } + 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. + 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. + */ + public class DefaultMethodStubber extends ClassVisitor { + + @SuppressWarnings("hiding") private ImmutableList interfaces; + 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(this.interfaces == null); + this.interfaces = ImmutableList.copyOf(interfaces); + interfaceName = name; + } + + @Override + public void visitEnd() { + stubMissingDefaultMethods(this.interfaces); + } + + @Override + public MethodVisitor visitMethod( + int access, String name, String desc, String signature, String[] exceptions) { + if (BitFlags.noneSet(access, Opcodes.ACC_ABSTRACT | Opcodes.ACC_STATIC | Opcodes.ACC_BRIDGE) + && instanceMethods.add(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. Don't do this for bridge methods, which we handle + // separately. + // 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 + } + } + + 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)); + for (String inheritedInterface : interfaces) { + // No point copying default methods that we'll also copy for a superclass. Note we may + // be processing a class in the bootclasspath, in which case the interfaces must also + // be in the bootclasspath and we can skip those as well. Also note this is best-effort, + // since these interfaces may extend other interfaces that we're not recording here. + seenInterfaces.add(inheritedInterface); + } + 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; + } + } +} 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 797cbc41be..5e24b3560f 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 @@ -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 static java.nio.charset.StandardCharsets.ISO_8859_1; @@ -42,9 +43,11 @@ import java.nio.file.attribute.BasicFileAttributes; import java.util.Iterator; import java.util.List; import java.util.Map; +import javax.annotation.Nullable; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.tree.ClassNode; /** * Command-line tool to desugar Java 8 constructs that dx doesn't know what to do with, in @@ -145,6 +148,16 @@ class Desugar { ) public int minSdkVersion; + @Option( + name = "desugar_interface_method_bodies_if_needed", + defaultValue = "false", + category = "misc", + help = "Rewrites default and static methods in interfaces if --min_sdk_version < 24. This " + + "only works correctly if subclasses of rewritten interfaces as well as uses of static " + + "interface methods are run through this tool as well." + ) + public boolean desugarInterfaceMethodBodiesIfNeeded; + @Option( name = "copy_bridges_from_classpath", defaultValue = "false", @@ -167,8 +180,11 @@ class Desugar { private final Path dumpDirectory; private final CoreLibraryRewriter rewriter; private final LambdaClassMaker lambdas; + private final GeneratedClassStore store; + private final boolean outputJava7; private final boolean allowDefaultMethods; private final boolean allowCallsToObjectsNonNull; + /** An instance of Desugar is expected to be used ONLY ONCE */ private boolean used; @@ -177,7 +193,10 @@ class Desugar { this.dumpDirectory = dumpDirectory; this.rewriter = new CoreLibraryRewriter(options.coreLibrary ? "__desugar__/" : ""); this.lambdas = new LambdaClassMaker(dumpDirectory); - this.allowDefaultMethods = options.minSdkVersion >= 24; + this.store = new GeneratedClassStore(); + this.outputJava7 = options.minSdkVersion < 24; + this.allowDefaultMethods = + options.desugarInterfaceMethodBodiesIfNeeded || options.minSdkVersion >= 24; this.allowCallsToObjectsNonNull = options.minSdkVersion >= 19; this.used = false; } @@ -187,28 +206,30 @@ class Desugar { this.used = true; try (Closer closer = Closer.create()) { - IndexedInputs indexedClasspath = - new IndexedInputs(toRegisteredInputFileProvider(closer, options.classpath)); + IndexedInputs indexedBootclasspath = + new IndexedInputs(toRegisteredInputFileProvider(closer, options.bootclasspath)); // Use a classloader that as much as possible uses the provided bootclasspath instead of // the tool's system classloader. Unfortunately we can't do that for java. classes. ClassLoader bootclassloader = options.bootclasspath.isEmpty() ? new ThrowingClassLoader() : new HeaderClassLoader( - new IndexedInputs(toRegisteredInputFileProvider(closer, options.bootclasspath)), + indexedBootclasspath, rewriter, new ThrowingClassLoader()); + IndexedInputs indexedClasspath = + new IndexedInputs(toRegisteredInputFileProvider(closer, options.classpath)); // Process each input separately for (InputOutputPair inputOutputPair : toInputOutputPairs(options)) { - desugarOneInput(inputOutputPair, indexedClasspath, bootclassloader); + desugarOneInput(inputOutputPair, indexedClasspath, bootclassloader, + new ClassReaderFactory(indexedBootclasspath, rewriter)); } } } - private void desugarOneInput( - InputOutputPair inputOutputPair, IndexedInputs indexedClasspath, ClassLoader bootclassloader) - throws Exception { + private void desugarOneInput(InputOutputPair inputOutputPair, IndexedInputs indexedClasspath, + ClassLoader bootclassloader, ClassReaderFactory bootclasspathReader) throws Exception { Path inputPath = inputOutputPair.getInput(); Path outputPath = inputOutputPair.getOutput(); checkArgument( @@ -227,24 +248,32 @@ class Desugar { ClassLoader loader = new HeaderClassLoader(indexedClasspathAndInputFiles, rewriter, bootclassloader); - ClassReaderFactory readerFactory = - new ClassReaderFactory( - (options.copyBridgesFromClasspath && !allowDefaultMethods) - ? indexedClasspathAndInputFiles - : indexedInputFiles, - rewriter); + ClassReaderFactory classpathReader = null; + ClassReaderFactory bridgeMethodReader = null; + if (outputJava7) { + classpathReader = new ClassReaderFactory(indexedClasspathAndInputFiles, rewriter); + if (options.copyBridgesFromClasspath) { + bridgeMethodReader = classpathReader; + } else { + bridgeMethodReader = new ClassReaderFactory(indexedInputFiles, rewriter); + } + } ImmutableSet.Builder interfaceLambdaMethodCollector = ImmutableSet.builder(); - desugarClassesInInput( - inputFiles, outputFileProvider, loader, readerFactory, interfaceLambdaMethodCollector); + desugarClassesInInput(inputFiles, outputFileProvider, loader, classpathReader, + bootclasspathReader, interfaceLambdaMethodCollector); + + desugarAndWriteDumpedLambdaClassesToOutput(outputFileProvider, loader, classpathReader, + bootclasspathReader, interfaceLambdaMethodCollector.build(), bridgeMethodReader); - desugarAndWriteDumpedLambdaClassesToOutput( - outputFileProvider, loader, readerFactory, interfaceLambdaMethodCollector); + desugarAndWriteGeneratedClasses(outputFileProvider); } - ImmutableMap leftBehind = lambdas.drain(); - checkState(leftBehind.isEmpty(), "Didn't process %s", leftBehind); + ImmutableMap lambdasLeftBehind = lambdas.drain(); + checkState(lambdasLeftBehind.isEmpty(), "Didn't process %s", lambdasLeftBehind); + ImmutableMap generatedLeftBehind = store.drain(); + checkState(generatedLeftBehind.isEmpty(), "Didn't process %s", generatedLeftBehind.keySet()); } /** Desugar the classes that are in the inputs specified in the command line arguments. */ @@ -252,7 +281,8 @@ class Desugar { InputFileProvider inputFiles, OutputFileProvider outputFileProvider, ClassLoader loader, - ClassReaderFactory readerFactory, + @Nullable ClassReaderFactory classpathReader, + ClassReaderFactory bootclasspathReader, Builder interfaceLambdaMethodCollector) throws IOException { for (String filename : inputFiles) { @@ -262,11 +292,14 @@ class Desugar { // any danger of accidentally uncompressed resources ending up in an .apk. if (filename.endsWith(".class")) { ClassReader reader = rewriter.reader(content); - UnprefixingClassWriter writer = - rewriter.writer(ClassWriter.COMPUTE_MAXS /*for bridge methods*/); + UnprefixingClassWriter writer = rewriter.writer(ClassWriter.COMPUTE_MAXS); ClassVisitor visitor = createClassVisitorsForClassesInInputs( - loader, readerFactory, interfaceLambdaMethodCollector, writer); + loader, + classpathReader, + bootclasspathReader, + interfaceLambdaMethodCollector, + writer); reader.accept(visitor, 0); outputFileProvider.write(filename, writer.toByteArray()); @@ -284,10 +317,11 @@ class Desugar { private void desugarAndWriteDumpedLambdaClassesToOutput( OutputFileProvider outputFileProvider, ClassLoader loader, - ClassReaderFactory readerFactory, - Builder interfaceLambdaMethodCollector) + @Nullable ClassReaderFactory classpathReader, + ClassReaderFactory bootclasspathReader, + ImmutableSet interfaceLambdaMethods, + @Nullable ClassReaderFactory bridgeMethodReader) throws IOException { - ImmutableSet interfaceLambdaMethods = interfaceLambdaMethodCollector.build(); checkState( !allowDefaultMethods || interfaceLambdaMethods.isEmpty(), "Desugaring with default methods enabled moved interface lambdas"); @@ -307,7 +341,13 @@ class Desugar { rewriter.writer(ClassWriter.COMPUTE_MAXS /*for invoking bridges*/); ClassVisitor visitor = createClassVisitorsForDumpedLambdaClasses( - loader, readerFactory, interfaceLambdaMethods, lambdaClass.getValue(), writer); + loader, + classpathReader, + bootclasspathReader, + interfaceLambdaMethods, + bridgeMethodReader, + lambdaClass.getValue(), + writer); reader.accept(visitor, 0); String filename = rewriter.unprefix(lambdaClass.getValue().desiredInternalName()) + ".class"; @@ -316,26 +356,53 @@ class Desugar { } } + private void desugarAndWriteGeneratedClasses(OutputFileProvider outputFileProvider) + throws IOException { + // Write out any classes we generated along the way + ImmutableMap generatedClasses = store.drain(); + checkState(generatedClasses.isEmpty() || (allowDefaultMethods && outputJava7), + "Didn't expect generated classes but got %s", generatedClasses.keySet()); + for (Map.Entry generated : generatedClasses.entrySet()) { + UnprefixingClassWriter writer = rewriter.writer(ClassWriter.COMPUTE_MAXS); + // checkState above implies that we want Java 7 .class files, so send through that visitor. + // Don't need a ClassReaderFactory b/c static interface methods should've been moved. + ClassVisitor visitor = new Java7Compatibility(writer, (ClassReaderFactory) null); + generated.getValue().accept(visitor); + String filename = rewriter.unprefix(generated.getKey()) + ".class"; + outputFileProvider.write(filename, writer.toByteArray()); + } + } + /** * Create the class visitors for the lambda classes that are generated on the fly. If no new class * visitors are not generated, then the passed-in {@code writer} will be returned. */ private ClassVisitor createClassVisitorsForDumpedLambdaClasses( ClassLoader loader, - ClassReaderFactory readerFactory, + @Nullable ClassReaderFactory classpathReader, + ClassReaderFactory bootclasspathReader, ImmutableSet interfaceLambdaMethods, + @Nullable ClassReaderFactory bridgeMethodReader, LambdaInfo lambdaClass, UnprefixingClassWriter writer) { - ClassVisitor visitor = writer; + ClassVisitor visitor = checkNotNull(writer); - if (!allowDefaultMethods) { + if (outputJava7) { // null ClassReaderFactory b/c we don't expect to need it for lambda classes visitor = new Java7Compatibility(visitor, (ClassReaderFactory) null); + if (options.desugarInterfaceMethodBodiesIfNeeded) { + visitor = new DefaultMethodClassFixer(visitor, classpathReader, bootclasspathReader); + visitor = new InterfaceDesugaring(visitor, bootclasspathReader, store); + } } - visitor = new LambdaClassFixer( - visitor, lambdaClass, readerFactory, interfaceLambdaMethods, allowDefaultMethods); + visitor, + lambdaClass, + bridgeMethodReader, + interfaceLambdaMethods, + allowDefaultMethods, + outputJava7); // Send lambda classes through desugaring to make sure there's no invokedynamic // instructions in generated lambda classes (checkState below will fail) visitor = new LambdaDesugaring(visitor, loader, lambdas, null, allowDefaultMethods); @@ -357,17 +424,20 @@ class Desugar { */ private ClassVisitor createClassVisitorsForClassesInInputs( ClassLoader loader, - ClassReaderFactory readerFactory, + @Nullable ClassReaderFactory classpathReader, + ClassReaderFactory bootclasspathReader, Builder interfaceLambdaMethodCollector, UnprefixingClassWriter writer) { - checkArgument(writer != null, "The class writer cannot be null"); - ClassVisitor visitor = writer; + ClassVisitor visitor = checkNotNull(writer); if (!options.onlyDesugarJavac9ForLint) { - if (!allowDefaultMethods) { - visitor = new Java7Compatibility(visitor, readerFactory); + if (outputJava7) { + visitor = new Java7Compatibility(visitor, classpathReader); + if (options.desugarInterfaceMethodBodiesIfNeeded) { + visitor = new DefaultMethodClassFixer(visitor, classpathReader, bootclasspathReader); + visitor = new InterfaceDesugaring(visitor, bootclasspathReader, store); + } } - visitor = new LambdaDesugaring( visitor, loader, lambdas, interfaceLambdaMethodCollector, allowDefaultMethods); diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/GeneratedClassStore.java b/src/tools/android/java/com/google/devtools/build/android/desugar/GeneratedClassStore.java new file mode 100644 index 0000000000..bf376a45ba --- /dev/null +++ b/src/tools/android/java/com/google/devtools/build/android/desugar/GeneratedClassStore.java @@ -0,0 +1,49 @@ +// 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.checkState; + +import com.google.common.collect.ImmutableMap; +import java.util.LinkedHashMap; +import java.util.Map; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.tree.ClassNode; + +/** + * Simple wrapper around a map that holds generated classes so they can be processed later. + */ +class GeneratedClassStore { + + /** Map from internal names to generated classes with deterministic iteration order. */ + private final Map classes = new LinkedHashMap<>(); + + /** + * Adds a class for the given internal name. It's the caller's responsibility to {@link + * ClassVisitor#visit} the returned object to initialize the desired class, and to avoid + * confusion, this method throws if the class had already been present. + */ + public ClassVisitor add(String internalClassName) { + ClassNode result = new ClassNode(); + checkState( + classes.put(internalClassName, result) == null, "Already present: %s", internalClassName); + return result; + } + + public ImmutableMap drain() { + ImmutableMap result = ImmutableMap.copyOf(classes); + classes.clear(); + return result; + } +} 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 new file mode 100644 index 0000000000..b2cf540646 --- /dev/null +++ b/src/tools/android/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java @@ -0,0 +1,312 @@ +// 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.checkState; + +import javax.annotation.Nullable; +import org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.TypePath; + +/** + * Visitor that moves methods with bodies from interfaces into a companion class and rewrites + * call sites accordingly (which is only needed for static interface methods). Default methods + * are kept as abstract methods with all their annotations. + * + *

Any necessary companion classes will be added to the given {@link GeneratedClassStore}. It's + * the caller's responsibility to write those out. + * + *

Relies on {@link DefaultMethodClassFixer} to stub in method bodies for moved default methods. + * Assumes that lambdas are already desugared. Ignores bridge methods, which are handled specially. + */ +class InterfaceDesugaring extends ClassVisitor { + + static final String COMPANION_SUFFIX = "$$CC"; + + private final ClassReaderFactory bootclasspath; + private final GeneratedClassStore store; + + private String internalName; + private int bytecodeVersion; + private int accessFlags; + @Nullable private ClassVisitor companion; + + public InterfaceDesugaring(ClassVisitor dest, ClassReaderFactory bootclasspath, + GeneratedClassStore store) { + super(Opcodes.ASM5, dest); + this.bootclasspath = bootclasspath; + this.store = store; + } + + @Override + public void visit( + int version, + int access, + String name, + String signature, + String superName, + String[] interfaces) { + companion = null; + internalName = name; + bytecodeVersion = version; + accessFlags = access; + super.visit(version, access, name, signature, superName, interfaces); + } + + @Override + public void visitEnd() { + if (companion != null) { + companion.visitEnd(); + } + super.visitEnd(); + } + + @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) + && !"".equals(name)) { + checkArgument(BitFlags.noneSet(access, Opcodes.ACC_NATIVE), "Forbidden per JLS ch 9.4"); + + boolean isLambdaBody = + name.startsWith("lambda$") && BitFlags.isSet(access, Opcodes.ACC_SYNTHETIC); + if (isLambdaBody) { + access &= ~Opcodes.ACC_PUBLIC; // undo visibility change from LambdaDesugaring + // Rename lambda method to reflect the new owner. Not doing so confuses LambdaDesugaring + // if it's run over this class again. + name += COMPANION_SUFFIX; + } + if (BitFlags.isSet(access, Opcodes.ACC_STATIC)) { + // Completely move static interface methods, which requires rewriting call sites + result = + companion() + .visitMethod(access & ~Opcodes.ACC_PRIVATE, name, desc, signature, exceptions); + } else { + MethodVisitor abstractDest; + if (isLambdaBody) { + // Completely move lambda bodies, which requires rewriting call sites + access &= ~Opcodes.ACC_PRIVATE; + abstractDest = null; + } else { + // Make default methods abstract but move their implementation into a static method with + // corresponding signature. Doesn't require callsite rewriting but implementing classes + // may need to implement default methods explicitly. + checkArgument(BitFlags.noneSet(access, Opcodes.ACC_PRIVATE), + "Unexpected private interface method %s.%s : %s", name, internalName, desc); + abstractDest = super.visitMethod( + access | Opcodes.ACC_ABSTRACT, name, desc, signature, exceptions); + } + + // TODO(b/37110951): adjust signature with explicit receiver type, which may be generic + MethodVisitor codeDest = + companion() + .visitMethod( + access | Opcodes.ACC_STATIC, + name, + companionDefaultMethodDescriptor(internalName, desc), + (String) null, // drop signature, since given one doesn't include the new param + exceptions); + + result = abstractDest != null ? new MultiplexAnnotations(codeDest, abstractDest) : codeDest; + } + } else { + result = super.visitMethod(access, name, desc, signature, exceptions); + } + return result != null ? new InterfaceInvocationRewriter(result) : null; + } + + /** + * 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 + * receiver type. + */ + static String companionDefaultMethodDescriptor(String interfaceName, String desc) { + Type type = Type.getMethodType(desc); + Type[] companionArgs = new Type[type.getArgumentTypes().length + 1]; + companionArgs[0] = Type.getObjectType(interfaceName); + System.arraycopy(type.getArgumentTypes(), 0, companionArgs, 1, type.getArgumentTypes().length); + return Type.getMethodDescriptor(type.getReturnType(), companionArgs); + } + + private ClassVisitor companion() { + if (companion == null) { + checkState(BitFlags.isSet(accessFlags, Opcodes.ACC_INTERFACE)); + String companionName = internalName + COMPANION_SUFFIX; + + companion = store.add(companionName); + companion.visit( + bytecodeVersion, + (accessFlags | Opcodes.ACC_SYNTHETIC) & ~Opcodes.ACC_INTERFACE, + companionName, + (String) null, // signature + "java/lang/Object", + new String[0]); + } + return companion; + } + + /** + * 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 devices without those methods). + */ + private class InterfaceInvocationRewriter extends MethodVisitor { + + public InterfaceInvocationRewriter(MethodVisitor dest) { + super(Opcodes.ASM5, dest); + } + + @Override + public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) { + // Assume that any static interface methods on the classpath are moved + if (itf) { + if (name.startsWith("lambda$")) { + // Redirect lambda invocations to completely remove all lambda methods from interfaces. + checkArgument(!owner.endsWith(COMPANION_SUFFIX), + "%s shouldn't consider %s an interface", internalName, owner); + checkArgument(!bootclasspath.isKnown(owner)); // must be in current input + if (opcode == Opcodes.INVOKEINTERFACE) { + opcode = Opcodes.INVOKESTATIC; + desc = companionDefaultMethodDescriptor(owner, desc); + } else { + checkArgument(opcode == Opcodes.INVOKESTATIC, + "Unexpected opcode %s to invoke %s.%s", opcode, owner, name); + } + // Reflect that InterfaceDesugaring moves and renames the lambda body method + owner += COMPANION_SUFFIX; + name += COMPANION_SUFFIX; + checkState(name.equals(LambdaDesugaring.uniqueInPackage(owner, name)), + "Unexpected lambda body method name %s for %s", name, owner); + itf = false; + } else if ((opcode == Opcodes.INVOKESTATIC || opcode == Opcodes.INVOKESPECIAL) + && !bootclasspath.isKnown(owner)) { + checkArgument(!owner.endsWith(COMPANION_SUFFIX), + "%s shouldn't consider %s an interface", internalName, owner); + if (opcode == Opcodes.INVOKESPECIAL) { + // Turn Interface.super.m() into Interface$$CC.m(receiver) + opcode = Opcodes.INVOKESTATIC; + desc = companionDefaultMethodDescriptor(owner, desc); + } + owner += COMPANION_SUFFIX; + itf = false; + } + } + super.visitMethodInsn(opcode, owner, name, desc, itf); + } + } + + /** + * Method visitor that behaves like a passthrough but additionally duplicates all annotations + * into a second given {@link MethodVisitor}. + */ + private static class MultiplexAnnotations extends MethodVisitor { + + private final MethodVisitor annotationOnlyDest; + + public MultiplexAnnotations(@Nullable MethodVisitor dest, MethodVisitor annotationOnlyDest) { + super(Opcodes.ASM5, dest); + this.annotationOnlyDest = annotationOnlyDest; + } + + @Override + public void visitParameter(String name, int access) { + super.visitParameter(name, access); + annotationOnlyDest.visitParameter(name, access); + } + + @Override + public AnnotationVisitor visitTypeAnnotation( + int typeRef, TypePath typePath, String desc, boolean visible) { + AnnotationVisitor dest = super.visitTypeAnnotation(typeRef, typePath, desc, visible); + AnnotationVisitor annoDest = + annotationOnlyDest.visitTypeAnnotation(typeRef, typePath, desc, visible); + return new MultiplexAnnotationVisitor(dest, annoDest); + } + + @Override + public AnnotationVisitor visitParameterAnnotation(int parameter, String desc, boolean visible) { + AnnotationVisitor dest = super.visitParameterAnnotation(parameter, desc, visible); + AnnotationVisitor annoDest = + annotationOnlyDest.visitParameterAnnotation(parameter, desc, visible); + return new MultiplexAnnotationVisitor(dest, annoDest); + } + } + + /** + * Annotation visitor that recursively passes the visited annotations to any number of given + * {@link AnnotationVisitor}s. + */ + private static class MultiplexAnnotationVisitor extends AnnotationVisitor { + + private final AnnotationVisitor[] moreDestinations; + + public MultiplexAnnotationVisitor(@Nullable AnnotationVisitor dest, + AnnotationVisitor... moreDestinations) { + super(Opcodes.ASM5, dest); + this.moreDestinations = moreDestinations; + } + + @Override + public void visit(String name, Object value) { + super.visit(name, value); + for (AnnotationVisitor dest : moreDestinations) { + dest.visit(name, value); + } + } + + @Override + public void visitEnum(String name, String desc, String value) { + super.visitEnum(name, desc, value); + for (AnnotationVisitor dest : moreDestinations) { + dest.visitEnum(name, desc, value); + } + } + + @Override + public AnnotationVisitor visitAnnotation(String name, String desc) { + AnnotationVisitor[] subVisitors = new AnnotationVisitor[moreDestinations.length]; + AnnotationVisitor dest = super.visitAnnotation(name, desc); + for (int i = 0; i < subVisitors.length; ++i) { + subVisitors[i] = moreDestinations[i].visitAnnotation(name, desc); + } + return new MultiplexAnnotationVisitor(dest, subVisitors); + } + + @Override + public AnnotationVisitor visitArray(String name) { + AnnotationVisitor[] subVisitors = new AnnotationVisitor[moreDestinations.length]; + AnnotationVisitor dest = super.visitArray(name); + for (int i = 0; i < subVisitors.length; ++i) { + subVisitors[i] = moreDestinations[i].visitArray(name); + } + return new MultiplexAnnotationVisitor(dest, subVisitors); + } + + @Override + public void visitEnd() { + super.visitEnd(); + for (AnnotationVisitor dest : moreDestinations) { + dest.visitEnd(); + } + } + } +} diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/Java7Compatibility.java b/src/tools/android/java/com/google/devtools/build/android/desugar/Java7Compatibility.java index cc3fe14ff2..55f82b3032 100644 --- a/src/tools/android/java/com/google/devtools/build/android/desugar/Java7Compatibility.java +++ b/src/tools/android/java/com/google/devtools/build/android/desugar/Java7Compatibility.java @@ -27,7 +27,7 @@ import org.objectweb.asm.Opcodes; import org.objectweb.asm.TypePath; /** - * Visitor that ensures bytecode version <= 51 (Java 7) and that throws if it default or static + * Visitor that ensures bytecode version <= 51 (Java 7) and that throws if it sees default or static * interface methods (i.e., non-abstract interface methods), which don't exist in Java 7. */ public class Java7Compatibility extends ClassVisitor { diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/LambdaClassFixer.java b/src/tools/android/java/com/google/devtools/build/android/desugar/LambdaClassFixer.java index d4726d346a..5e50fc87f6 100644 --- a/src/tools/android/java/com/google/devtools/build/android/desugar/LambdaClassFixer.java +++ b/src/tools/android/java/com/google/devtools/build/android/desugar/LambdaClassFixer.java @@ -49,6 +49,7 @@ class LambdaClassFixer extends ClassVisitor { private final ClassReaderFactory factory; private final ImmutableSet interfaceLambdaMethods; private final boolean allowDefaultMethods; + private final boolean copyBridgeMethods; private final HashSet implementedMethods = new HashSet<>(); private final LinkedHashSet methodsToMoveIn = new LinkedHashSet<>(); @@ -62,12 +63,16 @@ class LambdaClassFixer extends ClassVisitor { private String signature; public LambdaClassFixer(ClassVisitor dest, LambdaInfo lambdaInfo, ClassReaderFactory factory, - ImmutableSet interfaceLambdaMethods, boolean allowDefaultMethods) { + ImmutableSet interfaceLambdaMethods, boolean allowDefaultMethods, + boolean copyBridgeMethods) { super(Opcodes.ASM5, dest); + checkArgument(!allowDefaultMethods || interfaceLambdaMethods.isEmpty()); + checkArgument(allowDefaultMethods || copyBridgeMethods); this.lambdaInfo = lambdaInfo; this.factory = factory; this.interfaceLambdaMethods = interfaceLambdaMethods; this.allowDefaultMethods = allowDefaultMethods; + this.copyBridgeMethods = copyBridgeMethods; } @Override @@ -180,7 +185,7 @@ class LambdaClassFixer extends ClassVisitor { } copyRewrittenLambdaMethods(); - if (!allowDefaultMethods) { + if (copyBridgeMethods) { copyBridgeMethods(interfaces); } super.visitEnd(); @@ -327,8 +332,9 @@ class LambdaClassFixer extends ClassVisitor { // Only copy bridge methods--hand-written default methods are not supported--and only if // we haven't seen the method already. if (implementedMethods.add(name + ":" + desc)) { - return new AvoidJacocoInit( - LambdaClassFixer.super.visitMethod(access, name, desc, signature, exceptions)); + MethodVisitor result = + LambdaClassFixer.super.visitMethod(access, name, desc, signature, exceptions); + return allowDefaultMethods ? result : new AvoidJacocoInit(result); } } return null; @@ -348,6 +354,7 @@ class LambdaClassFixer extends ClassVisitor { public CopyOneMethod(String methodName) { // No delegate visitor; instead we'll add methods to the outer class's delegate where needed super(Opcodes.ASM5); + checkState(!allowDefaultMethods, "Couldn't copy interface lambda bodies"); this.methodName = methodName; } -- cgit v1.2.3