diff options
Diffstat (limited to 'java/com/google/devtools/build')
9 files changed, 1291 insertions, 63 deletions
diff --git a/java/com/google/devtools/build/android/desugar/ClassReaderFactory.java b/java/com/google/devtools/build/android/desugar/ClassReaderFactory.java index 2b44d76..bae5251 100644 --- a/java/com/google/devtools/build/android/desugar/ClassReaderFactory.java +++ b/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/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java b/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java new file mode 100644 index 0000000..8ad5dc2 --- /dev/null +++ b/java/com/google/devtools/build/android/desugar/DefaultMethodClassFixer.java @@ -0,0 +1,300 @@ +// 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<String> instanceMethods = new HashSet<>(); + private final HashSet<String> seenInterfaces = new HashSet<>(); + + private boolean isInterface; + private ImmutableList<String> 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 && defaultMethodsDefined(interfaces)) { + // 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) { + 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), + "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; + } + + private void stubMissingDefaultMethods(ImmutableList<String> 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<String> 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.contains(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 + } + } + + /** + * Visitor for interfaces that recursively searches interfaces for default method declarations. + */ + public 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 (BitFlags.noneSet(access, Opcodes.ACC_ABSTRACT | Opcodes.ACC_STATIC | Opcodes.ACC_BRIDGE) + && !instanceMethods.contains(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)); + 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/java/com/google/devtools/build/android/desugar/Desugar.java b/java/com/google/devtools/build/android/desugar/Desugar.java index 797cbc4..c4528ae 100644 --- a/java/com/google/devtools/build/android/desugar/Desugar.java +++ b/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; @@ -22,6 +23,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet.Builder; +import com.google.common.io.ByteStreams; import com.google.common.io.Closer; import com.google.devtools.build.android.Converters.ExistingPathConverter; import com.google.devtools.build.android.Converters.PathConverter; @@ -31,6 +33,7 @@ import com.google.devtools.common.options.OptionsBase; import com.google.devtools.common.options.OptionsParser; import com.google.devtools.common.options.OptionsParser.OptionUsageRestrictions; import com.google.errorprone.annotations.MustBeClosed; +import java.io.IOError; import java.io.IOException; import java.io.InputStream; import java.nio.file.FileVisitResult; @@ -42,9 +45,12 @@ import java.nio.file.attribute.BasicFileAttributes; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +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 @@ -146,6 +152,25 @@ class Desugar { public int minSdkVersion; @Option( + name = "desugar_interface_method_bodies_if_needed", + defaultValue = "true", + 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 = "desugar_try_with_resources_if_needed", + defaultValue = "false", + category = "misc", + help = "Rewrites try-with-resources statements if --min_sdk_version < 19." + ) + public boolean desugarTryWithResourcesIfNeeded; + + @Option( name = "copy_bridges_from_classpath", defaultValue = "false", category = "misc", @@ -167,6 +192,11 @@ class Desugar { private final Path dumpDirectory; private final CoreLibraryRewriter rewriter; private final LambdaClassMaker lambdas; + private final GeneratedClassStore store; + /** The counter to record the times of try-with-resources desugaring is invoked. */ + private final AtomicInteger numOfTryWithResourcesInvoked = new AtomicInteger(); + + private final boolean outputJava7; private final boolean allowDefaultMethods; private final boolean allowCallsToObjectsNonNull; /** An instance of Desugar is expected to be used ONLY ONCE */ @@ -177,7 +207,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,27 +220,33 @@ 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)), - rewriter, - new ThrowingClassLoader()); + : new HeaderClassLoader(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) + InputOutputPair inputOutputPair, + IndexedInputs indexedClasspath, + ClassLoader bootclassloader, + ClassReaderFactory bootclasspathReader) throws Exception { Path inputPath = inputOutputPair.getInput(); Path outputPath = inputOutputPair.getOutput(); @@ -227,24 +266,62 @@ 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<String> interfaceLambdaMethodCollector = ImmutableSet.builder(); desugarClassesInInput( - inputFiles, outputFileProvider, loader, readerFactory, interfaceLambdaMethodCollector); + inputFiles, + outputFileProvider, + loader, + classpathReader, + bootclasspathReader, + interfaceLambdaMethodCollector); desugarAndWriteDumpedLambdaClassesToOutput( - outputFileProvider, loader, readerFactory, interfaceLambdaMethodCollector); + outputFileProvider, + loader, + classpathReader, + bootclasspathReader, + interfaceLambdaMethodCollector.build(), + bridgeMethodReader); + + desugarAndWriteGeneratedClasses(outputFileProvider); + copyThrowableExtensionClass(outputFileProvider); } - ImmutableMap<Path, LambdaInfo> leftBehind = lambdas.drain(); - checkState(leftBehind.isEmpty(), "Didn't process %s", leftBehind); + ImmutableMap<Path, LambdaInfo> lambdasLeftBehind = lambdas.drain(); + checkState(lambdasLeftBehind.isEmpty(), "Didn't process %s", lambdasLeftBehind); + ImmutableMap<String, ClassNode> generatedLeftBehind = store.drain(); + checkState(generatedLeftBehind.isEmpty(), "Didn't process %s", generatedLeftBehind.keySet()); + } + + private void copyThrowableExtensionClass(OutputFileProvider outputFileProvider) { + if (!outputJava7 || !options.desugarTryWithResourcesIfNeeded) { + // try-with-resources statements are okay in the output jar. + return; + } + if (this.numOfTryWithResourcesInvoked.get() <= 0) { + // the try-with-resources desugaring pass does nothing, so no need to copy these class files. + return; + } + for (String className : + TryWithResourcesRewriter.THROWABLE_EXT_CLASS_INTERNAL_NAMES_WITH_CLASS_EXT) { + try (InputStream stream = Desugar.class.getClassLoader().getResourceAsStream(className)) { + outputFileProvider.write(className, ByteStreams.toByteArray(stream)); + } catch (IOException e) { + throw new IOError(e); + } + } } /** Desugar the classes that are in the inputs specified in the command line arguments. */ @@ -252,7 +329,8 @@ class Desugar { InputFileProvider inputFiles, OutputFileProvider outputFileProvider, ClassLoader loader, - ClassReaderFactory readerFactory, + @Nullable ClassReaderFactory classpathReader, + ClassReaderFactory bootclasspathReader, Builder<String> interfaceLambdaMethodCollector) throws IOException { for (String filename : inputFiles) { @@ -262,11 +340,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 +365,11 @@ class Desugar { private void desugarAndWriteDumpedLambdaClassesToOutput( OutputFileProvider outputFileProvider, ClassLoader loader, - ClassReaderFactory readerFactory, - Builder<String> interfaceLambdaMethodCollector) + @Nullable ClassReaderFactory classpathReader, + ClassReaderFactory bootclasspathReader, + ImmutableSet<String> interfaceLambdaMethods, + @Nullable ClassReaderFactory bridgeMethodReader) throws IOException { - ImmutableSet<String> interfaceLambdaMethods = interfaceLambdaMethodCollector.build(); checkState( !allowDefaultMethods || interfaceLambdaMethods.isEmpty(), "Desugaring with default methods enabled moved interface lambdas"); @@ -307,7 +389,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 +404,58 @@ class Desugar { } } + private void desugarAndWriteGeneratedClasses(OutputFileProvider outputFileProvider) + throws IOException { + // Write out any classes we generated along the way + ImmutableMap<String, ClassNode> generatedClasses = store.drain(); + checkState( + generatedClasses.isEmpty() || (allowDefaultMethods && outputJava7), + "Didn't expect generated classes but got %s", + generatedClasses.keySet()); + for (Map.Entry<String, ClassNode> 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<String> 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.desugarTryWithResourcesIfNeeded) { + visitor = new TryWithResourcesRewriter(visitor, loader, numOfTryWithResourcesInvoked); + } + 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 +477,23 @@ class Desugar { */ private ClassVisitor createClassVisitorsForClassesInInputs( ClassLoader loader, - ClassReaderFactory readerFactory, + @Nullable ClassReaderFactory classpathReader, + ClassReaderFactory bootclasspathReader, Builder<String> 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.desugarTryWithResourcesIfNeeded) { + visitor = new TryWithResourcesRewriter(visitor, loader, numOfTryWithResourcesInvoked); + } + if (options.desugarInterfaceMethodBodiesIfNeeded) { + visitor = new DefaultMethodClassFixer(visitor, classpathReader, bootclasspathReader); + visitor = new InterfaceDesugaring(visitor, bootclasspathReader, store); + } } - visitor = new LambdaDesugaring( visitor, loader, lambdas, interfaceLambdaMethodCollector, allowDefaultMethods); @@ -382,7 +508,6 @@ class Desugar { return visitor; } - public static void main(String[] args) throws Exception { // It is important that this method is called first. See its javadoc. Path dumpDirectory = createAndRegisterLambdaDumpDirectory(); @@ -440,7 +565,8 @@ class Desugar { final ImmutableList.Builder<InputOutputPair> ioPairListbuilder = ImmutableList.builder(); for (Iterator<Path> inputIt = options.inputJars.iterator(), outputIt = options.outputJars.iterator(); - inputIt.hasNext();) { + inputIt.hasNext(); + ) { ioPairListbuilder.add(InputOutputPair.create(inputIt.next(), outputIt.next())); } return ioPairListbuilder.build(); @@ -498,8 +624,7 @@ class Desugar { /** Transform a Path to an {@link OutputFileProvider} */ @MustBeClosed - private static OutputFileProvider toOutputFileProvider(Path path) - throws IOException { + private static OutputFileProvider toOutputFileProvider(Path path) throws IOException { if (Files.isDirectory(path)) { return new DirectoryOutputFileProvider(path); } else { @@ -509,8 +634,7 @@ class Desugar { /** Transform a Path to an InputFileProvider that needs to be closed by the caller. */ @MustBeClosed - private static InputFileProvider toInputFileProvider(Path path) - throws IOException { + private static InputFileProvider toInputFileProvider(Path path) throws IOException { if (Files.isDirectory(path)) { return new DirectoryInputFileProvider(path); } else { @@ -532,9 +656,7 @@ class Desugar { return builder.build(); } - /** - * Pair input and output. - */ + /** Pair input and output. */ @AutoValue abstract static class InputOutputPair { diff --git a/java/com/google/devtools/build/android/desugar/GeneratedClassStore.java b/java/com/google/devtools/build/android/desugar/GeneratedClassStore.java new file mode 100644 index 0000000..bf376a4 --- /dev/null +++ b/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<String, ClassNode> 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<String, ClassNode> drain() { + ImmutableMap<String, ClassNode> result = ImmutableMap.copyOf(classes); + classes.clear(); + return result; + } +} diff --git a/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java b/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java new file mode 100644 index 0000000..974c90b --- /dev/null +++ b/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java @@ -0,0 +1,313 @@ +// 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. + * + * <p>Any necessary companion classes will be added to the given {@link GeneratedClassStore}. It's + * the caller's responsibility to write those out. + * + * <p>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) + && !"<clinit>".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, + // Companion class must be public so moved methods can be called from anywhere + (accessFlags | Opcodes.ACC_SYNTHETIC | Opcodes.ACC_PUBLIC) & ~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/java/com/google/devtools/build/android/desugar/Java7Compatibility.java b/java/com/google/devtools/build/android/desugar/Java7Compatibility.java index cc3fe14..55f82b3 100644 --- a/java/com/google/devtools/build/android/desugar/Java7Compatibility.java +++ b/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/java/com/google/devtools/build/android/desugar/LambdaClassFixer.java b/java/com/google/devtools/build/android/desugar/LambdaClassFixer.java index dea6339..5e50fc8 100644 --- a/java/com/google/devtools/build/android/desugar/LambdaClassFixer.java +++ b/java/com/google/devtools/build/android/desugar/LambdaClassFixer.java @@ -49,6 +49,7 @@ class LambdaClassFixer extends ClassVisitor { private final ClassReaderFactory factory; private final ImmutableSet<String> interfaceLambdaMethods; private final boolean allowDefaultMethods; + private final boolean copyBridgeMethods; private final HashSet<String> implementedMethods = new HashSet<>(); private final LinkedHashSet<String> methodsToMoveIn = new LinkedHashSet<>(); @@ -61,14 +62,17 @@ class LambdaClassFixer extends ClassVisitor { private String desc; private String signature; - public LambdaClassFixer(ClassVisitor dest, LambdaInfo lambdaInfo, ClassReaderFactory factory, - ImmutableSet<String> interfaceLambdaMethods, boolean allowDefaultMethods) { + ImmutableSet<String> 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 @@ -181,7 +185,7 @@ class LambdaClassFixer extends ClassVisitor { } copyRewrittenLambdaMethods(); - if (!allowDefaultMethods) { + if (copyBridgeMethods) { copyBridgeMethods(interfaces); } super.visitEnd(); @@ -200,6 +204,7 @@ class LambdaClassFixer extends ClassVisitor { CopyOneMethod copier = new CopyOneMethod(methodName); // TODO(kmb): Set source file attribute for lambda classes so lambda debug info makes sense bytecode.accept(copier, ClassReader.SKIP_DEBUG); + checkState(copier.copied(), "Didn't find %s", rewritten); } } @@ -232,15 +237,16 @@ class LambdaClassFixer extends ClassVisitor { owner = getInternalName(); itf = false; // owner was interface but is now a class methodsToMoveIn.add(method); - } else { - if (originalInternalName.equals(owner)) { - // Reflect renaming of lambda classes - owner = getInternalName(); - } - if (name.startsWith("lambda$")) { - // Reflect renaming of lambda$ instance methods to avoid accidental overrides - name = LambdaDesugaring.uniqueInPackage(owner, name); - } + } else if (originalInternalName.equals(owner)) { + // Reflect renaming of lambda classes + owner = getInternalName(); + } + + if (name.startsWith("lambda$")) { + // Reflect renaming of lambda$ instance methods in LambdaDesugaring. Do this even if we'll + // move the method into the lambda class we're processing so the renaming done in + // LambdaDesugaring doesn't kick in if the class were desugared a second time. + name = LambdaDesugaring.uniqueInPackage(owner, name); } super.visitMethodInsn(opcode, owner, name, desc, itf); } @@ -326,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; @@ -347,9 +354,14 @@ 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; } + public boolean copied() { + return copied > 0; + } + @Override public void visit( int version, @@ -367,6 +379,8 @@ class LambdaClassFixer extends ClassVisitor { if (name.equals(methodName)) { checkState(copied == 0, "Found unexpected second method %s with descriptor %s", name, desc); ++copied; + // Rename for consistency with what we do in LambdaClassMethodRewriter + name = LambdaDesugaring.uniqueInPackage(getInternalName(), name); return new AvoidJacocoInit( LambdaClassFixer.super.visitMethod(access, name, desc, signature, exceptions)); } diff --git a/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriter.java b/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriter.java new file mode 100644 index 0000000..2429d2f --- /dev/null +++ b/java/com/google/devtools/build/android/desugar/TryWithResourcesRewriter.java @@ -0,0 +1,146 @@ +// 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 org.objectweb.asm.Opcodes.ASM5; +import static org.objectweb.asm.Opcodes.INVOKESTATIC; +import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL; + +import com.google.common.base.Function; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableMultimap; +import com.google.common.collect.ImmutableSet; +import java.util.concurrent.atomic.AtomicInteger; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; + +/** + * Desugar try-with-resources. This class visitor intercepts calls to the following methods, and + * redirect them to ThrowableExtension. + * <li>{@code Throwable.addSuppressed(Throwable)} + * <li>{@code Throwable.getSuppressed()} + * <li>{@code Throwable.printStackTrace()} + * <li>{@code Throwable.printStackTrace(PrintStream)} + * <li>{@code Throwable.printStackTrace(PringWriter)} + */ +public class TryWithResourcesRewriter extends ClassVisitor { + + private static final String RUNTIME_PACKAGE_INTERNAL_NAME = + "com/google/devtools/build/android/desugar/runtime"; + + static final String THROWABLE_EXTENSION_INTERNAL_NAME = + RUNTIME_PACKAGE_INTERNAL_NAME + '/' + "ThrowableExtension"; + + /** The extension classes for java.lang.Throwable. */ + static final ImmutableSet<String> THROWABLE_EXT_CLASS_INTERNAL_NAMES = + ImmutableSet.of( + THROWABLE_EXTENSION_INTERNAL_NAME, + THROWABLE_EXTENSION_INTERNAL_NAME + "$AbstractDesugaringStrategy", + THROWABLE_EXTENSION_INTERNAL_NAME + "$MimicDesugaringStrategy", + THROWABLE_EXTENSION_INTERNAL_NAME + "$NullDesugaringStrategy", + THROWABLE_EXTENSION_INTERNAL_NAME + "$ReuseDesugaringStrategy"); + + /** The extension classes for java.lang.Throwable. All the names end with ".class" */ + static final ImmutableSet<String> THROWABLE_EXT_CLASS_INTERNAL_NAMES_WITH_CLASS_EXT = + FluentIterable.from(THROWABLE_EXT_CLASS_INTERNAL_NAMES) + .transform( + new Function<String, String>() { + @Override + public String apply(String s) { + return s + ".class"; + } + }) + .toSet(); + + static final ImmutableMultimap<String, String> TARGET_METHODS = + ImmutableMultimap.<String, String>builder() + .put("addSuppressed", "(Ljava/lang/Throwable;)V") + .put("getSuppressed", "()[Ljava/lang/Throwable;") + .put("printStackTrace", "()V") + .put("printStackTrace", "(Ljava/io/PrintStream;)V") + .put("printStackTrace", "(Ljava/io/PrintWriter;)V") + .build(); + + static final ImmutableMap<String, String> METHOD_DESC_MAP = + ImmutableMap.<String, String>builder() + .put("(Ljava/lang/Throwable;)V", "(Ljava/lang/Throwable;Ljava/lang/Throwable;)V") + .put("()[Ljava/lang/Throwable;", "(Ljava/lang/Throwable;)[Ljava/lang/Throwable;") + .put("()V", "(Ljava/lang/Throwable;)V") + .put("(Ljava/io/PrintStream;)V", "(Ljava/lang/Throwable;Ljava/io/PrintStream;)V") + .put("(Ljava/io/PrintWriter;)V", "(Ljava/lang/Throwable;Ljava/io/PrintWriter;)V") + .build(); + + private final ClassLoader classLoader; + + private final AtomicInteger numOfTryWithResourcesInvoked; + + public TryWithResourcesRewriter( + ClassVisitor classVisitor, + ClassLoader classLoader, + AtomicInteger numOfTryWithResourcesInvoked) { + super(ASM5, classVisitor); + this.classLoader = classLoader; + this.numOfTryWithResourcesInvoked = numOfTryWithResourcesInvoked; + } + + @Override + public MethodVisitor visitMethod( + int access, String name, String desc, String signature, String[] exceptions) { + MethodVisitor visitor = super.cv.visitMethod(access, name, desc, signature, exceptions); + return visitor == null || THROWABLE_EXT_CLASS_INTERNAL_NAMES.contains(name) + ? visitor + : new TryWithResourceVisitor(visitor, classLoader); + } + + private class TryWithResourceVisitor extends MethodVisitor { + + private final ClassLoader classLoader; + + public TryWithResourceVisitor(MethodVisitor methodVisitor, ClassLoader classLoader) { + super(ASM5, methodVisitor); + this.classLoader = classLoader; + } + + @Override + public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) { + if (!isMethodCallTargeted(opcode, owner, name, desc)) { + super.visitMethodInsn(opcode, owner, name, desc, itf); + return; + } + numOfTryWithResourcesInvoked.incrementAndGet(); + super.visitMethodInsn( + INVOKESTATIC, THROWABLE_EXTENSION_INTERNAL_NAME, name, METHOD_DESC_MAP.get(desc), false); + } + + private boolean isMethodCallTargeted(int opcode, String owner, String name, String desc) { + if (opcode != INVOKEVIRTUAL) { + return false; + } + if (!TARGET_METHODS.containsEntry(name, desc)) { + return false; + } + if (owner.equals("java/lang/Throwable")) { + return true; // early return, for performance. + } + try { + Class<?> throwableClass = classLoader.loadClass("java.lang.Throwable"); + Class<?> klass = classLoader.loadClass(owner.replace('/', '.')); + return throwableClass.isAssignableFrom(klass); + } catch (ClassNotFoundException e) { + throw new AssertionError(e); + } + } + } +} diff --git a/java/com/google/devtools/build/android/desugar/runtime/ThrowableExtension.java b/java/com/google/devtools/build/android/desugar/runtime/ThrowableExtension.java new file mode 100644 index 0000000..3581fe8 --- /dev/null +++ b/java/com/google/devtools/build/android/desugar/runtime/ThrowableExtension.java @@ -0,0 +1,276 @@ +// 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.runtime; + +import java.io.PrintStream; +import java.io.PrintWriter; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; +import java.util.WeakHashMap; + +/** + * This is an extension class for java.lang.Throwable. It emulates the methods + * addSuppressed(Throwable) and getSuppressed(), so the language feature try-with-resources can be + * used on Android devices whose API level is below 19. + * + * <p>Note that the Desugar should avoid desugaring this class. + */ +public class ThrowableExtension { + + static final AbstractDesugaringStrategy STRATEGY; + /** + * This property allows users to change the desugared behavior of try-with-resources at runtime. + * If its value is {@code true}, then {@link MimicDesugaringStrategy} will NOT be used, and {@link + * NullDesugaringStrategy} is used instead. + * + * <p>Note: this property is ONLY used when the API level on the device is below 19. + */ + public static final String SYSTEM_PROPERTY_TWR_DISABLE_MIMIC = + "com.google.devtools.build.android.desugar.runtime.twr_disable_mimic"; + + static { + AbstractDesugaringStrategy strategy; + try { + Integer apiLevel = readApiLevelFromBuildVersion(); + if (apiLevel != null && apiLevel.intValue() >= 19) { + strategy = new ReuseDesugaringStrategy(); + } else if (useMimicStrategy()) { + strategy = new MimicDesugaringStrategy(); + } else { + strategy = new NullDesugaringStrategy(); + } + } catch (Throwable e) { + // This catchall block is intentionally created to avoid anything unexpected, so that + // the desugared app will continue running in case of exceptions. + System.err.println( + "An error has occured when initializing the try-with-resources desuguring strategy. " + + "The default strategy " + + NullDesugaringStrategy.class.getName() + + "will be used. The error is: "); + e.printStackTrace(System.err); + strategy = new NullDesugaringStrategy(); + } + STRATEGY = strategy; + } + + public static AbstractDesugaringStrategy getStrategy() { + return STRATEGY; + } + + public static void addSuppressed(Throwable receiver, Throwable suppressed) { + STRATEGY.addSuppressed(receiver, suppressed); + } + + public static Throwable[] getSuppressed(Throwable receiver) { + return STRATEGY.getSuppressed(receiver); + } + + public static void printStackTrace(Throwable receiver) { + STRATEGY.printStackTrace(receiver); + } + + public static void printStackTrace(Throwable receiver, PrintWriter writer) { + STRATEGY.printStackTrace(receiver, writer); + } + + public static void printStackTrace(Throwable receiver, PrintStream stream) { + STRATEGY.printStackTrace(receiver, stream); + } + + private static boolean useMimicStrategy() { + return !Boolean.getBoolean(SYSTEM_PROPERTY_TWR_DISABLE_MIMIC); + } + + private static final String ANDROID_OS_BUILD_VERSION = "android.os.Build$VERSION"; + + /** + * Get the API level from {@link android.os.Build.VERSION} via reflection. The reason to use + * relection is to avoid dependency on {@link android.os.Build.VERSION}. The advantage of doing + * this is that even when you desugar a jar twice, and Desugars sees this class, there is no need + * to put {@link android.os.Build.VERSION} on the classpath. + * + * <p>Another reason of doing this is that it does not introduce any additional dependency into + * the input jars. + * + * @return The API level of the current device. If it is {@code null}, then it means there was an + * exception. + */ + private static Integer readApiLevelFromBuildVersion() { + try { + Class<?> buildVersionClass = Class.forName(ANDROID_OS_BUILD_VERSION); + Field field = buildVersionClass.getField("SDK_INT"); + return (Integer) field.get(null); + } catch (Exception e) { + System.err.println( + "Failed to retrieve value from " + + ANDROID_OS_BUILD_VERSION + + ".SDK_INT due to the following exception."); + e.printStackTrace(System.err); + return null; + } + } + + /** + * The strategy to desugar try-with-resources statements. A strategy handles the behavior of an + * exception in terms of suppressed exceptions and stack trace printing. + */ + abstract static class AbstractDesugaringStrategy { + + protected static final Throwable[] EMPTY_THROWABLE_ARRAY = new Throwable[0]; + + public abstract void addSuppressed(Throwable receiver, Throwable suppressed); + + public abstract Throwable[] getSuppressed(Throwable receiver); + + public abstract void printStackTrace(Throwable receiver); + + public abstract void printStackTrace(Throwable receiver, PrintStream stream); + + public abstract void printStackTrace(Throwable receiver, PrintWriter writer); + } + + /** This strategy just delegates all the method calls to java.lang.Throwable. */ + static class ReuseDesugaringStrategy extends AbstractDesugaringStrategy { + + @Override + public void addSuppressed(Throwable receiver, Throwable suppressed) { + receiver.addSuppressed(suppressed); + } + + @Override + public Throwable[] getSuppressed(Throwable receiver) { + return receiver.getSuppressed(); + } + + @Override + public void printStackTrace(Throwable receiver) { + receiver.printStackTrace(); + } + + @Override + public void printStackTrace(Throwable receiver, PrintStream stream) { + receiver.printStackTrace(stream); + } + + @Override + public void printStackTrace(Throwable receiver, PrintWriter writer) { + receiver.printStackTrace(writer); + } + } + + /** This strategy mimics the behavior of suppressed exceptions with a map. */ + static class MimicDesugaringStrategy extends AbstractDesugaringStrategy { + + public static final String SUPPRESSED_PREFIX = "Suppressed: "; + private final WeakHashMap<Throwable, List<Throwable>> map = new WeakHashMap<>(); + + /** + * Suppress an exception. If the exception to be suppressed is {@receiver} or {@null}, an + * exception will be thrown. + * + * @param receiver + * @param suppressed + */ + @Override + public void addSuppressed(Throwable receiver, Throwable suppressed) { + if (suppressed == receiver) { + throw new IllegalArgumentException("Self suppression is not allowed.", suppressed); + } + if (suppressed == null) { + throw new NullPointerException("The suppressed exception cannot be null."); + } + synchronized (this) { + List<Throwable> list = map.get(receiver); + if (list == null) { + list = new ArrayList<>(1); + map.put(receiver, list); + } + list.add(suppressed); + } + } + + @Override + public synchronized Throwable[] getSuppressed(Throwable receiver) { + List<Throwable> list = map.get(receiver); + if (list == null || list.isEmpty()) { + return EMPTY_THROWABLE_ARRAY; + } + return list.toArray(new Throwable[0]); + } + + /** + * Print the stack trace for the parameter {@code receiver}. Note that it is deliberate to NOT + * reuse the implementation {@code MimicDesugaringStrategy.printStackTrace(Throwable, + * PrintStream)}, because we are not sure whether the developer prints the stack trace to a + * different stream other than System.err. Therefore, it is a caveat that the stack traces of + * {@code receiver} and its suppressed exceptions are printed in two different streams. + */ + @Override + public synchronized void printStackTrace(Throwable receiver) { + receiver.printStackTrace(); + for (Throwable suppressed : getSuppressed(receiver)) { + System.err.print(SUPPRESSED_PREFIX); + suppressed.printStackTrace(); + } + } + + @Override + public synchronized void printStackTrace(Throwable receiver, PrintStream stream) { + receiver.printStackTrace(stream); + for (Throwable suppressed : getSuppressed(receiver)) { + stream.print(SUPPRESSED_PREFIX); + suppressed.printStackTrace(stream); + } + } + + @Override + public synchronized void printStackTrace(Throwable receiver, PrintWriter writer) { + receiver.printStackTrace(writer); + for (Throwable suppressed : getSuppressed(receiver)) { + writer.print(SUPPRESSED_PREFIX); + suppressed.printStackTrace(writer); + } + } + } + + /** This strategy ignores all suppressed exceptions, which is how retrolambda does. */ + static class NullDesugaringStrategy extends AbstractDesugaringStrategy { + + @Override + public void addSuppressed(Throwable receiver, Throwable suppressed) { + // Do nothing. The suppressed exception is discarded. + } + + @Override + public Throwable[] getSuppressed(Throwable receiver) { + return EMPTY_THROWABLE_ARRAY; + } + + @Override + public void printStackTrace(Throwable receiver) { + receiver.printStackTrace(); + } + + @Override + public void printStackTrace(Throwable receiver, PrintStream stream) { + receiver.printStackTrace(stream); + } + + @Override + public void printStackTrace(Throwable receiver, PrintWriter writer) { + receiver.printStackTrace(writer); + } + } +} |