summaryrefslogtreecommitdiff
path: root/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java
diff options
context:
space:
mode:
Diffstat (limited to 'java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java')
-rw-r--r--java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java521
1 files changed, 521 insertions, 0 deletions
diff --git a/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java b/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java
new file mode 100644
index 0000000..f247074
--- /dev/null
+++ b/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java
@@ -0,0 +1,521 @@
+// Copyright 2018 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.util.stream.Stream.concat;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.LinkedHashMultimap;
+import com.google.common.collect.Multimap;
+import com.google.devtools.build.android.desugar.io.BitFlags;
+import com.google.devtools.build.android.desugar.io.CoreLibraryRewriter;
+import com.google.errorprone.annotations.Immutable;
+import java.lang.reflect.Method;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import javax.annotation.Nullable;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.Label;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
+
+/**
+ * Helper that keeps track of which core library classes and methods we want to rewrite.
+ */
+class CoreLibrarySupport {
+
+ private static final Object[] EMPTY_FRAME = new Object[0];
+ private static final String[] EMPTY_LIST = new String[0];
+
+ private final CoreLibraryRewriter rewriter;
+ private final ClassLoader targetLoader;
+ /** Internal name prefixes that we want to move to a custom package. */
+ private final ImmutableSet<String> renamedPrefixes;
+ private final ImmutableSet<String> excludeFromEmulation;
+ /** Internal names of interfaces whose default and static interface methods we'll emulate. */
+ private final ImmutableSet<Class<?>> emulatedInterfaces;
+ /** Map from {@code owner#name} core library members to their new owners. */
+ private final ImmutableMap<String, String> memberMoves;
+
+ /** For the collection of definitions of emulated default methods (deterministic iteration). */
+ private final Multimap<String, EmulatedMethod> emulatedDefaultMethods =
+ LinkedHashMultimap.create();
+
+ public CoreLibrarySupport(
+ CoreLibraryRewriter rewriter,
+ ClassLoader targetLoader,
+ List<String> renamedPrefixes,
+ List<String> emulatedInterfaces,
+ List<String> memberMoves,
+ List<String> excludeFromEmulation) {
+ this.rewriter = rewriter;
+ this.targetLoader = targetLoader;
+ checkArgument(
+ renamedPrefixes.stream().allMatch(prefix -> prefix.startsWith("java/")), renamedPrefixes);
+ this.renamedPrefixes = ImmutableSet.copyOf(renamedPrefixes);
+ this.excludeFromEmulation = ImmutableSet.copyOf(excludeFromEmulation);
+
+ ImmutableSet.Builder<Class<?>> classBuilder = ImmutableSet.builder();
+ for (String itf : emulatedInterfaces) {
+ checkArgument(itf.startsWith("java/util/"), itf);
+ Class<?> clazz = loadFromInternal(rewriter.getPrefix() + itf);
+ checkArgument(clazz.isInterface(), itf);
+ classBuilder.add(clazz);
+ }
+ this.emulatedInterfaces = classBuilder.build();
+
+ // We can call isRenamed and rename below b/c we initialized the necessary fields above
+ // Use LinkedHashMap to tolerate identical duplicates
+ LinkedHashMap<String, String> movesBuilder = new LinkedHashMap<>();
+ Splitter splitter = Splitter.on("->").trimResults().omitEmptyStrings();
+ for (String move : memberMoves) {
+ List<String> pair = splitter.splitToList(move);
+ checkArgument(pair.size() == 2, "Doesn't split as expected: %s", move);
+ checkArgument(pair.get(0).startsWith("java/"), "Unexpected member: %s", move);
+ int sep = pair.get(0).indexOf('#');
+ checkArgument(sep > 0 && sep == pair.get(0).lastIndexOf('#'), "invalid member: %s", move);
+ checkArgument(!isRenamedCoreLibrary(pair.get(0).substring(0, sep)),
+ "Original renamed, no need to move it: %s", move);
+ checkArgument(isRenamedCoreLibrary(pair.get(1)), "Target not renamed: %s", move);
+ checkArgument(!this.excludeFromEmulation.contains(pair.get(0)),
+ "Retargeted invocation %s shouldn't overlap with excluded", move);
+
+ String value = renameCoreLibrary(pair.get(1));
+ String existing = movesBuilder.put(pair.get(0), value);
+ checkArgument(existing == null || existing.equals(value),
+ "Two move destinations %s and %s configured for %s", existing, value, pair.get(0));
+ }
+ this.memberMoves = ImmutableMap.copyOf(movesBuilder);
+ }
+
+ public boolean isRenamedCoreLibrary(String internalName) {
+ String unprefixedName = rewriter.unprefix(internalName);
+ if (!unprefixedName.startsWith("java/") || renamedPrefixes.isEmpty()) {
+ return false; // shortcut
+ }
+ // Rename any classes desugar might generate under java/ (for emulated interfaces) as well as
+ // configured prefixes
+ return looksGenerated(unprefixedName)
+ || renamedPrefixes.stream().anyMatch(prefix -> unprefixedName.startsWith(prefix));
+ }
+
+ public String renameCoreLibrary(String internalName) {
+ internalName = rewriter.unprefix(internalName);
+ return (internalName.startsWith("java/"))
+ ? "j$/" + internalName.substring(/* cut away "java/" prefix */ 5)
+ : internalName;
+ }
+
+ @Nullable
+ public String getMoveTarget(String owner, String name) {
+ return memberMoves.get(rewriter.unprefix(owner) + '#' + name);
+ }
+
+ /**
+ * Returns {@code true} for java.* classes or interfaces that are subtypes of emulated interfaces.
+ * Note that implies that this method always returns {@code false} for user-written classes.
+ */
+ public boolean isEmulatedCoreClassOrInterface(String internalName) {
+ return getEmulatedCoreClassOrInterface(internalName) != null;
+ }
+
+ /** Includes the given method definition in any applicable core interface emulation logic. */
+ public void registerIfEmulatedCoreInterface(
+ int access,
+ String owner,
+ String name,
+ String desc,
+ String[] exceptions) {
+ Class<?> emulated = getEmulatedCoreClassOrInterface(owner);
+ if (emulated == null) {
+ return;
+ }
+ checkArgument(emulated.isInterface(), "Shouldn't be called for a class: %s.%s", owner, name);
+ checkArgument(
+ BitFlags.noneSet(
+ access,
+ Opcodes.ACC_ABSTRACT | Opcodes.ACC_NATIVE | Opcodes.ACC_STATIC | Opcodes.ACC_BRIDGE),
+ "Should only be called for default methods: %s.%s", owner, name);
+ emulatedDefaultMethods.put(
+ name + ":" + desc, EmulatedMethod.create(access, emulated, name, desc, exceptions));
+ }
+
+ /**
+ * If the given invocation needs to go through a companion class of an emulated or renamed
+ * core interface, this methods returns that interface. This is a helper method for
+ * {@link CoreLibraryInvocationRewriter}.
+ *
+ * <p>This method can only return non-{@code null} if {@code owner} is a core library type.
+ * It usually returns an emulated interface, unless the given invocation is a super-call to a
+ * core class's implementation of an emulated method that's being moved (other implementations
+ * of emulated methods in core classes are ignored). In that case the class is returned and the
+ * caller can use {@link #getMoveTarget} to find out where to redirect the invokespecial to.
+ */
+ // TODO(kmb): Rethink this API and consider combining it with getMoveTarget().
+ @Nullable
+ public Class<?> getCoreInterfaceRewritingTarget(
+ int opcode, String owner, String name, String desc, boolean itf) {
+ if (looksGenerated(owner)) {
+ // Regular desugaring handles generated classes, no emulation is needed
+ return null;
+ }
+ if (!itf && opcode == Opcodes.INVOKESTATIC) {
+ // Ignore static invocations on classes--they never need rewriting (unless moved but that's
+ // handled separately).
+ return null;
+ }
+ if ("<init>".equals(name)) {
+ return null; // Constructors aren't rewritten
+ }
+
+ Class<?> clazz;
+ if (isRenamedCoreLibrary(owner)) {
+ // For renamed invocation targets we just need to do what InterfaceDesugaring does, that is,
+ // only worry about invokestatic and invokespecial interface invocations; nothing to do for
+ // classes and invokeinterface. InterfaceDesugaring ignores bootclasspath interfaces,
+ // so we have to do its work here for renamed interfaces.
+ if (itf
+ && (opcode == Opcodes.INVOKESTATIC || opcode == Opcodes.INVOKESPECIAL)) {
+ clazz = loadFromInternal(owner);
+ } else {
+ return null;
+ }
+ } else {
+ // If not renamed, see if the owner needs emulation.
+ clazz = getEmulatedCoreClassOrInterface(owner);
+ if (clazz == null) {
+ return null;
+ }
+ }
+ checkArgument(itf == clazz.isInterface(), "%s expected to be interface: %s", owner, itf);
+
+ if (opcode == Opcodes.INVOKESTATIC) {
+ // Static interface invocation always goes to the given owner
+ checkState(itf); // we should've bailed out above.
+ return clazz;
+ }
+
+ // See if the invoked method is a default method, which will need rewriting. For invokespecial
+ // we can only get here if its a default method, and invokestatic we handled above.
+ Method callee = findInterfaceMethod(clazz, name, desc);
+ if (callee != null && callee.isDefault()) {
+ if (isExcluded(callee)) {
+ return null;
+ }
+
+ if (!itf && opcode == Opcodes.INVOKESPECIAL) {
+ // See if the invoked implementation is moved; note we ignore all other overrides in classes
+ Class<?> impl = clazz; // we know clazz is not an interface because !itf
+ while (impl != null) {
+ String implName = impl.getName().replace('.', '/');
+ if (getMoveTarget(implName, name) != null) {
+ return impl;
+ }
+ impl = impl.getSuperclass();
+ }
+ }
+
+ Class<?> result = callee.getDeclaringClass();
+ if (isRenamedCoreLibrary(result.getName().replace('.', '/'))
+ || emulatedInterfaces.stream().anyMatch(emulated -> emulated.isAssignableFrom(result))) {
+ return result;
+ }
+ // We get here if the declaring class is a supertype of an emulated interface. In that case
+ // use the emulated interface instead (since we don't desugar the supertype). Fail in case
+ // there are multiple possibilities.
+ Iterator<Class<?>> roots =
+ emulatedInterfaces
+ .stream()
+ .filter(
+ emulated -> emulated.isAssignableFrom(clazz) && result.isAssignableFrom(emulated))
+ .iterator();
+ checkState(roots.hasNext()); // must exist
+ Class<?> substitute = roots.next();
+ checkState(!roots.hasNext(), "Ambiguous emulation substitute: %s", callee);
+ return substitute;
+ } else {
+ checkArgument(!itf || opcode != Opcodes.INVOKESPECIAL,
+ "Couldn't resolve interface super call %s.super.%s : %s", owner, name, desc);
+ }
+ return null;
+ }
+
+ /**
+ * Returns the given class if it's a core library class or interface with emulated default
+ * methods. This is equivalent to calling {@link #isEmulatedCoreClassOrInterface} and then
+ * just loading the class (using the target class loader).
+ */
+ public Class<?> getEmulatedCoreClassOrInterface(String internalName) {
+ if (looksGenerated(internalName)) {
+ // Regular desugaring handles generated classes, no emulation is needed
+ return null;
+ }
+ {
+ String unprefixedOwner = rewriter.unprefix(internalName);
+ if (!unprefixedOwner.startsWith("java/util/") || isRenamedCoreLibrary(unprefixedOwner)) {
+ return null;
+ }
+ }
+
+ Class<?> clazz = loadFromInternal(internalName);
+ if (emulatedInterfaces.stream().anyMatch(itf -> itf.isAssignableFrom(clazz))) {
+ return clazz;
+ }
+ return null;
+ }
+
+ public void makeDispatchHelpers(GeneratedClassStore store) {
+ HashMap<Class<?>, ClassVisitor> dispatchHelpers = new HashMap<>();
+ for (Collection<EmulatedMethod> group : emulatedDefaultMethods.asMap().values()) {
+ checkState(!group.isEmpty());
+ Class<?> root = group
+ .stream()
+ .map(EmulatedMethod::owner)
+ .max(DefaultMethodClassFixer.SubtypeComparator.INSTANCE)
+ .get();
+ checkState(group.stream().map(m -> m.owner()).allMatch(o -> root.isAssignableFrom(o)),
+ "Not a single unique method: %s", group);
+ String methodName = group.stream().findAny().get().name();
+
+ ImmutableList<Class<?>> customOverrides = findCustomOverrides(root, methodName);
+
+ for (EmulatedMethod methodDefinition : group) {
+ Class<?> owner = methodDefinition.owner();
+ ClassVisitor dispatchHelper = dispatchHelpers.computeIfAbsent(owner, clazz -> {
+ String className = clazz.getName().replace('.', '/') + "$$Dispatch";
+ ClassVisitor result = store.add(className);
+ result.visit(
+ Opcodes.V1_7,
+ // Must be public so dispatch methods can be called from anywhere
+ Opcodes.ACC_SYNTHETIC | Opcodes.ACC_PUBLIC,
+ className,
+ /*signature=*/ null,
+ "java/lang/Object",
+ EMPTY_LIST);
+ return result;
+ });
+
+ // Types to check for before calling methodDefinition's companion, sub- before super-types
+ ImmutableList<Class<?>> typechecks =
+ concat(group.stream().map(EmulatedMethod::owner), customOverrides.stream())
+ .filter(o -> o != owner && owner.isAssignableFrom(o))
+ .distinct() // should already be but just in case
+ .sorted(DefaultMethodClassFixer.SubtypeComparator.INSTANCE)
+ .collect(ImmutableList.toImmutableList());
+ makeDispatchHelperMethod(dispatchHelper, methodDefinition, typechecks);
+ }
+ }
+ }
+
+ private ImmutableList<Class<?>> findCustomOverrides(Class<?> root, String methodName) {
+ ImmutableList.Builder<Class<?>> customOverrides = ImmutableList.builder();
+ for (ImmutableMap.Entry<String, String> move : memberMoves.entrySet()) {
+ // move.getKey is a string <owner>#<name> which we validated in the constructor.
+ // We need to take the string apart here to compare owner and name separately.
+ if (!methodName.equals(move.getKey().substring(move.getKey().indexOf('#') + 1))) {
+ continue;
+ }
+ Class<?> target =
+ loadFromInternal(
+ rewriter.getPrefix() + move.getKey().substring(0, move.getKey().indexOf('#')));
+ if (!root.isAssignableFrom(target)) {
+ continue;
+ }
+ checkState(!target.isInterface(), "can't move emulated interface method: %s", move);
+ customOverrides.add(target);
+ }
+ return customOverrides.build();
+ }
+
+ private void makeDispatchHelperMethod(
+ ClassVisitor helper, EmulatedMethod method, ImmutableList<Class<?>> typechecks) {
+ checkArgument(method.owner().isInterface());
+ String owner = method.owner().getName().replace('.', '/');
+ Type methodType = Type.getMethodType(method.descriptor());
+ String companionDesc =
+ InterfaceDesugaring.companionDefaultMethodDescriptor(owner, method.descriptor());
+ MethodVisitor dispatchMethod =
+ helper.visitMethod(
+ method.access() | Opcodes.ACC_STATIC,
+ method.name(),
+ companionDesc,
+ /*signature=*/ null, // signature is invalid due to extra "receiver" argument
+ method.exceptions().toArray(EMPTY_LIST));
+
+
+ dispatchMethod.visitCode();
+ {
+ // See if the receiver might come with its own implementation of the method, and call it.
+ // We do this by testing for the interface type created by EmulatedInterfaceRewriter
+ Label fallthrough = new Label();
+ String emulationInterface = renameCoreLibrary(owner);
+ dispatchMethod.visitVarInsn(Opcodes.ALOAD, 0); // load "receiver"
+ dispatchMethod.visitTypeInsn(Opcodes.INSTANCEOF, emulationInterface);
+ dispatchMethod.visitJumpInsn(Opcodes.IFEQ, fallthrough);
+ dispatchMethod.visitVarInsn(Opcodes.ALOAD, 0); // load "receiver"
+ dispatchMethod.visitTypeInsn(Opcodes.CHECKCAST, emulationInterface);
+
+ visitLoadArgs(dispatchMethod, methodType, 1 /* receiver already loaded above */);
+ dispatchMethod.visitMethodInsn(
+ Opcodes.INVOKEINTERFACE,
+ emulationInterface,
+ method.name(),
+ method.descriptor(),
+ /*itf=*/ true);
+ dispatchMethod.visitInsn(methodType.getReturnType().getOpcode(Opcodes.IRETURN));
+
+ dispatchMethod.visitLabel(fallthrough);
+ // Trivial frame for the branch target: same empty stack as before
+ dispatchMethod.visitFrame(Opcodes.F_SAME, 0, EMPTY_FRAME, 0, EMPTY_FRAME);
+ }
+
+ // Next, check for subtypes with specialized implementations and call them
+ for (Class<?> tested : typechecks) {
+ Label fallthrough = new Label();
+ String testedName = tested.getName().replace('.', '/');
+ // In case of a class this must be a member move; for interfaces use the companion.
+ String target =
+ tested.isInterface()
+ ? InterfaceDesugaring.getCompanionClassName(testedName)
+ : checkNotNull(memberMoves.get(rewriter.unprefix(testedName) + '#' + method.name()));
+ dispatchMethod.visitVarInsn(Opcodes.ALOAD, 0); // load "receiver"
+ dispatchMethod.visitTypeInsn(Opcodes.INSTANCEOF, testedName);
+ dispatchMethod.visitJumpInsn(Opcodes.IFEQ, fallthrough);
+ dispatchMethod.visitVarInsn(Opcodes.ALOAD, 0); // load "receiver"
+ dispatchMethod.visitTypeInsn(Opcodes.CHECKCAST, testedName); // make verifier happy
+
+ visitLoadArgs(dispatchMethod, methodType, 1 /* receiver already loaded above */);
+ dispatchMethod.visitMethodInsn(
+ Opcodes.INVOKESTATIC,
+ target,
+ method.name(),
+ InterfaceDesugaring.companionDefaultMethodDescriptor(testedName, method.descriptor()),
+ /*itf=*/ false);
+ dispatchMethod.visitInsn(methodType.getReturnType().getOpcode(Opcodes.IRETURN));
+
+ dispatchMethod.visitLabel(fallthrough);
+ // Trivial frame for the branch target: same empty stack as before
+ dispatchMethod.visitFrame(Opcodes.F_SAME, 0, EMPTY_FRAME, 0, EMPTY_FRAME);
+ }
+
+ // Call static type's default implementation in companion class
+ dispatchMethod.visitVarInsn(Opcodes.ALOAD, 0); // load "receiver"
+ visitLoadArgs(dispatchMethod, methodType, 1 /* receiver already loaded above */);
+ dispatchMethod.visitMethodInsn(
+ Opcodes.INVOKESTATIC,
+ InterfaceDesugaring.getCompanionClassName(owner),
+ method.name(),
+ companionDesc,
+ /*itf=*/ false);
+ dispatchMethod.visitInsn(methodType.getReturnType().getOpcode(Opcodes.IRETURN));
+
+ dispatchMethod.visitMaxs(0, 0);
+ dispatchMethod.visitEnd();
+ }
+
+ private boolean isExcluded(Method method) {
+ String unprefixedOwner =
+ rewriter.unprefix(method.getDeclaringClass().getName().replace('.', '/'));
+ return excludeFromEmulation.contains(unprefixedOwner + "#" + method.getName());
+ }
+
+ private Class<?> loadFromInternal(String internalName) {
+ try {
+ return targetLoader.loadClass(internalName.replace('/', '.'));
+ } catch (ClassNotFoundException e) {
+ throw (NoClassDefFoundError) new NoClassDefFoundError().initCause(e);
+ }
+ }
+
+ private static Method findInterfaceMethod(Class<?> clazz, String name, String desc) {
+ return collectImplementedInterfaces(clazz, new LinkedHashSet<>())
+ .stream()
+ // search more subtypes before supertypes
+ .sorted(DefaultMethodClassFixer.SubtypeComparator.INSTANCE)
+ .map(itf -> findMethod(itf, name, desc))
+ .filter(Objects::nonNull)
+ .findFirst()
+ .orElse((Method) null);
+ }
+
+ private static Method findMethod(Class<?> clazz, String name, String desc) {
+ for (Method m : clazz.getMethods()) {
+ if (m.getName().equals(name) && Type.getMethodDescriptor(m).equals(desc)) {
+ return m;
+ }
+ }
+ return null;
+ }
+
+ private static Set<Class<?>> collectImplementedInterfaces(Class<?> clazz, Set<Class<?>> dest) {
+ if (clazz.isInterface()) {
+ if (!dest.add(clazz)) {
+ return dest;
+ }
+ } else if (clazz.getSuperclass() != null) {
+ collectImplementedInterfaces(clazz.getSuperclass(), dest);
+ }
+
+ for (Class<?> itf : clazz.getInterfaces()) {
+ collectImplementedInterfaces(itf, dest);
+ }
+ return dest;
+ }
+
+ /**
+ * Emits instructions to load a method's parameters as arguments of a method call assumed to have
+ * compatible descriptor, starting at the given local variable slot.
+ */
+ private static void visitLoadArgs(MethodVisitor dispatchMethod, Type neededType, int slot) {
+ for (Type arg : neededType.getArgumentTypes()) {
+ dispatchMethod.visitVarInsn(arg.getOpcode(Opcodes.ILOAD), slot);
+ slot += arg.getSize();
+ }
+ }
+
+ /** Checks whether the given class is (likely) generated by desugar itself. */
+ private static boolean looksGenerated(String owner) {
+ return owner.contains("$$Lambda$") || owner.endsWith("$$CC") || owner.endsWith("$$Dispatch");
+ }
+
+ @AutoValue
+ @Immutable
+ abstract static class EmulatedMethod {
+ public static EmulatedMethod create(
+ int access, Class<?> owner, String name, String desc, @Nullable String[] exceptions) {
+ return new AutoValue_CoreLibrarySupport_EmulatedMethod(access, owner, name, desc,
+ exceptions != null ? ImmutableList.copyOf(exceptions) : ImmutableList.of());
+ }
+
+ abstract int access();
+ abstract Class<?> owner();
+ abstract String name();
+ abstract String descriptor();
+ abstract ImmutableList<String> exceptions();
+ }
+}