Check for other users of ClassLoader than ServiceLoader.load
Bug: 187090274
Change-Id: If3efeb9e10acefdb6a65d297fbb4d341a9531388
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/ServiceLoaderRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/ServiceLoaderRewriter.java
index b1fcc81..5454e6a 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/ServiceLoaderRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/ServiceLoaderRewriter.java
@@ -32,6 +32,7 @@
import com.android.tools.r8.ir.desugar.ServiceLoaderSourceCode;
import com.android.tools.r8.origin.SynthesizedOrigin;
import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.BooleanBox;
import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.Collections;
@@ -305,8 +306,20 @@
public void perform(InvokeVirtual classLoaderInvoke, DexMethod method) {
// Remove the ClassLoader call since this can throw and will not be removed otherwise.
if (classLoaderInvoke != null) {
- clearGetClassLoader(classLoaderInvoke);
- iterator.nextUntil(i -> i == serviceLoaderLoad);
+ BooleanBox allClassLoaderUsersAreServiceLoaders =
+ new BooleanBox(!classLoaderInvoke.outValue().hasPhiUsers());
+ classLoaderInvoke
+ .outValue()
+ .uniqueUsers()
+ .forEach(
+ user -> {
+ assert !user.isAssume();
+ allClassLoaderUsersAreServiceLoaders.and(user == serviceLoaderLoad);
+ });
+ if (allClassLoaderUsersAreServiceLoaders.get()) {
+ clearGetClassLoader(classLoaderInvoke);
+ iterator.nextUntil(i -> i == serviceLoaderLoad);
+ }
}
// Remove the ServiceLoader.load call.
diff --git a/src/main/java/com/android/tools/r8/utils/BooleanBox.java b/src/main/java/com/android/tools/r8/utils/BooleanBox.java
index f46ac1c..4c350ee 100644
--- a/src/main/java/com/android/tools/r8/utils/BooleanBox.java
+++ b/src/main/java/com/android/tools/r8/utils/BooleanBox.java
@@ -29,4 +29,8 @@
public void unset() {
set(false);
}
+
+ public void and(boolean value) {
+ set(value && this.value);
+ }
}
diff --git a/src/test/java/com/android/tools/r8/rewrite/ServiceLoaderClassLoaderRewritingTest.java b/src/test/java/com/android/tools/r8/rewrite/ServiceLoaderClassLoaderRewritingTest.java
index e44dbb4..fbaef02 100644
--- a/src/test/java/com/android/tools/r8/rewrite/ServiceLoaderClassLoaderRewritingTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/ServiceLoaderClassLoaderRewritingTest.java
@@ -6,7 +6,6 @@
import static com.android.tools.r8.utils.codeinspector.CodeMatchers.invokesMethodWithName;
import static junit.framework.TestCase.assertNull;
-import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.not;
import static org.junit.Assert.assertTrue;
@@ -19,7 +18,6 @@
import com.android.tools.r8.utils.StringUtils;
import com.android.tools.r8.utils.codeinspector.ClassSubject;
import com.android.tools.r8.utils.codeinspector.CodeInspector;
-import com.android.tools.r8.utils.codeinspector.InstructionSubject;
import java.nio.file.Path;
import java.util.ServiceLoader;
import java.util.zip.ZipFile;
@@ -96,7 +94,7 @@
.writeToZip(path)
.inspect(this::verifyNoServiceLoaderLoads)
.run(parameters.getRuntime(), MainRunner.class)
- .assertFailureWithErrorThatMatches(containsString("ClassLoader should not be null"));
+ .assertSuccessWithOutput(EXPECTED_OUTPUT);
// Check that we have removed the service configuration from META-INF/services.
ZipFile zip = new ZipFile(path.toFile());
@@ -109,9 +107,4 @@
classSubject.forAllMethods(
method -> MatcherAssert.assertThat(method, not(invokesMethodWithName("load"))));
}
-
- private static boolean isServiceLoaderLoad(InstructionSubject instruction) {
- return instruction.isInvokeStatic()
- && instruction.getMethod().qualifiedName().contains("ServiceLoader.load");
- }
}
diff --git a/src/test/java/com/android/tools/r8/rewrite/ServiceLoaderMultipleCallsSameMethodTest.java b/src/test/java/com/android/tools/r8/rewrite/ServiceLoaderMultipleCallsSameMethodTest.java
new file mode 100644
index 0000000..49589c6
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/rewrite/ServiceLoaderMultipleCallsSameMethodTest.java
@@ -0,0 +1,144 @@
+// Copyright (c) 2020, 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.rewrite;
+
+import static com.android.tools.r8.utils.codeinspector.CodeMatchers.invokesMethodWithName;
+import static junit.framework.TestCase.assertEquals;
+import static junit.framework.TestCase.assertNull;
+import static junit.framework.TestCase.assertTrue;
+import static org.hamcrest.CoreMatchers.not;
+import static org.junit.Assert.assertFalse;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.DataEntryResource;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.FoundClassSubject;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.ServiceLoader;
+import java.util.concurrent.ExecutionException;
+import java.util.zip.ZipFile;
+import org.hamcrest.MatcherAssert;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class ServiceLoaderMultipleCallsSameMethodTest extends TestBase {
+
+ private final TestParameters parameters;
+ private final String EXPECTED_OUTPUT = StringUtils.lines("Hello World!", "Hello World!");
+
+ public interface Service {
+
+ void print();
+ }
+
+ public static class ServiceImpl implements Service {
+
+ @Override
+ public void print() {
+ System.out.println("Hello World!");
+ }
+ }
+
+ public static class ServiceImpl2 implements Service {
+
+ @Override
+ public void print() {
+ System.out.println("Hello World 2!");
+ }
+ }
+
+ public static class MainRunner {
+
+ public static void main(String[] args) {
+ run1();
+ }
+
+ @NeverInline
+ public static void run1() {
+ ClassLoader classLoader = Service.class.getClassLoader();
+ for (Service x : ServiceLoader.load(Service.class, classLoader)) {
+ x.print();
+ }
+ for (Service x : ServiceLoader.load(Service.class, classLoader)) {
+ x.print();
+ }
+ }
+ }
+
+ @Parameterized.Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withAllRuntimesAndApiLevels().build();
+ }
+
+ public ServiceLoaderMultipleCallsSameMethodTest(TestParameters parameters) {
+ this.parameters = parameters;
+ }
+
+ @Test
+ public void testRewritings() throws IOException, CompilationFailedException, ExecutionException {
+ Path path = temp.newFile("out.zip").toPath();
+ testForR8(parameters.getBackend())
+ .addInnerClasses(ServiceLoaderMultipleCallsSameMethodTest.class)
+ .addKeepMainRule(MainRunner.class)
+ .setMinApi(parameters.getApiLevel())
+ .enableInliningAnnotations()
+ .addDataEntryResources(
+ DataEntryResource.fromBytes(
+ StringUtils.lines(ServiceImpl.class.getTypeName()).getBytes(),
+ "META-INF/services/" + Service.class.getTypeName(),
+ Origin.unknown()))
+ .compile()
+ .writeToZip(path)
+ .run(parameters.getRuntime(), MainRunner.class)
+ .assertSuccessWithOutput(EXPECTED_OUTPUT)
+ // Check that we have actually rewritten the calls to ServiceLoader.load.
+ .inspect(this::verifyNoServiceLoaderLoads)
+ .inspect(this::verifyNoClassLoaders)
+ .inspect(
+ inspector -> {
+ // Check the synthesize service loader method is a single shared method.
+ // Due to minification we just check there is only a single synthetic class with a
+ // single static method.
+ boolean found = false;
+ for (FoundClassSubject clazz : inspector.allClasses()) {
+ if (clazz.isSynthetic()) {
+ assertFalse(found);
+ found = true;
+ assertEquals(1, clazz.allMethods().size());
+ clazz.forAllMethods(m -> assertTrue(m.isStatic()));
+ }
+ }
+ });
+
+ // Check that we have removed the service configuration from META-INF/services.
+ ZipFile zip = new ZipFile(path.toFile());
+ assertNull(zip.getEntry("META-INF/services/" + Service.class.getTypeName()));
+ }
+
+ private void verifyNoServiceLoaderLoads(CodeInspector inspector) {
+ ClassSubject classSubject = inspector.clazz(MainRunner.class);
+ Assert.assertTrue(classSubject.isPresent());
+ classSubject.forAllMethods(
+ method -> MatcherAssert.assertThat(method, not(invokesMethodWithName("load"))));
+ }
+
+ private void verifyNoClassLoaders(CodeInspector inspector) {
+ ClassSubject classSubject = inspector.clazz(MainRunner.class);
+ Assert.assertTrue(classSubject.isPresent());
+ classSubject.forAllMethods(
+ method -> MatcherAssert.assertThat(method, not(invokesMethodWithName("getClassLoader"))));
+ }
+}