send invocations to emulated interfaces through dispatch helper.
fix logic for implementing emulated interfaces.
RELNOTES: None.

PiperOrigin-RevId: 187520298
GitOrigin-RevId: 4b6c0ec4b54e258763ce22e1a7f529d293aff026
Change-Id: If35dfebaa31dc5ea170c945f0ae7b26edf260ba2
diff --git a/java/com/google/devtools/build/android/desugar/CoreLibraryInvocationRewriter.java b/java/com/google/devtools/build/android/desugar/CoreLibraryInvocationRewriter.java
index fb62219..0e0610f 100644
--- a/java/com/google/devtools/build/android/desugar/CoreLibraryInvocationRewriter.java
+++ b/java/com/google/devtools/build/android/desugar/CoreLibraryInvocationRewriter.java
@@ -79,8 +79,7 @@
           checkArgument(itf, "Expected interface to rewrite %s.%s : %s", owner, name, desc);
           owner = InterfaceDesugaring.getCompanionClassName(coreInterfaceName);
         } else {
-          // TODO(kmb): Simulate dynamic dispatch instead of calling most general default method
-          owner = InterfaceDesugaring.getCompanionClassName(coreInterfaceName);
+          owner = coreInterfaceName + "$$Dispatch";
         }
 
         opcode = Opcodes.INVOKESTATIC;
diff --git a/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java b/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java
index 90e6bc0..c73874e 100644
--- a/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java
+++ b/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java
@@ -20,12 +20,16 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import java.lang.reflect.Method;
+import java.util.HashMap;
 import java.util.Iterator;
 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;
 
@@ -34,6 +38,8 @@
  */
 class CoreLibrarySupport {
 
+  private static final Object[] EMPTY_FRAME = new Object[0];
+
   private final CoreLibraryRewriter rewriter;
   private final ClassLoader targetLoader;
   /** Internal name prefixes that we want to move to a custom package. */
@@ -42,15 +48,20 @@
   private final ImmutableSet<Class<?>> emulatedInterfaces;
   /** Map from {@code owner#name} core library members to their new owners. */
   private final ImmutableMap<String, String> memberMoves;
+  private final GeneratedClassStore store;
+
+  private final HashMap<String, ClassVisitor> dispatchHelpers = new HashMap<>();
 
   public CoreLibrarySupport(
       CoreLibraryRewriter rewriter,
       ClassLoader targetLoader,
+      GeneratedClassStore store,
       List<String> renamedPrefixes,
       List<String> emulatedInterfaces,
       List<String> memberMoves) {
     this.rewriter = rewriter;
     this.targetLoader = targetLoader;
+    this.store = store;
     checkArgument(
         renamedPrefixes.stream().allMatch(prefix -> prefix.startsWith("java/")), renamedPrefixes);
     this.renamedPrefixes = ImmutableSet.copyOf(renamedPrefixes);
@@ -88,8 +99,7 @@
     }
     // Rename any classes desugar might generate under java/ (for emulated interfaces) as well as
     // configured prefixes
-    return unprefixedName.contains("$$Lambda$")
-        || unprefixedName.endsWith("$$CC")
+    return looksGenerated(unprefixedName)
         || renamedPrefixes.stream().anyMatch(prefix -> unprefixedName.startsWith(prefix));
   }
 
@@ -113,6 +123,78 @@
     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);
