Backport ThreadLocal.withInitial when Supplier is present
This will backport ThreadLocal.withInitial(java.util.function.Supplier)
when java.util.function.Supplier is present at runtime.
Without desugared library this makes the method usable from API level
24 (when Supplier was introduced) instead of from API level 26 (when
withInitial was introduced).
With desugared library the method is always usable as Supplier is
included in all desugared library versions.
Bug: b/160484830
Change-Id: I536879372d903d15b182a2ef071adf01361c8f01
diff --git a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
index 9620ee4..3ee7c50 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -291,6 +291,7 @@
public final DexString optionalLongDescriptor = createString("Ljava/util/OptionalLong;");
public final DexString streamDescriptor = createString("Ljava/util/stream/Stream;");
public final DexString arraysDescriptor = createString("Ljava/util/Arrays;");
+ public final DexString threadLocalDescriptor = createString("Ljava/lang/ThreadLocal;");
public final DexString throwableDescriptor = createString(throwableDescriptorString);
public final DexString illegalAccessErrorDescriptor =
@@ -446,6 +447,7 @@
public final DexType optionalIntType = createStaticallyKnownType(optionalIntDescriptor);
public final DexType optionalLongType = createStaticallyKnownType(optionalLongDescriptor);
public final DexType streamType = createStaticallyKnownType(streamDescriptor);
+ public final DexType threadLocalType = createStaticallyKnownType(threadLocalDescriptor);
public final DexType bufferType = createStaticallyKnownType(bufferDescriptor);
public final DexType byteBufferType = createStaticallyKnownType(byteBufferDescriptor);
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 e6faadc..a93e87f 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
@@ -5,10 +5,19 @@
package com.android.tools.r8.ir.desugar;
import com.android.tools.r8.androidapi.ComputedApiLevel;
+import com.android.tools.r8.cf.code.CfInstanceFieldRead;
+import com.android.tools.r8.cf.code.CfInstanceFieldWrite;
import com.android.tools.r8.cf.code.CfInstruction;
import com.android.tools.r8.cf.code.CfInvoke;
+import com.android.tools.r8.cf.code.CfLoad;
+import com.android.tools.r8.cf.code.CfNew;
+import com.android.tools.r8.cf.code.CfReturn;
+import com.android.tools.r8.cf.code.CfReturnVoid;
+import com.android.tools.r8.cf.code.CfStackInstruction;
+import com.android.tools.r8.cf.code.CfStackInstruction.Opcode;
import com.android.tools.r8.contexts.CompilationContext.MethodProcessingContext;
import com.android.tools.r8.dex.ApplicationReader;
+import com.android.tools.r8.dex.Constants;
import com.android.tools.r8.errors.Unreachable;
import com.android.tools.r8.graph.AppInfo;
import com.android.tools.r8.graph.AppView;
@@ -17,13 +26,19 @@
import com.android.tools.r8.graph.DexApplication;
import com.android.tools.r8.graph.DexClass;
import com.android.tools.r8.graph.DexClassAndMethod;
+import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexField;
import com.android.tools.r8.graph.DexItemFactory;
import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
import com.android.tools.r8.graph.DexProto;
import com.android.tools.r8.graph.DexString;
import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.FieldAccessFlags;
import com.android.tools.r8.graph.MethodAccessFlags;
import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.code.ValueType;
import com.android.tools.r8.ir.desugar.backports.BackportedMethodDesugaringEventConsumer;
import com.android.tools.r8.ir.desugar.backports.BackportedMethods;
import com.android.tools.r8.ir.desugar.backports.BooleanMethodRewrites;
@@ -39,6 +54,7 @@
import com.android.tools.r8.synthesis.SyntheticItems.GlobalSyntheticsStrategy;
import com.android.tools.r8.synthesis.SyntheticNaming;
import com.android.tools.r8.synthesis.SyntheticNaming.SyntheticKind;
+import com.android.tools.r8.synthesis.SyntheticProgramClassBuilder;
import com.android.tools.r8.utils.AndroidApiLevel;
import com.android.tools.r8.utils.AndroidApp;
import com.android.tools.r8.utils.InternalOptions;
@@ -186,6 +202,9 @@
}
if (options.getMinApiLevel().isLessThan(AndroidApiLevel.O)) {
initializeAndroidOMethodProviders(factory);
+ if (typeIsPresent(factory.supplierType)) {
+ initializeAndroidOThreadLocalMethodProviderWithSupplier(factory);
+ }
}
if (options.getMinApiLevel().isLessThan(AndroidApiLevel.R)) {
if (typeIsPresentWithoutBackportsFrom(factory.setType, AndroidApiLevel.R)) {
@@ -1617,6 +1636,17 @@
new MethodGenerator(method, BackportedMethods::ObjectsMethods_requireNonNullSupplier));
}
+ private void initializeAndroidOThreadLocalMethodProviderWithSupplier(DexItemFactory factory) {
+ // ThreadLocal
+ DexType type = factory.threadLocalType;
+
+ // ThreadLocal.withInitial(Supplier)
+ DexString name = factory.createString("withInitial");
+ DexProto proto = factory.createProto(type, factory.supplierType);
+ DexMethod method = factory.createMethod(type, proto, name);
+ addProvider(new ThreadLocalWithInitialWithSupplierGenerator(method));
+ }
+
private void addProvider(MethodProvider generator) {
MethodProvider replaced = rewritable.put(generator.method, generator);
assert replaced == null;
@@ -1754,6 +1784,174 @@
}
}
+ /*
+ Generate code for the SynthesizedThreadLocalSubClass class below, and rewrite
+
+ ThreadLocal.withInitial(someSupplier);
+
+ to
+
+ new SynthesizedThreadLocalSubClass(someSupplier);
+
+ class SynthesizedThreadLocalSubClass extends ThreadLocal<Object> {
+ private Supplier<Object> supplier;
+
+ SynthesizedThreadLocalSubClass(Supplier<Object> supplier) {
+ this.supplier = supplier;
+ }
+
+ protected Object initialValue() {
+ return supplier.get();
+ }
+ }
+ */
+ private static class ThreadLocalWithInitialWithSupplierGenerator extends MethodGenerator {
+
+ ThreadLocalWithInitialWithSupplierGenerator(DexMethod method) {
+ super(method, null, method.name.toString());
+ }
+
+ @Override
+ protected SyntheticKind getSyntheticKind(SyntheticNaming naming) {
+ return naming.THREAD_LOCAL;
+ }
+
+ @Override
+ public Collection<CfInstruction> rewriteInvoke(
+ CfInvoke invoke,
+ AppView<?> appView,
+ BackportedMethodDesugaringEventConsumer eventConsumer,
+ MethodProcessingContext methodProcessingContext,
+ LocalStackAllocator localStackAllocator) {
+ DexItemFactory factory = appView.dexItemFactory();
+ DexProgramClass threadLocalSubclass =
+ appView
+ .getSyntheticItems()
+ .createClass(
+ kinds -> kinds.THREAD_LOCAL,
+ methodProcessingContext.createUniqueContext(),
+ appView,
+ builder -> new ThreadLocalSubclassGenerator(builder, appView));
+ eventConsumer.acceptBackportedClass(
+ threadLocalSubclass, methodProcessingContext.getMethodContext());
+ return ImmutableList.of(
+ new CfNew(threadLocalSubclass.type),
+ // Massage the stack with dup_x1 and swap:
+ // ..., SomeSupplier, ThreadLocalSubClass ->
+ // ..., ThreadLocalSubClass, ThreadLocalSubClass, SomeSupplier
+ new CfStackInstruction(Opcode.DupX1),
+ new CfStackInstruction(Opcode.Swap),
+ new CfInvoke(
+ Opcodes.INVOKESPECIAL,
+ factory.createMethod(
+ threadLocalSubclass.type,
+ factory.createProto(factory.voidType, factory.supplierType),
+ factory.constructorMethodName),
+ false));
+ }
+ }
+
+ private static class ThreadLocalSubclassGenerator {
+ private final DexItemFactory factory;
+ private final DexType type;
+ private final DexField supplierField;
+ private final DexMethod constructor;
+ private final DexMethod initialValueMethod;
+
+ ThreadLocalSubclassGenerator(SyntheticProgramClassBuilder builder, AppView<?> appView) {
+ this.factory = appView.dexItemFactory();
+ this.type = builder.getType();
+ this.supplierField =
+ factory.createField(
+ builder.getType(),
+ factory.supplierType,
+ factory.createString("initialValueSupplier"));
+ this.constructor =
+ factory.createMethod(
+ type,
+ factory.createProto(factory.voidType, factory.supplierType),
+ factory.constructorMethodName);
+ this.initialValueMethod =
+ factory.createMethod(
+ type, factory.createProto(factory.objectType), factory.createString("initialValue"));
+
+ builder.setSuperType(factory.threadLocalType);
+ synthesizeInstanceFields(builder);
+ synthesizeDirectMethods(builder);
+ synthesizeVirtualMethods(builder);
+ }
+
+ private void synthesizeInstanceFields(SyntheticProgramClassBuilder builder) {
+ // Instance field:
+ //
+ // private Supplier<Object> supplier
+ builder.setInstanceFields(
+ ImmutableList.of(
+ DexEncodedField.syntheticBuilder()
+ .setField(supplierField)
+ .setAccessFlags(FieldAccessFlags.createPublicFinalSynthetic())
+ .disableAndroidApiLevelCheck()
+ .build()));
+ }
+
+ private void synthesizeDirectMethods(SyntheticProgramClassBuilder builder) {
+ // Code for:
+ //
+ // SynthesizedThreadLocalSubClass(Supplier<Object> supplier) {
+ // this.supplier = supplier;
+ // }
+ List<CfInstruction> instructions = new ArrayList<>();
+ instructions.add(new CfLoad(ValueType.OBJECT, 0));
+ instructions.add(new CfStackInstruction(Opcode.Dup));
+ instructions.add(
+ new CfInvoke(
+ Opcodes.INVOKESPECIAL,
+ factory.createMethod(
+ factory.threadLocalType,
+ factory.createProto(factory.voidType),
+ factory.constructorMethodName),
+ false));
+ instructions.add(new CfLoad(ValueType.fromDexType(factory.supplierType), 1));
+ instructions.add(new CfInstanceFieldWrite(supplierField));
+ instructions.add(new CfReturnVoid());
+
+ builder.setDirectMethods(
+ ImmutableList.of(
+ DexEncodedMethod.syntheticBuilder()
+ .setMethod(constructor)
+ .setAccessFlags(
+ MethodAccessFlags.fromSharedAccessFlags(
+ Constants.ACC_PUBLIC | Constants.ACC_SYNTHETIC, true))
+ .setCode(new CfCode(type, 2, 2, instructions))
+ .disableAndroidApiLevelCheck()
+ .build()));
+ }
+
+ private void synthesizeVirtualMethods(SyntheticProgramClassBuilder builder) {
+ // Code for:
+ //
+ // protected Object initialValue() {
+ // return supplier.get();
+ // }
+ List<CfInstruction> instructions = new ArrayList<>();
+ instructions.add(new CfLoad(ValueType.OBJECT, 0));
+ instructions.add(new CfInstanceFieldRead(supplierField));
+ instructions.add(new CfInvoke(Opcodes.INVOKEINTERFACE, factory.supplierMembers.get, true));
+ instructions.add(new CfReturn(ValueType.OBJECT));
+
+ builder.setVirtualMethods(
+ ImmutableList.of(
+ DexEncodedMethod.syntheticBuilder()
+ .setMethod(initialValueMethod)
+ .setAccessFlags(
+ MethodAccessFlags.fromSharedAccessFlags(
+ Constants.ACC_PROTECTED | Constants.ACC_SYNTHETIC, false))
+ .setCode(new CfCode(type, 1, 1, instructions))
+ .disableAndroidApiLevelCheck()
+ .build()));
+ }
+ }
+
private interface TemplateMethodFactory {
CfCode create(DexItemFactory factory, DexMethod method);
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/CfInstructionDesugaringEventConsumer.java b/src/main/java/com/android/tools/r8/ir/desugar/CfInstructionDesugaringEventConsumer.java
index 6a9fb76..e44467e 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/CfInstructionDesugaringEventConsumer.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/CfInstructionDesugaringEventConsumer.java
@@ -127,6 +127,13 @@
}
@Override
+ public void acceptBackportedClass(DexProgramClass backportedClass, ProgramMethod context) {
+ backportedClass
+ .programMethods()
+ .forEach(method -> methodProcessor.scheduleMethodForProcessing(method, this));
+ }
+
+ @Override
public void acceptRecordMethod(ProgramMethod method) {
methodProcessor.scheduleDesugaredMethodForProcessing(method);
}
@@ -398,6 +405,11 @@
}
@Override
+ public void acceptBackportedClass(DexProgramClass backportedClass, ProgramMethod context) {
+ // Intentionally empty. The method will be hit by tracing if required.
+ }
+
+ @Override
public void acceptInvokeSpecialBridgeInfo(InvokeSpecialBridgeInfo info) {
synchronized (pendingInvokeSpecialBridges) {
pendingInvokeSpecialBridges.add(info);
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/backports/BackportedMethodDesugaringEventConsumer.java b/src/main/java/com/android/tools/r8/ir/desugar/backports/BackportedMethodDesugaringEventConsumer.java
index c62501f..11f9d56 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/backports/BackportedMethodDesugaringEventConsumer.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/backports/BackportedMethodDesugaringEventConsumer.java
@@ -4,9 +4,12 @@
package com.android.tools.r8.ir.desugar.backports;
+import com.android.tools.r8.graph.DexProgramClass;
import com.android.tools.r8.graph.ProgramMethod;
public interface BackportedMethodDesugaringEventConsumer {
void acceptBackportedMethod(ProgramMethod backportedMethod, ProgramMethod context);
+
+ void acceptBackportedClass(DexProgramClass backportedClass, ProgramMethod context);
}
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 e1ac8ba..59c8d20 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
@@ -57,6 +57,7 @@
// Locally generated synthetic classes.
public final SyntheticKind LAMBDA = generator.forInstanceClass("Lambda");
+ public final SyntheticKind THREAD_LOCAL = generator.forInstanceClass("ThreadLocal");
// TODO(b/214901256): Sharing of synthetic classes may lead to duplicate method errors.
public final SyntheticKind NON_FIXED_INIT_TYPE_ARGUMENT =
diff --git a/src/test/java/com/android/tools/r8/desugar/backports/ThreadLocalBackportTest.java b/src/test/java/com/android/tools/r8/desugar/backports/ThreadLocalBackportTest.java
new file mode 100644
index 0000000..929a3fb
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/backports/ThreadLocalBackportTest.java
@@ -0,0 +1,88 @@
+// 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 static com.android.tools.r8.utils.AndroidApiLevel.LATEST;
+
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRunResult;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.desugar.desugaredlibrary.DesugaredLibraryTestBase;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class ThreadLocalBackportTest extends DesugaredLibraryTestBase {
+
+ @Parameter(0)
+ public TestParameters parameters;
+
+ @Parameters(name = "{0}")
+ public static List<Object[]> data() {
+ return buildParameters(
+ getTestParameters().withAllRuntimes().withAllApiLevelsAlsoForCf().build());
+ }
+
+ private static final String EXPECTED_OUTPUT = StringUtils.lines("Hello, world!");
+
+ @Test
+ public void testD8() throws Exception {
+ testForD8(parameters.getBackend())
+ .addInnerClasses(getClass())
+ .setMinApi(parameters.getApiLevel())
+ .run(parameters.getRuntime(), TestClass.class)
+ .apply(this::checkExpected);
+ }
+
+ @Test
+ public void testR8() throws Exception {
+ testForR8(parameters.getBackend())
+ .addLibraryFiles(
+ parameters.isCfRuntime()
+ ? ToolHelper.getJava8RuntimeJar()
+ : ToolHelper.getAndroidJar(LATEST))
+ .addInnerClasses(getClass())
+ .addKeepMainRule(TestClass.class)
+ .setMinApi(parameters.getApiLevel())
+ .run(parameters.getRuntime(), TestClass.class)
+ .apply(this::checkExpected);
+ }
+
+ private void checkExpected(TestRunResult<?> result) {
+ result.applyIf(
+ parameters.getRuntime().isCf()
+ || parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.N)
+ || parameters
+ .getRuntime()
+ .asDex()
+ .getMinApiLevel()
+ .isGreaterThanOrEqualTo(AndroidApiLevel.O),
+ r -> r.assertSuccessWithOutput(EXPECTED_OUTPUT),
+ parameters.getRuntime().isDex()
+ && parameters
+ .getRuntime()
+ .asDex()
+ .getMinApiLevel()
+ .isGreaterThanOrEqualTo(AndroidApiLevel.N)
+ && parameters.getRuntime().asDex().getMinApiLevel().isLessThan(AndroidApiLevel.O)
+ && parameters.getApiLevel().isLessThan(AndroidApiLevel.N),
+ r -> r.assertFailureWithErrorThatThrows(NoSuchMethodError.class),
+ r -> r.assertFailureWithErrorThatThrows(NoClassDefFoundError.class));
+ }
+
+ static class TestClass {
+
+ public static void main(String[] args) {
+ ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "Hello, world!");
+ System.out.println(threadLocal.get());
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/backports/ThreadLocalBackportWithDesugaredLibraryTest.java b/src/test/java/com/android/tools/r8/desugar/backports/ThreadLocalBackportWithDesugaredLibraryTest.java
new file mode 100644
index 0000000..67ee012
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/backports/ThreadLocalBackportWithDesugaredLibraryTest.java
@@ -0,0 +1,65 @@
+// 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 static com.android.tools.r8.desugar.desugaredlibrary.test.CompilationSpecification.SPECIFICATIONS_WITH_CF2CF;
+import static com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification.JDK11;
+import static com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification.JDK11_LEGACY;
+import static com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification.JDK11_MINIMAL;
+import static com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification.JDK11_PATH;
+import static com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification.JDK8;
+
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.desugar.desugaredlibrary.DesugaredLibraryTestBase;
+import com.android.tools.r8.desugar.desugaredlibrary.test.CompilationSpecification;
+import com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification;
+import com.android.tools.r8.utils.StringUtils;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class ThreadLocalBackportWithDesugaredLibraryTest extends DesugaredLibraryTestBase {
+
+ @Parameter(0)
+ public TestParameters parameters;
+
+ @Parameter(1)
+ public LibraryDesugaringSpecification libraryDesugaringSpecification;
+
+ @Parameter(2)
+ public CompilationSpecification compilationSpecification;
+
+ @Parameters(name = "{0}, spec: {1}, {2}")
+ public static List<Object[]> data() {
+ return buildParameters(
+ getTestParameters().withAllRuntimes().withAllApiLevelsAlsoForCf().build(),
+ ImmutableList.of(JDK8, JDK11_LEGACY, JDK11_MINIMAL, JDK11, JDK11_PATH),
+ SPECIFICATIONS_WITH_CF2CF);
+ }
+
+ private static final String EXPECTED_OUTPUT = StringUtils.lines("Hello, world!");
+
+ @Test
+ public void testWithDesugaredLibrary() throws Exception {
+ testForDesugaredLibrary(parameters, libraryDesugaringSpecification, compilationSpecification)
+ .addInnerClasses(getClass())
+ .addKeepMainRule(TestClass.class)
+ .run(parameters.getRuntime(), TestClass.class)
+ .assertSuccessWithOutput(EXPECTED_OUTPUT);
+ }
+
+ static class TestClass {
+
+ public static void main(String[] args) {
+ ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "Hello, world!");
+ System.out.println(threadLocal.get());
+ }
+ }
+}