diff options
author | Adam Michael <ajmichael@google.com> | 2017-02-10 02:43:34 +0000 |
---|---|---|
committer | Kristina Chodorow <kchodorow@google.com> | 2017-02-10 15:35:54 +0000 |
commit | 29aa0eb17c85a96e3edae8362ba93fddeed4c1e0 (patch) | |
tree | 8be36b954b22f0babb90ed51a955427e0fe4f7bb /src/tools | |
parent | 6bb3f875b780578abff192cf6444dbdfaf85061f (diff) |
Open source java 8 desugarer.
Fixes https://github.com/bazelbuild/bazel/issues/2222.
RELNOTES: Support for Java 8 lambdas, method references, type annotations and repeated annotations in Android builds with --experimental_desugar_for_android.
--
PiperOrigin-RevId: 147109786
MOS_MIGRATED_REVID=147109786
Diffstat (limited to 'src/tools')
12 files changed, 1728 insertions, 0 deletions
diff --git a/src/tools/android/java/com/google/devtools/build/android/BUILD b/src/tools/android/java/com/google/devtools/build/android/BUILD index 34cf96cc8a..56027340ca 100644 --- a/src/tools/android/java/com/google/devtools/build/android/BUILD +++ b/src/tools/android/java/com/google/devtools/build/android/BUILD @@ -7,6 +7,7 @@ filegroup( srcs = [ "BUILD.tools", "classes_deploy.jar", + "//src/tools/android/java/com/google/devtools/build/android/desugar:embedded_tools", "//src/tools/android/java/com/google/devtools/build/android/proto:srcs", ], ) @@ -50,6 +51,7 @@ java_library( filegroup( name = "srcs", srcs = glob(["**"]) + [ + "//src/tools/android/java/com/google/devtools/build/android/desugar:srcs", "//src/tools/android/java/com/google/devtools/build/android/dexer:srcs", "//src/tools/android/java/com/google/devtools/build/android/ideinfo:srcs", "//src/tools/android/java/com/google/devtools/build/android/idlclass:srcs", diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/BUILD b/src/tools/android/java/com/google/devtools/build/android/desugar/BUILD new file mode 100644 index 0000000000..cc2dcead72 --- /dev/null +++ b/src/tools/android/java/com/google/devtools/build/android/desugar/BUILD @@ -0,0 +1,38 @@ +# Description: +# Tool for desugaring Java constructs not supported by Android tools or devices. + +filegroup( + name = "embedded_tools", + srcs = [ + "BUILD.tools", + "desugar_bin_deploy.jar", + ], + visibility = ["//src/tools/android/java/com/google/devtools/build/android:__pkg__"], +) + +java_library( + name = "desugar", + srcs = glob(["*.java"]), + deps = [ + "//src/main/java/com/google/devtools/common/options", + "//src/main/protobuf:worker_protocol_java_proto", + "//src/tools/android/java/com/google/devtools/build/android:android_builder_lib", + "//third_party:asm", + "//third_party:asm-tree", + "//third_party:auto_value", + "//third_party:guava", + "//third_party:jsr305", + ], +) + +java_binary( + name = "desugar_bin", + main_class = "does.not.exist", + runtime_deps = [":desugar"], +) + +filegroup( + name = "srcs", + srcs = glob(["**"]), + visibility = ["//src/tools/android/java/com/google/devtools/build/android:__pkg__"], +) diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/BUILD.tools b/src/tools/android/java/com/google/devtools/build/android/desugar/BUILD.tools new file mode 100644 index 0000000000..29e860205c --- /dev/null +++ b/src/tools/android/java/com/google/devtools/build/android/desugar/BUILD.tools @@ -0,0 +1,7 @@ +package(default_visibility = ["//visibility:public"]) + +java_binary( + name = "Desugar", + main_class = "com.google.devtools.build.android.desugar.Desugar", + runtime_deps = [":desugar_bin_deploy.jar"], +)
\ No newline at end of file diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/BitFlags.java b/src/tools/android/java/com/google/devtools/build/android/desugar/BitFlags.java new file mode 100644 index 0000000000..8542719c87 --- /dev/null +++ b/src/tools/android/java/com/google/devtools/build/android/desugar/BitFlags.java @@ -0,0 +1,39 @@ +// Copyright 2016 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; + +/** + * Convenience method for working with {@code int} bitwise flags. + */ +class BitFlags { + + /** + * Returns {@code true} iff <b>all</b> bits in {@code bitmask} are set in {@code flags}. + * Trivially returns {@code true} if {@code bitmask} is 0. + */ + public static boolean isSet(int flags, int bitmask) { + return (flags & bitmask) == bitmask; + } + + /** + * Returns {@code true} iff <b>none</b> of the bits in {@code bitmask} are set in {@code flags}. + * Trivially returns {@code true} if {@code bitmask} is 0. + */ + public static boolean noneSet(int flags, int bitmask) { + return (flags & bitmask) == 0; + } + + // Static methods only + private BitFlags() {} +} 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 new file mode 100644 index 0000000000..d324723853 --- /dev/null +++ b/src/tools/android/java/com/google/devtools/build/android/desugar/ClassReaderFactory.java @@ -0,0 +1,50 @@ +// Copyright 2016 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 java.io.IOException; +import java.io.InputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import javax.annotation.Nullable; +import org.objectweb.asm.ClassReader; + +class ClassReaderFactory { + private final ZipFile jar; + + public ClassReaderFactory(ZipFile jar) { + this.jar = jar; + } + + /** + * Returns a reader for the given/internal/Class$Name if the class is defined in the wrapped Jar + * and {@code null} otherwise. For simplicity this method turns checked into runtime excpetions + * under the assumption that all classes have already been read once when this method is called. + */ + @Nullable + public ClassReader readIfKnown(String internalClassName) { + ZipEntry entry = jar.getEntry(internalClassName + ".class"); + if (entry == null) { + return null; + } + try (InputStream bytecode = jar.getInputStream(entry)) { + // ClassReader doesn't take ownership and instead eagerly reads the stream's contents + return new ClassReader(bytecode); + } catch (IOException e) { + // We should've already read through all files in the Jar once at this point, so we don't + // expect failures reading some files a second time. + throw new IllegalStateException("Couldn't load " + internalClassName, e); + } + } +} 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 new file mode 100644 index 0000000000..9f927b5024 --- /dev/null +++ b/src/tools/android/java/com/google/devtools/build/android/desugar/Desugar.java @@ -0,0 +1,232 @@ +// Copyright 2016 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 static java.nio.charset.StandardCharsets.ISO_8859_1; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.io.ByteStreams; +import com.google.devtools.build.android.Converters.ExistingPathConverter; +import com.google.devtools.build.android.Converters.PathConverter; +import com.google.devtools.common.options.Option; +import com.google.devtools.common.options.OptionsBase; +import com.google.devtools.common.options.OptionsParser; +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Enumeration; +import java.util.List; +import java.util.Map; +import java.util.zip.CRC32; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipOutputStream; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassWriter; + +/** + * Command-line tool to desugar Java 8 constructs that dx doesn't know what to do with, in + * particular lambdas and method references. + */ +class Desugar { + + /** + * Commandline options for {@link Desugar}. + */ + public static class Options extends OptionsBase { + @Option(name = "input", + defaultValue = "null", + category = "input", + converter = ExistingPathConverter.class, + abbrev = 'i', + help = "Input Jar with classes to desugar.") + public Path inputJar; + + @Option(name = "classpath_entry", + allowMultiple = true, + defaultValue = "", + category = "input", + converter = ExistingPathConverter.class, + help = "Ordered classpath to resolve symbols in the --input Jar, like javac's -cp flag.") + public List<Path> classpath; + + @Option(name = "bootclasspath_entry", + allowMultiple = true, + defaultValue = "", + category = "input", + converter = ExistingPathConverter.class, + help = "Bootclasspath that was used to compile the --input Jar with, like javac's " + + "-bootclasspath flag. If no bootclasspath is explicitly given then the tool's own " + + "bootclasspath is used.") + public List<Path> bootclasspath; + + @Option(name = "output", + defaultValue = "null", + category = "output", + converter = PathConverter.class, + abbrev = 'o', + help = "Output Jar to write desugared classes into.") + public Path outputJar; + + @Option(name = "verbose", + defaultValue = "false", + category = "misc", + abbrev = 'v', + help = "Enables verbose debugging output.") + public boolean verbose; + } + + public static void main(String[] args) throws Exception { + // LambdaClassMaker generates lambda classes for us, but it does so by essentially simulating + // the call to LambdaMetafactory that the JVM would make when encountering an invokedynamic. + // LambdaMetafactory is in the JDK and its implementation has a property to write out ("dump") + // generated classes, which we take advantage of here. Set property before doing anything else + // since the property is read in the static initializer; if this breaks we can investigate + // setting the property when calling the tool. + Path dumpDirectory = Files.createTempDirectory("lambdas"); + System.setProperty( + LambdaClassMaker.LAMBDA_METAFACTORY_DUMPER_PROPERTY, dumpDirectory.toString()); + + if (args.length == 1 && args[0].startsWith("@")) { + args = Files.readAllLines(Paths.get(args[0].substring(1)), ISO_8859_1).toArray(new String[0]); + } + + OptionsParser optionsParser = + OptionsParser.newOptionsParser(Options.class); + optionsParser.parseAndExitUponError(args); + Options options = optionsParser.getOptions(Options.class); + + if (options.verbose) { + System.out.printf("Lambda classes will be written under %s%n", dumpDirectory); + } + ClassLoader loader = + createClassLoader(options.bootclasspath, options.inputJar, options.classpath); + try (ZipFile in = new ZipFile(options.inputJar.toFile()); + ZipOutputStream out = new ZipOutputStream(new BufferedOutputStream( + Files.newOutputStream(options.outputJar)))) { + LambdaClassMaker lambdas = new LambdaClassMaker(dumpDirectory); + ClassReaderFactory readerFactory = new ClassReaderFactory(in); + ImmutableSet.Builder<String> interfaceLambdaMethodCollector = ImmutableSet.builder(); + + // Process input Jar, desugaring as we go + for (Enumeration<? extends ZipEntry> entries = in.entries(); entries.hasMoreElements(); ) { + ZipEntry entry = entries.nextElement(); + try (InputStream content = in.getInputStream(entry)) { + // We can write classes uncompressed since they need to be converted to .dex format for + // Android anyways. Resources are written as they were in the input jar to avoid any + // danger of accidentally uncompressed resources ending up in an .apk. + if (entry.getName().endsWith(".class")) { + ClassReader reader = new ClassReader(content); + ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS /*for bridge methods*/); + reader.accept( + new LambdaDesugaring( + new Java7Compatibility(writer, readerFactory), + loader, + lambdas, + interfaceLambdaMethodCollector), + 0); + writeStoredEntry(out, entry.getName(), writer.toByteArray()); + } else { + // TODO(bazel-team): Avoid de- and re-compressing resource files + ZipEntry destEntry = new ZipEntry(entry); + destEntry.setCompressedSize(-1); + out.putNextEntry(destEntry); + ByteStreams.copy(content, out); + out.closeEntry(); + } + } + } + + // Write out the lambda classes we generated along the way + ImmutableSet<String> interfaceLambdaMethods = interfaceLambdaMethodCollector.build(); + for (Map.Entry<Path, LambdaInfo> lambdaClass : lambdas.drain().entrySet()) { + try (InputStream bytecode = + Files.newInputStream(dumpDirectory.resolve(lambdaClass.getKey()))) { + ClassReader reader = new ClassReader(bytecode); + ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS /*for invoking bridges*/); + LambdaClassFixer lambdaFixer = + new LambdaClassFixer( + // null ClassReaderFactory b/c we don't expect to need it for lambda classes + new Java7Compatibility(writer, (ClassReaderFactory) null), + lambdaClass.getValue(), + readerFactory, + interfaceLambdaMethods); + // Send lambda classes through desugaring to make sure there's no invokedynamic + // instructions in generated lambda classes (checkState below will fail) + reader.accept(new LambdaDesugaring(lambdaFixer, loader, lambdas, null), 0); + writeStoredEntry(out, lambdaFixer.getInternalName() + ".class", writer.toByteArray()); + } + } + + Map<Path, LambdaInfo> leftBehind = lambdas.drain(); + checkState(leftBehind.isEmpty(), "Didn't process %s", leftBehind); + } + // Use input's timestamp for output file so the output file is stable. + Files.setLastModifiedTime(options.outputJar, Files.getLastModifiedTime(options.inputJar)); + } + + private static void writeStoredEntry(ZipOutputStream out, String filename, byte[] content) + throws IOException { + // Need to pre-compute checksum for STORED (uncompressed) entries) + CRC32 checksum = new CRC32(); + checksum.update(content); + + ZipEntry result = new ZipEntry(filename); + result.setTime(0L); // Use stable timestamp Jan 1 1980 + result.setCrc(checksum.getValue()); + result.setSize(content.length); + result.setCompressedSize(content.length); + // Write uncompressed, since this is just an intermediary artifact that we will convert to .dex + result.setMethod(ZipEntry.STORED); + + out.putNextEntry(result); + out.write(content); + out.closeEntry(); + } + + private static ClassLoader createClassLoader(List<Path> bootclasspath, Path inputJar, + List<Path> classpath) throws IOException { + // Prepend classpath with input jar itself so LambdaDesugaring can load classes with lambdas. + // Note that inputJar and classpath need to be in the same classloader because we typically get + // the header Jar for inputJar on the classpath and having the header Jar in a parent loader + // means the header version is preferred over the real thing. + classpath = ImmutableList.<Path>builder().add(inputJar).addAll(classpath).build(); + if (bootclasspath.isEmpty()) { + // TODO(b/31547323): Require bootclasspath once Bazel always provides it. Using the tool's + // bootclasspath as a fallback is iffy at best and produces wrong results at worst. + return HeaderClassLoader.fromClassPath(classpath); + } + // 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. + return HeaderClassLoader.fromClassPath(classpath, + HeaderClassLoader.fromClassPath(bootclasspath, + new ClassLoader() { + @Override + protected Class<?> loadClass(String name, boolean resolve) + throws ClassNotFoundException { + if (name.startsWith("java.")) { + // Use system class loader for java. classes, since ClassLoader.defineClass gets + // grumpy when those don't come from the standard place. + return super.loadClass(name, resolve); + } + throw new ClassNotFoundException(); + } + })); + } +} diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/HeaderClassLoader.java b/src/tools/android/java/com/google/devtools/build/android/desugar/HeaderClassLoader.java new file mode 100644 index 0000000000..8d9f7ea1e0 --- /dev/null +++ b/src/tools/android/java/com/google/devtools/build/android/desugar/HeaderClassLoader.java @@ -0,0 +1,156 @@ +// Copyright 2016 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 java.io.IOError; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.zip.ZipEntry; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +/** + * Class loader that can "load" classes from header Jars. This class loader stubs in missing code + * attributes on the fly to make {@link ClassLoader#defineClass} happy. Classes loaded are unusable + * other than to resolve method references, so this class loader should only be used to process + * or inspect classes, not to execute their code. Also note that the resulting classes may be + * missing private members, which header Jars may omit. + * + * @see java.net.URLClassLoader + */ +class HeaderClassLoader extends ClassLoader { + + private final Map<String, JarFile> jarfiles; + + /** Creates a classloader from the given classpath with the system classloader as its parent. */ + public static HeaderClassLoader fromClassPath(List<Path> classpath) throws IOException { + return new HeaderClassLoader(indexJars(classpath)); + } + + /** Creates a classloader from the given classpath with the given parent. */ + public static HeaderClassLoader fromClassPath(List<Path> classpath, ClassLoader parent) + throws IOException { + return new HeaderClassLoader(indexJars(classpath), parent); + } + + /** + * Opens the given list of Jar files and returns an index of all classes in them, to avoid + * scanning all Jars over and over for each class in {@link #findClass}. + */ + private static Map<String, JarFile> indexJars(List<Path> classpath) throws IOException { + HashMap<String, JarFile> result = new HashMap<>(); + for (Path jarfile : classpath) { + JarFile jar = new JarFile(jarfile.toFile()); + for (Enumeration<JarEntry> cur = jar.entries(); cur.hasMoreElements(); ) { + JarEntry entry = cur.nextElement(); + if (entry.getName().endsWith(".class") && !result.containsKey(entry.getName())) { + result.put(entry.getName(), jar); + } + } + } + return result; + } + + private HeaderClassLoader(Map<String, JarFile> jarfiles) { + super(); + this.jarfiles = jarfiles; + } + + private HeaderClassLoader(Map<String, JarFile> jarfiles, ClassLoader parent) { + super(parent); + this.jarfiles = jarfiles; + } + + @Override + protected Class<?> findClass(String name) throws ClassNotFoundException { + String filename = name.replace('.', '/') + ".class"; + JarFile jarfile = jarfiles.get(filename); + if (jarfile == null) { + throw new ClassNotFoundException(); + } + ZipEntry entry = jarfile.getEntry(filename); + byte[] bytecode; + try (InputStream content = jarfile.getInputStream(entry)) { + ClassReader reader = new ClassReader(content); + // Have ASM compute maxs so we don't need to figure out how many formal parameters there are + ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS); + reader.accept(new CodeStubber(writer), 0); + bytecode = writer.toByteArray(); + } catch (IOException e) { + throw new IOError(e); + } + return defineClass(name, bytecode, 0, bytecode.length); + } + + /** Class visitor that stubs in missing code attributes. */ + private static class CodeStubber extends ClassVisitor { + + public CodeStubber(ClassVisitor cv) { + super(Opcodes.ASM5, cv); + } + + @Override + public MethodVisitor visitMethod( + int access, String name, String desc, String signature, String[] exceptions) { + MethodVisitor dest = super.visitMethod(access, name, desc, signature, exceptions); + if ((access & (Opcodes.ACC_ABSTRACT | Opcodes.ACC_NATIVE)) != 0) { + // No need to stub out abstract or native methods + return dest; + } + return new BodyStubber(dest); + } + + } + + /** Method visitor used by {@link CodeStubber} to put code into methods without code. */ + private static class BodyStubber extends MethodVisitor { + + private static final String EXCEPTION_INTERNAL_NAME = "java/lang/UnsupportedOperationException"; + + private boolean hasCode = false; + + public BodyStubber(MethodVisitor mv) { + super(Opcodes.ASM5, mv); + } + + @Override + public void visitCode() { + hasCode = true; + super.visitCode(); + } + + @Override + public void visitEnd() { + if (!hasCode) { + super.visitTypeInsn(Opcodes.NEW, EXCEPTION_INTERNAL_NAME); + super.visitInsn(Opcodes.DUP); + super.visitMethodInsn( + Opcodes.INVOKESPECIAL, EXCEPTION_INTERNAL_NAME, "<init>", "()V", /*itf*/ false); + super.visitInsn(Opcodes.ATHROW); + super.visitMaxs(0, 0); // triggers computation of the actual max's + } + super.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 new file mode 100644 index 0000000000..cc3fe14ff2 --- /dev/null +++ b/src/tools/android/java/com/google/devtools/build/android/desugar/Java7Compatibility.java @@ -0,0 +1,239 @@ +// Copyright 2016 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 org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.Attribute; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +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 + * interface methods (i.e., non-abstract interface methods), which don't exist in Java 7. + */ +public class Java7Compatibility extends ClassVisitor { + + private final ClassReaderFactory factory; + + private boolean isInterface; + private String internalName; + + public Java7Compatibility(ClassVisitor cv, ClassReaderFactory factory) { + super(Opcodes.ASM5, cv); + this.factory = factory; + } + + @Override + public void visit( + int version, + int access, + String name, + String signature, + String superName, + String[] interfaces) { + internalName = name; + isInterface = BitFlags.isSet(access, Opcodes.ACC_INTERFACE); + super.visit( + Math.min(version, Opcodes.V1_7), + access, + name, + signature, + superName, + interfaces); + } + + @Override + public MethodVisitor visitMethod( + int access, String name, String desc, String signature, String[] exceptions) { + // Remove bridge default methods in interfaces; javac generates them again for implementing + // classes anyways. + if (isInterface + && (access & (Opcodes.ACC_BRIDGE | Opcodes.ACC_ABSTRACT | Opcodes.ACC_STATIC)) + == Opcodes.ACC_BRIDGE) { + return null; + } + if (isInterface + && "$jacocoInit".equals(name) + && BitFlags.isSet(access, Opcodes.ACC_SYNTHETIC | Opcodes.ACC_STATIC)) { + // Drop static interface method that Jacoco generates--we'll inline it into the static + // initializer instead + return null; + } + // TODO(b/31547323): Avoid stack trace and report errors more user-friendly + checkArgument(!isInterface + || BitFlags.isSet(access, Opcodes.ACC_ABSTRACT) + || "<clinit>".equals(name), + "Interface %s defines non-abstract method %s%s, which is not supported", + internalName, name, desc); + MethodVisitor result = super.visitMethod(access, name, desc, signature, exceptions); + return (isInterface && "<clinit>".equals(name)) ? new InlineJacocoInit(result) : result; + } + + @Override + public void visitInnerClass(String name, String outerName, String innerName, int access) { + // Drop MethodHandles$Lookup inner class information--it shouldn't be needed anymore. Proguard + // complains about this even though the inner class information is never used. + if (!"java/lang/invoke/MethodHandles$Lookup".equals(name)) { + super.visitInnerClass(name, outerName, innerName, access); + } + } + + private class InlineJacocoInit extends MethodVisitor { + public InlineJacocoInit(MethodVisitor dest) { + super(Opcodes.ASM5, dest); + } + + @Override + public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) { + if (opcode == Opcodes.INVOKESTATIC + && "$jacocoInit".equals(name) + && internalName.equals(owner)) { + ClassReader bytecode = checkNotNull(factory.readIfKnown(internalName), + "Couldn't load interface %s to inline $jacocoInit()", internalName); + InlineOneMethod copier = new InlineOneMethod("$jacocoInit", this); + bytecode.accept(copier, ClassReader.SKIP_DEBUG /* we're copying generated code anyway */); + } else { + super.visitMethodInsn(opcode, owner, name, desc, itf); + } + } + } + + private static class InlineOneMethod extends ClassVisitor { + + private final String methodName; + private final MethodVisitor dest; + private int copied = 0; + + public InlineOneMethod(String methodName, MethodVisitor dest) { + super(Opcodes.ASM5); + this.methodName = methodName; + this.dest = dest; + } + + @Override + public void visit( + int version, + int access, + String name, + String signature, + String superName, + String[] interfaces) { + checkArgument(BitFlags.isSet(access, Opcodes.ACC_INTERFACE)); + } + + @Override + public MethodVisitor visitMethod( + int access, String name, String desc, String signature, String[] exceptions) { + if (name.equals(methodName)) { + checkState(copied == 0, "Found unexpected second method %s with descriptor %s", name, desc); + ++copied; + return new InlineMethodBody(dest); + } + return null; + } + } + + private static class InlineMethodBody extends MethodVisitor { + private final MethodVisitor dest; + + public InlineMethodBody(MethodVisitor dest) { + // We'll set the destination visitor in visitCode() to reduce the risk of copying anything + // we didn't mean to copy + super(Opcodes.ASM5, (MethodVisitor) null); + this.dest = dest; + } + + @Override + public void visitParameter(String name, int access) { + } + + @Override + public AnnotationVisitor visitAnnotationDefault() { + throw new IllegalStateException("Don't use to copy annotation attributes"); + } + + @Override + public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + return null; + } + + @Override + public AnnotationVisitor visitTypeAnnotation( + int typeRef, TypePath typePath, String desc, boolean visible) { + return null; + } + + @Override + public AnnotationVisitor visitParameterAnnotation(int parameter, String desc, boolean visible) { + return null; + } + + @Override + public void visitAttribute(Attribute attr) { + mv = null; // don't care about anything but the code attribute + } + + @Override + public void visitCode() { + // Start copying instructions but don't call super.visitCode() since dest is already in the + // middle of visiting another method + mv = dest; + } + + @Override + public void visitInsn(int opcode) { + switch (opcode) { + case Opcodes.IRETURN: + case Opcodes.LRETURN: + case Opcodes.FRETURN: + case Opcodes.DRETURN: + case Opcodes.ARETURN: + case Opcodes.RETURN: + checkState(mv != null, "Encountered a second return it would seem: %s", opcode); + mv = null; // Done: we don't expect anything to follow + return; + default: + super.visitInsn(opcode); + } + } + + @Override + public void visitVarInsn(int opcode, int var) { + throw new UnsupportedOperationException( + "We don't support inlining methods with locals: " + opcode + " " + var); + } + + @Override + public void visitLocalVariable( + String name, String desc, String signature, Label start, Label end, int index) { + throw new UnsupportedOperationException( + "We don't support inlining methods with locals: " + name + ": " + desc); + } + + @Override + public void visitMaxs(int maxStack, int maxLocals) { + // Drop this, since dest will get more instructions and will need to recompute stack size. + // This does indicate the end of visiting bytecode instructions, so defensively reset mv. + mv = null; + } + } +} 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 new file mode 100644 index 0000000000..b429f76208 --- /dev/null +++ b/src/tools/android/java/com/google/devtools/build/android/desugar/LambdaClassFixer.java @@ -0,0 +1,409 @@ +// Copyright 2016 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 com.google.common.collect.ImmutableSet; +import java.util.HashSet; +import java.util.LinkedHashSet; +import org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.TypeInsnNode; + +/** + * Visitor intended to fix up lambda classes to match assumptions made in {@link LambdaDesugaring}. + * Specifically this includes fixing visibilities and generating any missing factory methods. + * + * <p>Each instance can only visit one class. This is because the signature of the needed factory + * method is passed into the constructor. + */ +class LambdaClassFixer extends ClassVisitor { + + /** Magic method name used by {@link java.lang.invoke.LambdaMetafactory} */ + public static final String FACTORY_METHOD_NAME = "get$Lambda"; + + private final LambdaInfo lambdaInfo; + private final ClassReaderFactory factory; + private final ImmutableSet<String> interfaceLambdaMethods; + private final HashSet<String> implementedMethods = new HashSet<>(); + private final LinkedHashSet<String> methodsToMoveIn = new LinkedHashSet<>(); + + private String internalName; + private ImmutableList<String> interfaces; + + private boolean hasState; + private boolean hasFactory; + + private String desc; + private String signature; + private String[] exceptions; + + + public LambdaClassFixer(ClassVisitor dest, LambdaInfo lambdaInfo, ClassReaderFactory factory, + ImmutableSet<String> interfaceLambdaMethods) { + super(Opcodes.ASM5, dest); + this.lambdaInfo = lambdaInfo; + this.factory = factory; + this.interfaceLambdaMethods = interfaceLambdaMethods; + } + + public String getInternalName() { + return internalName; + } + + @Override + public void visit( + int version, + int access, + String name, + String signature, + String superName, + String[] interfaces) { + checkArgument(BitFlags.noneSet(access, Opcodes.ACC_INTERFACE), "Not a class: %s", name); + checkState(internalName == null, "Already visited %s, can't reuse for %s", internalName, name); + internalName = name; + hasState = false; + hasFactory = false; + desc = null; + this.signature = null; + exceptions = null; + this.interfaces = ImmutableList.copyOf(interfaces); + super.visit(version, access, name, signature, superName, interfaces); + } + + @Override + public FieldVisitor visitField( + int access, String name, String desc, String signature, Object value) { + hasState = true; + return super.visitField(access, name, desc, signature, value); + } + + @Override + public MethodVisitor visitMethod( + int access, String name, String desc, String signature, String[] exceptions) { + if (name.equals("writeReplace") && BitFlags.noneSet(access, Opcodes.ACC_STATIC) + && desc.equals("()Ljava/lang/Object;")) { + // Lambda serialization hooks use java/lang/invoke/SerializedLambda, which isn't available on + // Android. Since Jack doesn't do anything special for serializable lambdas we just drop these + // serialization hooks. + // https://docs.oracle.com/javase/8/docs/platform/serialization/spec/output.html#a5324 gives + // details on the role and signature of this method. + return null; + } + if (BitFlags.noneSet(access, Opcodes.ACC_ABSTRACT | Opcodes.ACC_STATIC)) { + // Keep track of instance methods implemented in this class for later. Since this visitor + // is intended for lambda classes, no need to look at the superclass. + implementedMethods.add(name + ":" + desc); + } + if (FACTORY_METHOD_NAME.equals(name)) { + hasFactory = true; + access &= ~Opcodes.ACC_PRIVATE; // make factory method accessible + } else if ("<init>".equals(name)) { + this.desc = desc; + this.signature = signature; + this.exceptions = exceptions; + } + MethodVisitor methodVisitor = + new LambdaClassMethodRewriter(super.visitMethod(access, name, desc, signature, exceptions)); + if (!lambdaInfo.bridgeMethod().equals(lambdaInfo.methodReference())) { + // Skip UseBridgeMethod unless we actually need it + methodVisitor = + new UseBridgeMethod(methodVisitor, lambdaInfo, access, name, desc, signature, exceptions); + } + return methodVisitor; + } + + @Override + public void visitEnd() { + checkState(!hasState || hasFactory, + "Expected factory method for capturing lambda %s", internalName); + if (!hasFactory) { + // Fake factory method if LambdaMetafactory didn't generate it + checkState(signature == null, + "Didn't expect generic constructor signature %s %s", internalName, signature); + + // Since this is a stateless class we populate and use a static singleton field "$instance" + String singletonFieldDesc = Type.getObjectType(internalName).getDescriptor(); + super.visitField( + Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC | Opcodes.ACC_FINAL, + "$instance", + singletonFieldDesc, + (String) null, + (Object) null) + .visitEnd(); + + MethodVisitor codeBuilder = + super.visitMethod( + Opcodes.ACC_STATIC, + "<clinit>", + "()V", + (String) null, + new String[0]); + codeBuilder.visitTypeInsn(Opcodes.NEW, internalName); + codeBuilder.visitInsn(Opcodes.DUP); + codeBuilder.visitMethodInsn(Opcodes.INVOKESPECIAL, internalName, "<init>", + checkNotNull(desc, "didn't see a constructor for %s", internalName), /*itf*/ false); + codeBuilder.visitFieldInsn(Opcodes.PUTSTATIC, internalName, "$instance", singletonFieldDesc); + codeBuilder.visitInsn(Opcodes.RETURN); + codeBuilder.visitMaxs(2, 0); // two values are pushed onto the stack + codeBuilder.visitEnd(); + + codeBuilder = // reuse codeBuilder variable to avoid accidental additions to previous method + super.visitMethod( + Opcodes.ACC_STATIC, + FACTORY_METHOD_NAME, + lambdaInfo.factoryMethodDesc(), + (String) null, + exceptions); + codeBuilder.visitFieldInsn(Opcodes.GETSTATIC, internalName, "$instance", singletonFieldDesc); + codeBuilder.visitInsn(Opcodes.ARETURN); + codeBuilder.visitMaxs(1, 0); // one value on the stack + } + + copyRewrittenLambdaMethods(); + copyBridgeMethods(interfaces); + super.visitEnd(); + } + + private void copyRewrittenLambdaMethods() { + for (String rewritten : methodsToMoveIn) { + String interfaceInternalName = rewritten.substring(0, rewritten.indexOf('#')); + String methodName = rewritten.substring(interfaceInternalName.length() + 1); + ClassReader bytecode = checkNotNull(factory.readIfKnown(interfaceInternalName), + "Couldn't load interface with lambda method %s", rewritten); + 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); + } + } + + private void copyBridgeMethods(ImmutableList<String> interfaces) { + for (String implemented : interfaces) { + ClassReader bytecode = factory.readIfKnown(implemented); + if (bytecode != null) { + // Don't copy line numbers and local variable tables. They would be misleading or wrong + // and other methods in generated lambda classes don't have debug info either. + bytecode.accept(new CopyBridgeMethods(), ClassReader.SKIP_DEBUG); + } // else the interface is defined in a different Jar, which we can ignore here + } + } + + /** + * Rewriter for methods in generated lambda classes. + */ + private class LambdaClassMethodRewriter extends MethodVisitor { + public LambdaClassMethodRewriter(MethodVisitor dest) { + super(Opcodes.ASM5, dest); + } + + @Override + public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) { + String method = owner + "#" + name; + // Rewrite invocations of lambda methods in interfaces to anticipate the lambda method being + // moved into the lambda class (i.e., the class being visited here). + if (interfaceLambdaMethods.contains(method)) { + checkArgument(opcode == Opcodes.INVOKESTATIC, "Cannot move instance method %s", method); + owner = internalName; + itf = false; // owner was interface but is now a class + methodsToMoveIn.add(method); + } + super.visitMethodInsn(opcode, owner, name, desc, itf); + } + + @Override + public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + // Drop annotation that's part of the generated lambda class that's not available on Android. + // Proguard complains about this otherwise. + if ("Ljava/lang/invoke/LambdaForm$Hidden;".equals(desc)) { + return null; + } + return super.visitAnnotation(desc, visible); + } + } + + /** + * Visitor that copies bridge methods from the visited interface into the class visited by the + * surrounding {@link LambdaClassFixer}. Descends recursively into interfaces extended by the + * visited interface. + */ + private class CopyBridgeMethods extends ClassVisitor { + + @SuppressWarnings("hiding") private ImmutableList<String> interfaces; + + public CopyBridgeMethods() { + // No delegate visitor; instead we'll add methods to the outer class's delegate where needed + 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); + } + + @Override + public MethodVisitor visitMethod( + int access, String name, String desc, String signature, String[] exceptions) { + if ((access & (Opcodes.ACC_BRIDGE | Opcodes.ACC_ABSTRACT | Opcodes.ACC_STATIC)) + == Opcodes.ACC_BRIDGE) { + // 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)); + } + } + return null; + } + + @Override + public void visitEnd() { + copyBridgeMethods(this.interfaces); + } + } + + private class CopyOneMethod extends ClassVisitor { + + private final String methodName; + private int copied = 0; + + public CopyOneMethod(String methodName) { + // No delegate visitor; instead we'll add methods to the outer class's delegate where needed + super(Opcodes.ASM5); + this.methodName = methodName; + } + + @Override + public void visit( + int version, + int access, + String name, + String signature, + String superName, + String[] interfaces) { + checkArgument(BitFlags.isSet(access, Opcodes.ACC_INTERFACE)); + } + + @Override + public MethodVisitor visitMethod( + int access, String name, String desc, String signature, String[] exceptions) { + if (name.equals(methodName)) { + checkState(copied == 0, "Found unexpected second method %s with descriptor %s", name, desc); + ++copied; + return new AvoidJacocoInit( + LambdaClassFixer.super.visitMethod(access, name, desc, signature, exceptions)); + } + return null; + } + } + + /** + * Method visitor that rewrites {@code $jacocoInit()} calls to equivalent field accesses. + * + * <p>This class should only be used to visit interface methods and assumes that the code in + * {@code $jacocoInit()} is always executed in the interface's static initializer, which is the + * case in the absence of hand-written static or default interface methods (which + * {@link Java7Compatibility} makes sure of). + */ + private static class AvoidJacocoInit extends MethodVisitor { + public AvoidJacocoInit(MethodVisitor dest) { + super(Opcodes.ASM5, dest); + } + + @Override + public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) { + if (opcode == Opcodes.INVOKESTATIC && "$jacocoInit".equals(name)) { + // Rewrite $jacocoInit() calls to just read the $jacocoData field + super.visitFieldInsn(Opcodes.GETSTATIC, owner, "$jacocoData", "[Z"); + } else { + super.visitMethodInsn(opcode, owner, name, desc, itf); + } + } + } + + private static class UseBridgeMethod extends MethodNode { + + private final MethodVisitor dest; + private final LambdaInfo lambdaInfo; + + public UseBridgeMethod(MethodVisitor dest, LambdaInfo lambdaInfo, + int access, String name, String desc, String signature, String[] exceptions) { + super(Opcodes.ASM5, access, name, desc, signature, exceptions); + this.dest = dest; + this.lambdaInfo = lambdaInfo; + } + + @Override + public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) { + if (owner.equals(lambdaInfo.methodReference().getOwner()) + && name.equals(lambdaInfo.methodReference().getName()) + && desc.equals(lambdaInfo.methodReference().getDesc())) { + if (lambdaInfo.methodReference().getTag() == Opcodes.H_NEWINVOKESPECIAL + && lambdaInfo.bridgeMethod().getTag() != Opcodes.H_NEWINVOKESPECIAL) { + // We're changing a constructor call to a factory method call, so we unfortunately need + // to go find the NEW/DUP pair preceding the constructor call and remove it + removeLastAllocation(); + } + super.visitMethodInsn( + LambdaDesugaring.invokeOpcode(lambdaInfo.bridgeMethod()), + lambdaInfo.bridgeMethod().getOwner(), + lambdaInfo.bridgeMethod().getName(), + lambdaInfo.bridgeMethod().getDesc(), + lambdaInfo.bridgeMethod().isInterface()); + + } else { + super.visitMethodInsn(opcode, owner, name, desc, itf); + } + } + + private void removeLastAllocation() { + AbstractInsnNode insn = instructions.getLast(); + while (insn != null && insn.getPrevious() != null) { + AbstractInsnNode prev = insn.getPrevious(); + if (prev.getOpcode() == Opcodes.NEW && insn.getOpcode() == Opcodes.DUP + && ((TypeInsnNode) prev).desc.equals(lambdaInfo.methodReference().getOwner())) { + instructions.remove(prev); + instructions.remove(insn); + return; + } + insn = prev; + } + throw new IllegalStateException( + "Couldn't find allocation to rewrite ::new reference " + lambdaInfo.methodReference()); + } + + @Override + public void visitEnd() { + accept(dest); + } + } +} diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/LambdaClassMaker.java b/src/tools/android/java/com/google/devtools/build/android/desugar/LambdaClassMaker.java new file mode 100644 index 0000000000..8a2ebea51b --- /dev/null +++ b/src/tools/android/java/com/google/devtools/build/android/desugar/LambdaClassMaker.java @@ -0,0 +1,95 @@ +// Copyright 2016 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 com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterators; +import java.io.IOException; +import java.lang.invoke.MethodHandle; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Stream; + +class LambdaClassMaker { + + static final String LAMBDA_METAFACTORY_DUMPER_PROPERTY = "jdk.internal.lambda.dumpProxyClasses"; + + private final Path rootDirectory; + private final Map<Path, LambdaInfo> generatedClasses = new LinkedHashMap<>(); + + public LambdaClassMaker(Path rootDirectory) { + checkArgument(Files.isDirectory(rootDirectory)); + this.rootDirectory = rootDirectory; + } + + public String generateLambdaClass(String invokerInternalName, LambdaInfo lambdaInfo, + MethodHandle bootstrapMethod, ArrayList<Object> bsmArgs) throws IOException { + // Invoking the bootstrap method will dump the generated class + try { + bootstrapMethod.invokeWithArguments(bsmArgs); + } catch (Throwable e) { + throw new IllegalStateException("Failed to generate lambda class for class " + + invokerInternalName + " using " + bootstrapMethod + " with arguments " + bsmArgs, e); + } + + Path generatedClassFile = findOnlyUnprocessed(invokerInternalName + "$$Lambda$"); + String lambdaClassName = generatedClassFile.toString(); + checkState(lambdaClassName.endsWith(".class"), "Unexpected filename %s", lambdaClassName); + lambdaClassName = lambdaClassName.substring(0, lambdaClassName.length() - ".class".length()); + generatedClasses.put(generatedClassFile, lambdaInfo); + return lambdaClassName; + } + + /** + * Returns relative paths to .class files generated since the last call to this method together + * with a string descriptor of the factory method. + */ + public Map<Path, LambdaInfo> drain() { + ImmutableMap<Path, LambdaInfo> result = ImmutableMap.copyOf(generatedClasses); + generatedClasses.clear(); + return result; + } + + private Path findOnlyUnprocessed(final String pathPrefix) throws IOException { + // TODO(kmb): Investigate making this faster in the case of many lambdas + // TODO(bazel-team): This could be much nicer with lambdas + try (Stream<Path> results = + Files.walk(rootDirectory) + .map( + new Function<Path, Path>() { + @Override + public Path apply(Path path) { + return rootDirectory.relativize(path); + } + }) + .filter( + new Predicate<Path>() { + @Override + public boolean test(Path path) { + return path.toString().startsWith(pathPrefix) + && !generatedClasses.containsKey(path); + } + })) { + return Iterators.getOnlyElement(results.iterator()); + } + } +} diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/LambdaDesugaring.java b/src/tools/android/java/com/google/devtools/build/android/desugar/LambdaDesugaring.java new file mode 100644 index 0000000000..2248fa0a70 --- /dev/null +++ b/src/tools/android/java/com/google/devtools/build/android/desugar/LambdaDesugaring.java @@ -0,0 +1,431 @@ +// Copyright 2016 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 static java.lang.invoke.MethodHandles.publicLookup; +import static org.objectweb.asm.Opcodes.ASM5; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableSet; +import java.io.IOException; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles.Lookup; +import java.lang.invoke.MethodType; +import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import javax.annotation.Nullable; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.Handle; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; + +/** + * Visitor that desugars classes with uses of lambdas into Java 7-looking code. This includes + * rewriting lambda-related invokedynamic instructions as well as fixing accessibility of methods + * that javac emits for lambda bodies. + */ +class LambdaDesugaring extends ClassVisitor { + + private final ClassLoader targetLoader; + private final LambdaClassMaker lambdas; + private final ImmutableSet.Builder<String> aggregateInterfaceLambdaMethods; + private final Map<Handle, MethodReferenceBridgeInfo> bridgeMethods = new HashMap<>(); + + private String internalName; + private boolean isInterface; + + public LambdaDesugaring(ClassVisitor dest, ClassLoader targetLoader, LambdaClassMaker lambdas, + ImmutableSet.Builder<String> aggregateInterfaceLambdaMethods) { + super(Opcodes.ASM5, dest); + this.targetLoader = targetLoader; + this.lambdas = lambdas; + this.aggregateInterfaceLambdaMethods = aggregateInterfaceLambdaMethods; + } + + @Override + public void visit( + int version, + int access, + String name, + String signature, + String superName, + String[] interfaces) { + internalName = name; + isInterface = BitFlags.isSet(access, Opcodes.ACC_INTERFACE); + super.visit(version, access, name, signature, superName, interfaces); + } + + @Override + public void visitEnd() { + for (Map.Entry<Handle, MethodReferenceBridgeInfo> bridge : bridgeMethods.entrySet()) { + Handle original = bridge.getKey(); + Handle neededMethod = bridge.getValue().bridgeMethod(); + checkState(neededMethod.getTag() == Opcodes.H_INVOKESTATIC + || neededMethod.getTag() == Opcodes.H_INVOKEVIRTUAL, + "Cannot generate bridge method %s to reach %s", neededMethod, original); + checkState(bridge.getValue().referenced() != null, + "Need referenced method %s to generate bridge %s", original, neededMethod); + + int access = Opcodes.ACC_BRIDGE | Opcodes.ACC_SYNTHETIC | Opcodes.ACC_FINAL; + if (neededMethod.getTag() == Opcodes.H_INVOKESTATIC) { + access |= Opcodes.ACC_STATIC; + } + MethodVisitor bridgeMethod = + super.visitMethod( + access, + neededMethod.getName(), + neededMethod.getDesc(), + (String) null, + toInternalNames(bridge.getValue().referenced().getExceptionTypes())); + + // Bridge is a factory method calling a constructor + if (original.getTag() == Opcodes.H_NEWINVOKESPECIAL) { + bridgeMethod.visitTypeInsn(Opcodes.NEW, original.getOwner()); + bridgeMethod.visitInsn(Opcodes.DUP); + } + + int slot = 0; + if (neededMethod.getTag() != Opcodes.H_INVOKESTATIC) { + bridgeMethod.visitVarInsn(Opcodes.ALOAD, slot++); + } + Type neededType = Type.getMethodType(neededMethod.getDesc()); + for (Type arg : neededType.getArgumentTypes()) { + bridgeMethod.visitVarInsn(arg.getOpcode(Opcodes.ILOAD), slot); + slot += arg.getSize(); + } + bridgeMethod.visitMethodInsn(invokeOpcode(original), original.getOwner(), original.getName(), + original.getDesc(), original.isInterface()); + bridgeMethod.visitInsn(neededType.getReturnType().getOpcode(Opcodes.IRETURN)); + + bridgeMethod.visitMaxs(0, 0); // rely on class writer to compute these + bridgeMethod.visitEnd(); + } + super.visitEnd(); + } + + @Override + public MethodVisitor visitMethod( + int access, String name, String desc, String signature, String[] exceptions) { + if (name.equals("$deserializeLambda$") && BitFlags.isSet(access, Opcodes.ACC_SYNTHETIC)) { + // Android doesn't do anything special for lambda serialization so drop the special + // deserialization hook that javac generates. This also makes sure we don't reference + // java/lang/invoke/SerializedLambda, which doesn't exist on Android. + return null; + } + if (name.startsWith("lambda$") && BitFlags.isSet(access, Opcodes.ACC_SYNTHETIC)) { + if (isInterface && BitFlags.isSet(access, Opcodes.ACC_STATIC)) { + // There must be a lambda in the interface (which in the absence of hand-written default or + // static interface methods must mean it's in the <clinit> method or inside another lambda). + // We'll move this method out of this class, so just record and drop it here. + // (Note lambda body methods have unique names, so we don't need to remember desc here.) + aggregateInterfaceLambdaMethods.add(internalName + '#' + name); + return null; + } + if (BitFlags.isSet(access, Opcodes.ACC_PRIVATE)) { + // Make lambda body method accessible from lambda class + access &= ~Opcodes.ACC_PRIVATE; + // Method was private so it can be final, which should help VMs perform dispatch. + access |= Opcodes.ACC_FINAL; + } + } + MethodVisitor dest = super.visitMethod(access, name, desc, signature, exceptions); + return new InvokedynamicRewriter(dest); + } + + /** + * Makes {@link #visitEnd} generate a bridge method for the given method handle if the + * referenced method will be invisible to the generated lambda class. + * + * @return struct containing either {@code invokedMethod} or {@code invokedMethod} and a handle + * representing the bridge method that will be generated for {@code invokedMethod}. + */ + private MethodReferenceBridgeInfo queueUpBridgeMethodIfNeeded(Handle invokedMethod) + throws ClassNotFoundException { + if (invokedMethod.getName().startsWith("lambda$")) { + // We adjust lambda bodies to be visible + return MethodReferenceBridgeInfo.noBridge(invokedMethod); + } + + // invokedMethod is a method reference if we get here + Executable invoked = findTargetMethod(invokedMethod); + if (isVisibleToLambdaClass(invoked, invokedMethod.getOwner())) { + // Referenced method is visible to the generated class, so nothing to do + return MethodReferenceBridgeInfo.noBridge(invokedMethod); + } + + // We need a bridge method if we get here + checkState(!isInterface, + "%s is an interface and shouldn't need bridge to %s", internalName, invokedMethod); + checkState(!invokedMethod.isInterface(), + "%s's lambda classes can't see interface method: %s", internalName, invokedMethod); + MethodReferenceBridgeInfo result = bridgeMethods.get(invokedMethod); + if (result != null) { + return result; // we're already queued up a bridge method for this method reference + } + + String name = "bridge$lambda$" + bridgeMethods.size(); + Handle bridgeMethod; + switch (invokedMethod.getTag()) { + case Opcodes.H_INVOKESTATIC: + bridgeMethod = new Handle(invokedMethod.getTag(), internalName, name, + invokedMethod.getDesc(), /*itf*/ false); + break; + case Opcodes.H_INVOKEVIRTUAL: + case Opcodes.H_INVOKESPECIAL: // we end up calling these using invokevirtual + bridgeMethod = new Handle(Opcodes.H_INVOKEVIRTUAL, internalName, name, + invokedMethod.getDesc(), /*itf*/ false); + break; + case Opcodes.H_NEWINVOKESPECIAL: { + // Call invisible constructor through generated bridge "factory" method, so we need to + // compute the descriptor for the bridge method from the constructor's descriptor + String desc = + Type.getMethodDescriptor( + Type.getObjectType(invokedMethod.getOwner()), + Type.getArgumentTypes(invokedMethod.getDesc())); + bridgeMethod = new Handle(Opcodes.H_INVOKESTATIC, internalName, name, desc, /*itf*/ false); + break; + } + case Opcodes.H_INVOKEINTERFACE: + // Shouldn't get here + default: + throw new UnsupportedOperationException("Cannot bridge " + invokedMethod); + } + result = MethodReferenceBridgeInfo.bridge(invokedMethod, invoked, bridgeMethod); + MethodReferenceBridgeInfo old = bridgeMethods.put(invokedMethod, result); + checkState(old == null, "Already had bridge %s so we don't also want %s", old, result); + return result; + } + + /** + * Checks whether the referenced method would be visible by an unrelated class in the same package + * as the currently visited class. + */ + private boolean isVisibleToLambdaClass(Executable invoked, String owner) { + int modifiers = invoked.getModifiers(); + if (Modifier.isPrivate(modifiers)) { + return false; + } + if (Modifier.isPublic(modifiers)) { + return true; + } + // invoked is protected or package-private, either way we need it to be in the same package + // because the additional visibility protected gives doesn't help lambda classes, which are in + // a different class hierarchy (and typically just extend Object) + return packageName(internalName).equals(packageName(owner)); + } + + private Executable findTargetMethod(Handle invokedMethod) throws ClassNotFoundException { + Type descriptor = Type.getMethodType(invokedMethod.getDesc()); + Class<?> owner = loadFromInternal(invokedMethod.getOwner()); + if (invokedMethod.getTag() == Opcodes.H_NEWINVOKESPECIAL) { + for (Constructor<?> c : owner.getDeclaredConstructors()) { + if (Type.getType(c).equals(descriptor)) { + return c; + } + } + } else { + for (Method m : owner.getDeclaredMethods()) { + if (m.getName().equals(invokedMethod.getName()) + && Type.getType(m).equals(descriptor)) { + return m; + } + } + } + throw new IllegalArgumentException("Referenced method not found: " + invokedMethod); + } + + private Class<?> loadFromInternal(String internalName) throws ClassNotFoundException { + return targetLoader.loadClass(internalName.replace('/', '.')); + } + + static int invokeOpcode(Handle invokedMethod) { + switch (invokedMethod.getTag()) { + case Opcodes.H_INVOKESTATIC: + return Opcodes.INVOKESTATIC; + case Opcodes.H_INVOKEVIRTUAL: + return Opcodes.INVOKEVIRTUAL; + case Opcodes.H_INVOKESPECIAL: + case Opcodes.H_NEWINVOKESPECIAL: // Must be preceded by NEW + return Opcodes.INVOKESPECIAL; + case Opcodes.H_INVOKEINTERFACE: + return Opcodes.INVOKEINTERFACE; + default: + throw new UnsupportedOperationException("Don't know how to call " + invokedMethod); + } + } + + private static String[] toInternalNames(Class<?>[] classes) { + String[] result = new String[classes.length]; + for (int i = 0; i < classes.length; ++i) { + result[i] = Type.getInternalName(classes[i]); + } + return result; + } + + private static String packageName(String internalClassName) { + int lastSlash = internalClassName.lastIndexOf('/'); + return lastSlash > 0 ? internalClassName.substring(0, lastSlash) : ""; + } + + /** + * Desugaring that replaces invokedynamics for {@link java.lang.invoke.LambdaMetafactory} with + * static factory method invocations and triggers a class to be generated for each invokedynamic. + */ + private class InvokedynamicRewriter extends MethodVisitor { + + public InvokedynamicRewriter(MethodVisitor dest) { + super(ASM5, dest); + } + + @Override + public void visitInvokeDynamicInsn(String name, String desc, Handle bsm, Object... bsmArgs) { + if (!"java/lang/invoke/LambdaMetafactory".equals(bsm.getOwner())) { + // Not an invokedynamic for a lambda expression + super.visitInvokeDynamicInsn(name, desc, bsm, bsmArgs); + return; + } + + try { + Lookup lookup = createLookup(internalName); + ArrayList<Object> args = new ArrayList<>(bsmArgs.length + 3); + args.add(lookup); + args.add(name); + args.add(MethodType.fromMethodDescriptorString(desc, targetLoader)); + for (Object bsmArg : bsmArgs) { + args.add(toJvmMetatype(lookup, bsmArg)); + } + + // Both bootstrap methods in LambdaMetafactory expect a MethodHandle as their 5th argument + // so we can assume bsmArgs[1] (the 5th arg) to be a Handle. + MethodReferenceBridgeInfo bridgeInfo = queueUpBridgeMethodIfNeeded((Handle) bsmArgs[1]); + + // Resolve the bootstrap method in "host configuration" (this tool's default classloader) + // since targetLoader may only contain stubs that we can't actually execute. + // generateLambdaClass() below will invoke the bootstrap method, so a stub isn't enough, + // and ultimately we don't care if the bootstrap method was even on the bootclasspath + // when this class was compiled (although it must've been since javac is unhappy otherwise). + MethodHandle bsmMethod = toMethodHandle(publicLookup(), bsm, /*target*/ false); + String lambdaClassName = lambdas.generateLambdaClass( + internalName, + LambdaInfo.create(desc, bridgeInfo.methodReference(), bridgeInfo.bridgeMethod()), + bsmMethod, + args); + // Emit invokestatic that calls the factory method generated in the lambda class + super.visitMethodInsn( + Opcodes.INVOKESTATIC, + lambdaClassName, + LambdaClassFixer.FACTORY_METHOD_NAME, + desc, + /*itf*/ false); + } catch (IOException | ReflectiveOperationException e) { + throw new IllegalStateException("Couldn't desugar invokedynamic for " + internalName + "." + + name + " using " + bsm + " with arguments " + Arrays.toString(bsmArgs), e); + } + } + + private Lookup createLookup(String lookupClass) throws ReflectiveOperationException { + Class<?> clazz = loadFromInternal(lookupClass); + Constructor<Lookup> constructor = Lookup.class.getDeclaredConstructor(Class.class); + constructor.setAccessible(true); + return constructor.newInstance(clazz); + } + + /** + * Produces a {@link MethodHandle} or {@link MethodType} using {@link #targetLoader} for the + * given ASM {@link Handle} or {@link Type}. {@code lookup} is only used for resolving + * {@link Handle}s. + */ + private Object toJvmMetatype(Lookup lookup, Object asm) throws ReflectiveOperationException { + if (asm instanceof Number) { + return asm; + } + if (asm instanceof Type) { + Type type = (Type) asm; + switch (type.getSort()) { + case Type.OBJECT: + return loadFromInternal(type.getInternalName()); + case Type.METHOD: + return MethodType.fromMethodDescriptorString(type.getDescriptor(), targetLoader); + default: + throw new IllegalArgumentException("Cannot convert: " + asm); + } + } + if (asm instanceof Handle) { + return toMethodHandle(lookup, (Handle) asm, /*target*/ true); + } + throw new IllegalArgumentException("Cannot convert: " + asm); + } + + /** + * Produces a {@link MethodHandle} using either the context or {@link #targetLoader} class + * loader, depending on {@code target}. + */ + private MethodHandle toMethodHandle(Lookup lookup, Handle asmHandle, boolean target) + throws ReflectiveOperationException { + Class<?> owner = loadFromInternal(asmHandle.getOwner()); + MethodType signature = MethodType.fromMethodDescriptorString(asmHandle.getDesc(), + target ? targetLoader : Thread.currentThread().getContextClassLoader()); + switch (asmHandle.getTag()) { + case Opcodes.H_INVOKESTATIC: + return lookup.findStatic(owner, asmHandle.getName(), signature); + case Opcodes.H_INVOKEVIRTUAL: + case Opcodes.H_INVOKESPECIAL: // we end up calling these using invokevirtual + case Opcodes.H_INVOKEINTERFACE: + return lookup.findVirtual(owner, asmHandle.getName(), signature); + case Opcodes.H_NEWINVOKESPECIAL: + return lookup.findConstructor(owner, signature); + default: + throw new UnsupportedOperationException("Cannot resolve " + asmHandle); + } + } + } + + /** + * Record of how a lambda class can reach its referenced method through a possibly-different + * bridge method. + * + * <p>In a JVM, lambda classes are allowed to call the referenced methods directly, but we don't + * have that luxury when the generated lambda class is evaluated using normal visibility rules. + */ + @AutoValue + abstract static class MethodReferenceBridgeInfo { + public static MethodReferenceBridgeInfo noBridge(Handle methodReference) { + return new AutoValue_LambdaDesugaring_MethodReferenceBridgeInfo( + methodReference, (Executable) null, methodReference); + } + public static MethodReferenceBridgeInfo bridge( + Handle methodReference, Executable referenced, Handle bridgeMethod) { + checkArgument(!bridgeMethod.equals(methodReference)); + return new AutoValue_LambdaDesugaring_MethodReferenceBridgeInfo( + methodReference, checkNotNull(referenced), bridgeMethod); + } + + public abstract Handle methodReference(); + + /** Returns {@code null} iff {@link #bridgeMethod} equals {@link #methodReference}. */ + @Nullable public abstract Executable referenced(); + + public abstract Handle bridgeMethod(); + } +} diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/LambdaInfo.java b/src/tools/android/java/com/google/devtools/build/android/desugar/LambdaInfo.java new file mode 100644 index 0000000000..e3bb84f891 --- /dev/null +++ b/src/tools/android/java/com/google/devtools/build/android/desugar/LambdaInfo.java @@ -0,0 +1,30 @@ +// Copyright 2016 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 com.google.auto.value.AutoValue; +import org.objectweb.asm.Handle; + +@AutoValue +abstract class LambdaInfo { + public static LambdaInfo create( + String factoryMethodDesc, Handle methodReference, Handle bridgeMethod) { + return new AutoValue_LambdaInfo( + factoryMethodDesc, methodReference, bridgeMethod); + } + + public abstract String factoryMethodDesc(); + public abstract Handle methodReference(); + public abstract Handle bridgeMethod(); +} |