+
+    ClassVisitor helper = dispatchHelper(owner);
+    String companionDesc = InterfaceDesugaring.companionDefaultMethodDescriptor(owner, desc);
+    MethodVisitor dispatchMethod =
+        helper.visitMethod(
+            access | Opcodes.ACC_STATIC,
+            name,
+            companionDesc,
+            /*signature=*/ null,  // signature is invalid due to extra "receiver" argument
+            exceptions);
+
+    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 callCompanion = new Label();
+      String emulationInterface = renameCoreLibrary(owner);
+      dispatchMethod.visitVarInsn(Opcodes.ALOAD, 0);  // load "receiver"
+      dispatchMethod.visitTypeInsn(Opcodes.INSTANCEOF, emulationInterface);
+      dispatchMethod.visitJumpInsn(Opcodes.IFEQ, callCompanion);
+      dispatchMethod.visitVarInsn(Opcodes.ALOAD, 0);  // load "receiver"
+      dispatchMethod.visitTypeInsn(Opcodes.CHECKCAST, emulationInterface);
+
+      Type neededType = Type.getMethodType(desc);
+      visitLoadArgs(dispatchMethod, neededType, 1 /* receiver already loaded above*/);
+      dispatchMethod.visitMethodInsn(
+          Opcodes.INVOKEINTERFACE,
+          emulationInterface,
+          name,
+          desc,
+          /*itf=*/ true);
+      dispatchMethod.visitInsn(neededType.getReturnType().getOpcode(Opcodes.IRETURN));
+
+      dispatchMethod.visitLabel(callCompanion);
+      // 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
+    Type neededType = Type.getMethodType(companionDesc);
+    visitLoadArgs(dispatchMethod, neededType, 0);
+    // TODO(b/70681189): Also test emulated subtypes and call their implementations before falling
+    // back on static type's default implementation
+    dispatchMethod.visitMethodInsn(
+        Opcodes.INVOKESTATIC,
+        InterfaceDesugaring.getCompanionClassName(owner),
+        name,
+        companionDesc,
+        /*itf=*/ false);
+    dispatchMethod.visitInsn(neededType.getReturnType().getOpcode(Opcodes.IRETURN));
+
+    dispatchMethod.visitMaxs(0, 0);
+    dispatchMethod.visitEnd();
+  }
+
   /**
    * 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
@@ -124,7 +206,7 @@
   @Nullable
   public Class<?> getCoreInterfaceRewritingTarget(
       int opcode, String owner, String name, String desc, boolean itf) {
-    if (owner.contains("$$Lambda$") || owner.endsWith("$$CC")) {
+    if (looksGenerated(owner)) {
       // Regular desugaring handles generated classes, no emulation is needed
       return null;
     }
@@ -188,8 +270,13 @@
     return null;
   }
 
-  private Class<?> getEmulatedCoreClassOrInterface(String internalName) {
-    if (internalName.contains("$$Lambda$") || internalName.endsWith("$$CC")) {
+  /**
+   * 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;
     }
@@ -215,6 +302,22 @@
     }
   }
 
+  private ClassVisitor dispatchHelper(String internalName) {
+    return dispatchHelpers.computeIfAbsent(internalName, className -> {
+      className += "$$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",
+          new String[0]);
+      return result;
+    });
+  }
+
   private static Method findInterfaceMethod(Class<?> clazz, String name, String desc) {
     return collectImplementedInterfaces(clazz, new LinkedHashSet<>())
         .stream()
@@ -249,4 +352,20 @@
     }
     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");
+  }
 }
diff --git a/java/com/google/devtools/build/android/desugar/Desugar.java b/java/com/google/devtools/build/android/desugar/Desugar.java
index 0fad8c2..dd1992a 100644
--- a/java/com/google/devtools/build/android/desugar/Desugar.java
+++ b/java/com/google/devtools/build/android/desugar/Desugar.java
@@ -406,6 +406,7 @@
               ? new CoreLibrarySupport(
                   rewriter,
                   loader,
+                  store,
                   options.rewriteCoreLibraryPrefixes,
                   options.emulateCoreLibraryInterfaces,
                   options.retargetCoreLibraryMembers)
@@ -719,6 +720,7 @@
                 visitor,
                 interfaceCache,
                 depsCollector,
+                coreLibrarySupport,
                 bootclasspathReader,
                 loader,
                 store,
@@ -800,6 +802,7 @@
                   visitor,
                   interfaceCache,
                   depsCollector,
+                  coreLibrarySupport,
                   bootclasspathReader,
                   loader,
                   store,
diff --git a/java/com/google/devtools/build/android/desugar/EmulatedInterfaceRewriter.java b/java/com/google/devtools/build/android/desugar/EmulatedInterfaceRewriter.java
index d3e786d..f066f2a 100644
--- a/java/com/google/devtools/build/android/desugar/EmulatedInterfaceRewriter.java
+++ b/java/com/google/devtools/build/android/desugar/EmulatedInterfaceRewriter.java
@@ -13,18 +13,21 @@
 // limitations under the License.
 package com.google.devtools.build.android.desugar;
 
-import java.util.ArrayList;
 import java.util.Collections;
+import java.util.LinkedHashSet;
 import org.objectweb.asm.ClassVisitor;
 import org.objectweb.asm.Opcodes;
 
 /**
  * Visitor that renames emulated interfaces and marks classes that extend emulated interfaces to
  * also implement the renamed interfaces.  {@link DefaultMethodClassFixer} makes sure the requisite
- * methods are present.  Doing this helps with dynamic dispatch on emulated interfaces.
+ * methods are present in all classes implementing the renamed interface.  Doing this helps with
+ * dynamic dispatch on emulated interfaces.
  */
 public class EmulatedInterfaceRewriter extends ClassVisitor {
 
+  private static final String[] EMPTY_ARRAY = new String[0];
+
   private final CoreLibrarySupport support;
 
   public EmulatedInterfaceRewriter(ClassVisitor dest, CoreLibrarySupport support) {
@@ -40,24 +43,47 @@
       String signature,
       String superName,
       String[] interfaces) {
-    boolean isEmulated = support.isEmulatedCoreClassOrInterface(name);
-    if (interfaces != null && interfaces.length > 0 && !isEmulated) {
-      // Make classes implementing emulated interfaces also implement the renamed interfaces we
-      // create below.
-      ArrayList<String> newInterfaces = new ArrayList<>(interfaces.length + 2);
-      Collections.addAll(newInterfaces, interfaces);
-      for (String itf : interfaces) {
-        if (support.isEmulatedCoreClassOrInterface(itf)) {
-          newInterfaces.add(support.renameCoreLibrary(itf));
+    boolean emulated = support.isEmulatedCoreClassOrInterface(name);
+    {
+      // 1. see if we should implement any additional interfaces.
+      // Use LinkedHashSet to dedupe but maintain deterministic order
+      LinkedHashSet<String> newInterfaces = new LinkedHashSet<>();
+      if (interfaces != null && interfaces.length > 0) {
+        // Make classes implementing emulated interfaces also implement the renamed interfaces we
+        // create below.  This includes making the renamed interfaces extends each other as needed.
+        Collections.addAll(newInterfaces, interfaces);
+        for (String itf : interfaces) {
+          if (support.isEmulatedCoreClassOrInterface(itf)) {
+            newInterfaces.add(support.renameCoreLibrary(itf));
+          }
         }
       }
-      if (interfaces.length != newInterfaces.size()) {
-        interfaces = newInterfaces.toArray(interfaces);
-        signature = null; // additional interfaces invalidate signature
+      if (!emulated) {
+        // For an immediate subclass of an emulated class, also fill in any interfaces implemented
+        // by superclasses, similar to the additional default method stubbing performed in
+        // DefaultMethodClassFixer in this situation.
+        Class<?> superclass = support.getEmulatedCoreClassOrInterface(superName);
+        while (superclass != null) {
+          for (Class<?> implemented : superclass.getInterfaces()) {
+            String itf = implemented.getName().replace('.', '/');
+            if (support.isEmulatedCoreClassOrInterface(itf)) {
+              newInterfaces.add(support.renameCoreLibrary(itf));
+            }
+          }
+          superclass = superclass.getSuperclass();
+        }
+      }
+      // Update implemented interfaces and signature if we did anything above
+      if (interfaces == null
+          ? !newInterfaces.isEmpty()
+          : interfaces.length != newInterfaces.size()) {
+        interfaces = newInterfaces.toArray(EMPTY_ARRAY);
+        signature = null; // additional interfaces invalidate any signature
       }
     }
 
-    if (BitFlags.isInterface(access) && isEmulated) {
+    // 2. see if we need to rename this interface itself
+    if (BitFlags.isInterface(access) && emulated) {
       name = support.renameCoreLibrary(name);
     }
     super.visit(version, access, name, signature, superName, interfaces);
diff --git a/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java b/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java
index f17f114..0a10df1 100644
--- a/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java
+++ b/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java
@@ -47,6 +47,7 @@
 
   private final ClassVsInterface interfaceCache;
   private final DependencyCollector depsCollector;
+  private final CoreLibrarySupport coreLibrarySupport;
   private final ClassReaderFactory bootclasspath;
   private final ClassLoader targetLoader;
   private final GeneratedClassStore store;
@@ -63,6 +64,7 @@
       ClassVisitor dest,
       ClassVsInterface interfaceCache,
       DependencyCollector depsCollector,
+      @Nullable CoreLibrarySupport coreLibrarySupport,
       ClassReaderFactory bootclasspath,
       ClassLoader targetLoader,
       GeneratedClassStore store,
@@ -70,6 +72,7 @@
     super(Opcodes.ASM6, dest);
     this.interfaceCache = interfaceCache;
     this.depsCollector = depsCollector;
+    this.coreLibrarySupport = coreLibrarySupport;
     this.bootclasspath = bootclasspath;
     this.targetLoader = targetLoader;
     this.store = store;
@@ -214,6 +217,10 @@
               internalName,
               desc);
           ++numberOfDefaultMethods;
+          if (coreLibrarySupport != null) {
+            coreLibrarySupport.registerIfEmulatedCoreInterface(
+                access, internalName, name, desc, exceptions);
+          }
           abstractDest =
               super.visitMethod(access | Opcodes.ACC_ABSTRACT, name, desc, signature, exceptions);
         }
diff --git a/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java b/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java
index cc17748..90350ce 100644
--- a/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java
+++ b/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java
@@ -34,6 +34,7 @@
         new CoreLibrarySupport(
             new CoreLibraryRewriter(""),
             null,
+            null,
             ImmutableList.of("java/time/"),
             ImmutableList.of(),
             ImmutableList.of());
@@ -51,6 +52,7 @@
         new CoreLibrarySupport(
             new CoreLibraryRewriter("__/"),
             null,
+            null,
             ImmutableList.of("java/time/"),
             ImmutableList.of(),
             ImmutableList.of());
@@ -67,6 +69,7 @@
         new CoreLibrarySupport(
             new CoreLibraryRewriter(""),
             null,
+            null,
             ImmutableList.of(),
             ImmutableList.of(),
             ImmutableList.of());
@@ -80,6 +83,7 @@
         new CoreLibrarySupport(
             new CoreLibraryRewriter("__/"),
             null,
+            null,
             ImmutableList.of(),
             ImmutableList.of(),
             ImmutableList.of());
@@ -93,6 +97,7 @@
         new CoreLibrarySupport(
             new CoreLibraryRewriter("__/"),
             null,
+            null,
             ImmutableList.of("java/util/Helper"),
             ImmutableList.of(),
             ImmutableList.of("java/util/Existing#match -> java/util/Helper"));
@@ -108,6 +113,7 @@
         new CoreLibrarySupport(
             new CoreLibraryRewriter(""),
             Thread.currentThread().getContextClassLoader(),
+            null,
             ImmutableList.of("java/util/concurrent/"),
             ImmutableList.of("java/util/Map"),
             ImmutableList.of());
@@ -126,6 +132,7 @@
         new CoreLibrarySupport(
             new CoreLibraryRewriter(""),
             Thread.currentThread().getContextClassLoader(),
+            null,
             ImmutableList.of(),
             ImmutableList.of("java/util/Collection"),
             ImmutableList.of());
@@ -161,6 +168,7 @@
         new CoreLibrarySupport(
             new CoreLibraryRewriter(""),
             Thread.currentThread().getContextClassLoader(),
+            null,
             ImmutableList.of(),
             ImmutableList.of("java/util/Collection"),
             ImmutableList.of());
@@ -188,6 +196,7 @@
         new CoreLibrarySupport(
             new CoreLibraryRewriter(""),
             Thread.currentThread().getContextClassLoader(),
+            null,
             ImmutableList.of(),
             ImmutableList.of("java/util/Map"),
             ImmutableList.of());
@@ -223,6 +232,7 @@
         new CoreLibrarySupport(
             new CoreLibraryRewriter(""),
             Thread.currentThread().getContextClassLoader(),
+            null,
             ImmutableList.of(),
             ImmutableList.of("java/util/Comparator"),
             ImmutableList.of());
@@ -246,6 +256,7 @@
         new CoreLibrarySupport(
             new CoreLibraryRewriter(""),
             Thread.currentThread().getContextClassLoader(),
+            null,
             ImmutableList.of("java/util/"),
             ImmutableList.of(),
             ImmutableList.of());
@@ -314,6 +325,7 @@
         new CoreLibrarySupport(
             new CoreLibraryRewriter(""),
             Thread.currentThread().getContextClassLoader(),
+            null,
             ImmutableList.of("java/util/concurrent/"),  // should return null for these
             ImmutableList.of("java/util/Map"),
             ImmutableList.of());
diff --git a/test/java/com/google/devtools/build/android/desugar/CorePackageRenamerTest.java b/test/java/com/google/devtools/build/android/desugar/CorePackageRenamerTest.java
index 0ff0f25..d998aa2 100644
--- a/test/java/com/google/devtools/build/android/desugar/CorePackageRenamerTest.java
+++ b/test/java/com/google/devtools/build/android/desugar/CorePackageRenamerTest.java
@@ -35,6 +35,7 @@
             new CoreLibrarySupport(
                 new CoreLibraryRewriter(""),
                 null,
+                null,
                 ImmutableList.of("java/time/"),
                 ImmutableList.of(),
                 ImmutableList.of("java/util/A#m->java/time/B")));