Add backporting of sun.misc.unsafe.compareAndSwapObject

Bug: 211646483
Change-Id: Ifa66351fbe170ceb9faeedfe4d47160f42f5f240
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfFrame.java b/src/main/java/com/android/tools/r8/cf/code/CfFrame.java
index fbeda56..f8b10e0 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfFrame.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfFrame.java
@@ -105,6 +105,24 @@
       return false;
     }
 
+    FrameType map(java.util.function.Function<DexType, DexType> func) {
+      if (isInitialized()) {
+        DexType type = getInitializedType();
+        DexType newType = func.apply(type);
+        if (type != newType) {
+          return initialized(newType);
+        }
+      }
+      if (isUninitializedNew()) {
+        DexType type = getUninitializedNewType();
+        DexType newType = func.apply(type);
+        if (type != newType) {
+          return uninitializedNew(getUninitializedLabel(), newType);
+        }
+      }
+      return this;
+    }
+
     private FrameType() {}
 
     public static FrameType fromMemberType(MemberType memberType, DexItemFactory factory) {
@@ -512,4 +530,16 @@
     }
     return other;
   }
+
+  public CfFrame map(java.util.function.Function<DexType, DexType> func) {
+    Int2ReferenceSortedMap<FrameType> newLocals = new Int2ReferenceAVLTreeMap<>();
+    for (int var : locals.keySet()) {
+      newLocals.put(var, locals.get(var).map(func));
+    }
+    Deque<FrameType> newStack = new ArrayDeque<>();
+    for (FrameType frameType : stack) {
+      newStack.addLast(frameType.map(func));
+    }
+    return new CfFrame(newLocals, newStack);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/BackportedMethodRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/BackportedMethodRewriter.java
index 8cd6162..a3fc0e8 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/BackportedMethodRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/BackportedMethodRewriter.java
@@ -34,6 +34,7 @@
 import com.android.tools.r8.ir.desugar.backports.SparseArrayMethodRewrites;
 import com.android.tools.r8.ir.desugar.desugaredlibrary.retargeter.DesugaredLibraryRetargeter;
 import com.android.tools.r8.synthesis.SyntheticNaming;
+import com.android.tools.r8.synthesis.SyntheticNaming.SyntheticKind;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.InternalOptions;
@@ -90,7 +91,10 @@
   @Override
   public boolean needsDesugaring(CfInstruction instruction, ProgramMethod context) {
     return instruction.isInvoke()
-        && getMethodProviderOrNull(instruction.asInvoke().getMethod()) != null;
+        && getMethodProviderOrNull(instruction.asInvoke().getMethod()) != null
+        && !appView
+            .getSyntheticItems()
+            .isSyntheticOfKind(context.getContextType(), SyntheticKind.BACKPORT_WITH_FORWARDING);
   }
 
   public static List<DexMethod> generateListOfBackportedMethods(
@@ -121,7 +125,6 @@
     BackportedMethods.registerSynthesizedCodeReferences(options.itemFactory);
   }
 
-
   private MethodProvider getMethodProviderOrNull(DexMethod method) {
     DexMethod original = appView.graphLens().getOriginalMethodSignature(method);
     assert original != null;
@@ -182,6 +185,9 @@
       if (options.getMinApiLevel().isLessThan(AndroidApiLevel.S)) {
         initializeAndroidSMethodProviders(factory);
       }
+      if (options.getMinApiLevel().isLessThan(AndroidApiLevel.Sv2)) {
+        initializeAndroidSv2MethodProviders(factory);
+      }
 
       // The following providers are currently not implemented at any API level in Android.
       // They however require the Optional/Stream class to be present, either through desugared
@@ -1084,6 +1090,30 @@
               factory.androidUtilSparseArrayMembers.set, SparseArrayMethodRewrites.rewriteSet()));
     }
 
+    private void initializeAndroidSv2MethodProviders(DexItemFactory factory) {
+      DexString name;
+      DexProto proto;
+      DexMethod method;
+      // sun.misc.Unsafe
+
+      // compareAndSwapObject(Object receiver, long offset, Object expect, Object update)
+      name = factory.createString("compareAndSwapObject");
+      proto =
+          factory.createProto(
+              factory.booleanType,
+              factory.objectType,
+              factory.longType,
+              factory.objectType,
+              factory.objectType);
+      method = factory.createMethod(factory.unsafeType, proto, name);
+      addProvider(
+          new StatifyingMethodWithForwardingGenerator(
+              method,
+              BackportedMethods::UnsafeMethods_compareAndSwapObject,
+              "compareAndSwapObject",
+              factory.unsafeType));
+    }
+
     private void initializeJava9MethodProviders(DexItemFactory factory) {
       // Integer
       DexType type = factory.boxedIntType;
@@ -1446,6 +1476,10 @@
       this.methodName = methodName;
     }
 
+    protected SyntheticKind getSyntheticKind() {
+      return SyntheticNaming.SyntheticKind.BACKPORT;
+    }
+
     @Override
     public Collection<CfInstruction> rewriteInvoke(
         CfInvoke invoke,
@@ -1463,7 +1497,7 @@
       return appView
           .getSyntheticItems()
           .createMethod(
-              SyntheticNaming.SyntheticKind.BACKPORT,
+              getSyntheticKind(),
               methodProcessingContext.createUniqueContext(),
               appView,
               builder ->
@@ -1502,6 +1536,20 @@
     }
   }
 
+  // Version of StatifyingMethodGenerator for backports which will call the method they backport.
+  // Such backports will not go through backporting again as that would cause infinite recursion.
+  private static class StatifyingMethodWithForwardingGenerator extends StatifyingMethodGenerator {
+    StatifyingMethodWithForwardingGenerator(
+        DexMethod method, TemplateMethodFactory factory, String methodName, DexType receiverType) {
+      super(method, factory, methodName, receiverType);
+    }
+
+    @Override
+    protected SyntheticKind getSyntheticKind() {
+      return SyntheticKind.BACKPORT_WITH_FORWARDING;
+    }
+  }
+
   private interface TemplateMethodFactory {
 
     CfCode create(InternalOptions options, DexMethod method);
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/backports/BackportedMethods.java b/src/main/java/com/android/tools/r8/ir/desugar/backports/BackportedMethods.java
index b2fd06d..d7f7109 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/backports/BackportedMethods.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/backports/BackportedMethods.java
@@ -105,6 +105,7 @@
     factory.createSynthesizedType("Ljava/util/stream/IntStream;");
     factory.createSynthesizedType("Ljava/util/stream/LongStream;");
     factory.createSynthesizedType("Ljava/util/stream/Stream;");
+    factory.createSynthesizedType("Lsun/misc/Unsafe;");
     factory.createSynthesizedType("[Ljava/lang/CharSequence;");
     factory.createSynthesizedType("[Ljava/lang/Class;");
     factory.createSynthesizedType("[Ljava/lang/Object;");
@@ -9077,4 +9078,84 @@
         ImmutableList.of(),
         ImmutableList.of());
   }
+
+  public static CfCode UnsafeMethods_compareAndSwapObject(
+      InternalOptions options, DexMethod method) {
+    CfLabel label0 = new CfLabel();
+    CfLabel label1 = new CfLabel();
+    CfLabel label2 = new CfLabel();
+    CfLabel label3 = new CfLabel();
+    CfLabel label4 = new CfLabel();
+    return new CfCode(
+        method.holder,
+        6,
+        6,
+        ImmutableList.of(
+            label0,
+            new CfFrame(
+                new Int2ReferenceAVLTreeMap<>(
+                    new int[] {0, 1, 2, 4, 5},
+                    new FrameType[] {
+                      FrameType.initialized(options.itemFactory.createType("Lsun/misc/Unsafe;")),
+                      FrameType.initialized(options.itemFactory.objectType),
+                      FrameType.initialized(options.itemFactory.longType),
+                      FrameType.initialized(options.itemFactory.objectType),
+                      FrameType.initialized(options.itemFactory.objectType)
+                    }),
+                new ArrayDeque<>(Arrays.asList())),
+            new CfLoad(ValueType.OBJECT, 0),
+            new CfLoad(ValueType.OBJECT, 1),
+            new CfLoad(ValueType.LONG, 2),
+            new CfLoad(ValueType.OBJECT, 4),
+            new CfLoad(ValueType.OBJECT, 5),
+            new CfInvoke(
+                182,
+                options.itemFactory.createMethod(
+                    options.itemFactory.createType("Lsun/misc/Unsafe;"),
+                    options.itemFactory.createProto(
+                        options.itemFactory.booleanType,
+                        options.itemFactory.objectType,
+                        options.itemFactory.longType,
+                        options.itemFactory.objectType,
+                        options.itemFactory.objectType),
+                    options.itemFactory.createString("compareAndSwapObject")),
+                false),
+            new CfIf(If.Type.EQ, ValueType.INT, label2),
+            label1,
+            new CfConstNumber(1, ValueType.INT),
+            new CfReturn(ValueType.INT),
+            label2,
+            new CfFrame(
+                new Int2ReferenceAVLTreeMap<>(
+                    new int[] {0, 1, 2, 4, 5},
+                    new FrameType[] {
+                      FrameType.initialized(options.itemFactory.createType("Lsun/misc/Unsafe;")),
+                      FrameType.initialized(options.itemFactory.objectType),
+                      FrameType.initialized(options.itemFactory.longType),
+                      FrameType.initialized(options.itemFactory.objectType),
+                      FrameType.initialized(options.itemFactory.objectType)
+                    }),
+                new ArrayDeque<>(Arrays.asList())),
+            new CfLoad(ValueType.OBJECT, 0),
+            new CfLoad(ValueType.OBJECT, 1),
+            new CfLoad(ValueType.LONG, 2),
+            new CfInvoke(
+                182,
+                options.itemFactory.createMethod(
+                    options.itemFactory.createType("Lsun/misc/Unsafe;"),
+                    options.itemFactory.createProto(
+                        options.itemFactory.objectType,
+                        options.itemFactory.objectType,
+                        options.itemFactory.longType),
+                    options.itemFactory.createString("getObject")),
+                false),
+            new CfLoad(ValueType.OBJECT, 4),
+            new CfIfCmp(If.Type.EQ, ValueType.OBJECT, label0),
+            label3,
+            new CfConstNumber(0, ValueType.INT),
+            new CfReturn(ValueType.INT),
+            label4),
+        ImmutableList.of(),
+        ImmutableList.of());
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
index fc487f7..c80a94b 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
@@ -43,6 +43,7 @@
     ENUM_UNBOXING_CHECK_NOT_ZERO_METHOD("CheckNotZero", 27, true),
     RECORD_HELPER("Record", 9, true),
     BACKPORT("Backport", 10, true),
+    BACKPORT_WITH_FORWARDING("BackportWithForwarding", 34, true),
     STATIC_INTERFACE_CALL("StaticInterfaceCall", 11, true),
     TO_STRING_IF_NOT_NULL("ToStringIfNotNull", 12, true),
     THROW_CCE_IF_NOT_NULL("ThrowCCEIfNotNull", 13, true),
diff --git a/src/test/java/com/android/tools/r8/desugar/backports/AbstractBackportTest.java b/src/test/java/com/android/tools/r8/desugar/backports/AbstractBackportTest.java
index 7f77265..0c6d576 100644
--- a/src/test/java/com/android/tools/r8/desugar/backports/AbstractBackportTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/backports/AbstractBackportTest.java
@@ -42,26 +42,34 @@
   private final Set<String> ignoredInvokes = new HashSet<>();
 
   private static class ClassInfo {
+    private final String name;
     private final Class<?> clazz;
     private final List<byte[]> classFileData;
 
+    private ClassInfo(String name) {
+      this.name = name;
+      this.clazz = null;
+      this.classFileData = null;
+    }
+
     private ClassInfo(Class<?> clazz) {
+      this.name = clazz.getName();
       this.clazz = clazz;
       this.classFileData = null;
     }
 
     private ClassInfo(byte[] classFileData) {
-      this.clazz = null;
-      this.classFileData = ImmutableList.of(classFileData);
+      this(ImmutableList.of(classFileData));
     }
 
     private ClassInfo(List<byte[]> classFileData) {
+      this.name = extractClassName(classFileData.get(0));
       this.clazz = null;
       this.classFileData = classFileData;
     }
 
     String getName() {
-      return clazz != null ? clazz.getName() : extractClassName(classFileData.get(0));
+      return name;
     }
 
     TestBuilder<?, ?> addAsProgramClass(TestBuilder<?, ?> builder) throws IOException {
@@ -79,6 +87,11 @@
   }
 
   AbstractBackportTest(
+      TestParameters parameters, String className, List<byte[]> testClassFileData) {
+    this(parameters, new ClassInfo(className), new ClassInfo(testClassFileData), null, null);
+  }
+
+  AbstractBackportTest(
       TestParameters parameters, byte[] targetClassFileData, List<byte[]> testClassFileData) {
     this(
         parameters,
diff --git a/src/test/java/com/android/tools/r8/desugar/backports/UnsafeBackportTest.java b/src/test/java/com/android/tools/r8/desugar/backports/UnsafeBackportTest.java
new file mode 100644
index 0000000..dbe597d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/backports/UnsafeBackportTest.java
@@ -0,0 +1,90 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.desugar.backports;
+
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.transformers.ClassFileTransformer.MethodPredicate;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.google.common.collect.ImmutableList;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class UnsafeBackportTest extends AbstractBackportTest {
+
+  private static final String UNSAFE_TYPE_NAME = "sun.misc.Unsafe";
+  private static final String UNSAFE_DESCRIPTOR =
+      DescriptorUtils.javaTypeToDescriptor(UNSAFE_TYPE_NAME);
+
+  @Parameters(name = "{0}")
+  public static Iterable<?> data() {
+    return getTestParameters()
+        .withDexRuntimesStartingFromExcluding(Version.V4_0_4)
+        .withAllApiLevels()
+        .build();
+  }
+
+  public UnsafeBackportTest(TestParameters parameters) throws IOException {
+    super(
+        parameters,
+        "sun.misc.Unsafe",
+        ImmutableList.of(UnsafeBackportTest.getTestRunner(), UnsafeBackportTest.getA()));
+
+    ignoreInvokes("objectFieldOffset");
+
+    // sun.misc.Unsafe issue is on API 31.
+    registerTarget(AndroidApiLevel.Sv2, 3);
+  }
+
+  public static class UnsafeStub {
+
+    boolean compareAndSwapObject(Object receiver, long offset, Object expect, Object update) {
+      throw new RuntimeException("Stub called.");
+    }
+
+    public long objectFieldOffset(Field field) {
+      throw new RuntimeException("Stub called.");
+    }
+  }
+
+  private static byte[] getTestRunner() throws IOException {
+    return transformer(TestRunner.class)
+        .setReturnType(MethodPredicate.onName("getUnsafe"), UNSAFE_TYPE_NAME)
+        .replaceClassDescriptorInMethodInstructions(descriptor(UnsafeStub.class), UNSAFE_DESCRIPTOR)
+        .transform();
+  }
+
+  private static byte[] getA() throws IOException {
+    return transformer(A.class).transform();
+  }
+
+  public static class A {
+    public String field;
+  }
+
+  public static class TestRunner extends MiniAssert {
+
+    private static UnsafeStub getUnsafe() throws Exception {
+      Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
+      Field f = unsafeClass.getDeclaredField("theUnsafe");
+      f.setAccessible(true);
+      return (UnsafeStub) f.get(null);
+    }
+
+    public static void main(String[] args) throws Exception {
+      UnsafeStub theUnsafe = getUnsafe();
+      A x = new A();
+      long offset = theUnsafe.objectFieldOffset(A.class.getField("field"));
+      assertTrue(theUnsafe.compareAndSwapObject(x, offset, null, "A"));
+      assertTrue(theUnsafe.compareAndSwapObject(x, offset, "A", "B"));
+      assertFalse(theUnsafe.compareAndSwapObject(x, offset, "A", "B"));
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/desugar/backports/GenerateBackportMethods.java b/src/test/java/com/android/tools/r8/ir/desugar/backports/GenerateBackportMethods.java
index 5e79870..ce75bd9 100644
--- a/src/test/java/com/android/tools/r8/ir/desugar/backports/GenerateBackportMethods.java
+++ b/src/test/java/com/android/tools/r8/ir/desugar/backports/GenerateBackportMethods.java
@@ -50,7 +50,8 @@
           OptionalMethods.class,
           ShortMethods.class,
           StreamMethods.class,
-          StringMethods.class);
+          StringMethods.class,
+          UnsafeMethods.class);
 
   protected final TestParameters parameters;
 
@@ -101,6 +102,34 @@
     }
   }
 
+  private static CfInstruction rewriteToUnsafe(
+      DexItemFactory itemFactory, CfInstruction instruction) {
+    // Rewrite references to UnsafeStub to sun.misc.Unsafe.
+    if (instruction.isInvoke()) {
+      String name = instruction.asInvoke().getMethod().getName().toString();
+      if (name.equals("compareAndSwapObject") || name.equals("getObject")) {
+        CfInvoke invoke = instruction.asInvoke();
+        return new CfInvoke(
+            invoke.getOpcode(),
+            itemFactory.createMethod(
+                itemFactory.createType("Lsun/misc/Unsafe;"),
+                invoke.getMethod().getProto(),
+                itemFactory.createString(name)),
+            invoke.isInterface());
+      }
+    }
+    if (instruction.isFrame()) {
+      return instruction
+          .asFrame()
+          .map(
+              type ->
+                  (type.getTypeName().endsWith("$UnsafeStub"))
+                      ? itemFactory.createType("Lsun/misc/Unsafe;")
+                      : type);
+    }
+    return instruction;
+  }
+
   @Override
   protected CfCode getCode(String holderName, String methodName, CfCode code) {
     if (methodName.endsWith("Stub")) {
@@ -113,6 +142,12 @@
               .map(instruction -> rewriteToJava9API(factory, instruction))
               .collect(Collectors.toList()));
     }
+    if (holderName.equals("UnsafeMethods") && methodName.equals("compareAndSwapObject")) {
+      code.setInstructions(
+          code.getInstructions().stream()
+              .map(instruction -> rewriteToUnsafe(factory, instruction))
+              .collect(Collectors.toList()));
+    }
     return code;
   }
 
diff --git a/src/test/java/com/android/tools/r8/ir/desugar/backports/UnsafeMethods.java b/src/test/java/com/android/tools/r8/ir/desugar/backports/UnsafeMethods.java
new file mode 100644
index 0000000..95dab48
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/desugar/backports/UnsafeMethods.java
@@ -0,0 +1,31 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.desugar.backports;
+
+public final class UnsafeMethods {
+  // Stub out sun.misc.Unsafe to avoid compiler issues with referring to sun.misc.Unsafe.
+  private static class UnsafeStub {
+
+    public boolean compareAndSwapObject(
+        Object receiver, long offset, Object expect, Object update) {
+      throw new RuntimeException("Stub called.");
+    }
+
+    public Object getObject(Object receiver, long offset) {
+      throw new RuntimeException("Stub called.");
+    }
+  }
+
+  // Workaround Android S issue with compareAndSwapObject (b/211646483).
+  public static boolean compareAndSwapObject(
+      UnsafeStub unsafe, Object receiver, long offset, Object expect, Object update) {
+    do {
+      if (unsafe.compareAndSwapObject(receiver, offset, expect, update)) {
+        return true;
+      }
+    } while (unsafe.getObject(receiver, offset) == expect);
+    return false;
+  }
+}