R8 Assistant: Support Proxy and ServiceLoader
Change-Id: Ifa630002516bd14c21a603f48f6cbadaa32e16d4
diff --git a/src/assistant/java/com/android/tools/r8/assistant/runtime/EmptyReflectiveOperationReceiver.java b/src/assistant/java/com/android/tools/r8/assistant/runtime/EmptyReflectiveOperationReceiver.java
index 0aa1326..d0a7baf 100644
--- a/src/assistant/java/com/android/tools/r8/assistant/runtime/EmptyReflectiveOperationReceiver.java
+++ b/src/assistant/java/com/android/tools/r8/assistant/runtime/EmptyReflectiveOperationReceiver.java
@@ -5,6 +5,7 @@
import com.android.tools.r8.assistant.runtime.ReflectiveOracle.Stack;
import com.android.tools.r8.keepanno.annotations.KeepForApi;
+import java.lang.reflect.InvocationHandler;
@KeepForApi
public class EmptyReflectiveOperationReceiver implements ReflectiveOperationReceiver {
@@ -63,4 +64,14 @@
@Override
public void onAtomicReferenceFieldUpdaterNewUpdater(
Stack stack, Class<?> clazz, Class<?> fieldClass, String name) {}
+
+ @Override
+ public void onServiceLoaderLoad(Stack stack, Class<?> clazz, ClassLoader classLoader) {}
+
+ @Override
+ public void onProxyNewProxyInstance(
+ Stack stack,
+ ClassLoader classLoader,
+ Class<?>[] interfaces,
+ InvocationHandler invocationHandler) {}
}
diff --git a/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOperationLogger.java b/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOperationLogger.java
index 5df728a..71f8b99 100644
--- a/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOperationLogger.java
+++ b/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOperationLogger.java
@@ -6,6 +6,8 @@
import com.android.tools.r8.assistant.runtime.ReflectiveOracle.Stack;
import com.android.tools.r8.keepanno.annotations.KeepForApi;
+import java.lang.reflect.InvocationHandler;
+import java.util.Arrays;
@KeepForApi
public class ReflectiveOperationLogger implements ReflectiveOperationReceiver {
@@ -107,6 +109,26 @@
+ name);
}
+ @Override
+ public void onServiceLoaderLoad(Stack stack, Class<?> clazz, ClassLoader classLoader) {
+ System.out.println("Reflectively got ServiceLoader.load on " + clazz + " with " + classLoader);
+ }
+
+ @Override
+ public void onProxyNewProxyInstance(
+ Stack stack,
+ ClassLoader classLoader,
+ Class<?>[] interfaces,
+ InvocationHandler invocationHandler) {
+ System.out.println(
+ "Reflectively got Proxy.newProxyInstance on "
+ + Arrays.toString(interfaces)
+ + " with "
+ + classLoader
+ + " and "
+ + invocationHandler);
+ }
+
public boolean requiresStackInformation() {
return true;
}
diff --git a/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOperationReceiver.java b/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOperationReceiver.java
index 79ff0e5..69088f1 100644
--- a/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOperationReceiver.java
+++ b/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOperationReceiver.java
@@ -5,6 +5,7 @@
import com.android.tools.r8.assistant.runtime.ReflectiveOracle.Stack;
import com.android.tools.r8.keepanno.annotations.KeepForApi;
+import java.lang.reflect.InvocationHandler;
@KeepForApi
public interface ReflectiveOperationReceiver {
@@ -48,6 +49,14 @@
void onAtomicReferenceFieldUpdaterNewUpdater(
Stack stack, Class<?> clazz, Class<?> fieldClass, String name);
+ void onServiceLoaderLoad(Stack stack, Class<?> clazz, ClassLoader classLoader);
+
+ void onProxyNewProxyInstance(
+ Stack stack,
+ ClassLoader classLoader,
+ Class<?>[] interfaces,
+ InvocationHandler invocationHandler);
+
@KeepForApi
enum ClassFlag {
ANNOTATION,
diff --git a/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOracle.java b/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOracle.java
index 54d0eb9..aaacd87 100644
--- a/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOracle.java
+++ b/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOracle.java
@@ -6,6 +6,7 @@
import com.android.tools.r8.assistant.runtime.ReflectiveOperationReceiver.ClassFlag;
import com.android.tools.r8.assistant.runtime.ReflectiveOperationReceiver.NameLookupType;
import com.android.tools.r8.keepanno.annotations.KeepForApi;
+import java.lang.reflect.InvocationHandler;
import java.util.Arrays;
@KeepForApi
@@ -196,4 +197,23 @@
getInstance()
.onAtomicReferenceFieldUpdaterNewUpdater(Stack.createStack(), clazz, fieldClass, name);
}
+
+ public static void onServiceLoaderLoad(Class<?> clazz) {
+ getInstance().onServiceLoaderLoad(Stack.createStack(), clazz, null);
+ }
+
+ public static void onServiceLoaderLoadWithClassLoader(Class<?> clazz, ClassLoader classLoader) {
+ getInstance().onServiceLoaderLoad(Stack.createStack(), clazz, classLoader);
+ }
+
+ public static void onServiceLoaderLoadInstalled(Class<?> clazz) {
+ getInstance()
+ .onServiceLoaderLoad(Stack.createStack(), clazz, ClassLoader.getSystemClassLoader());
+ }
+
+ public static void onProxyNewProxyInstance(
+ ClassLoader classLoader, Class<?>[] interfaces, InvocationHandler invocationHandler) {
+ getInstance()
+ .onProxyNewProxyInstance(Stack.createStack(), classLoader, interfaces, invocationHandler);
+ }
}
diff --git a/src/main/java/com/android/tools/r8/assistant/InstrumentedReflectiveMethodList.java b/src/main/java/com/android/tools/r8/assistant/InstrumentedReflectiveMethodList.java
index 928982f..75a08d7 100644
--- a/src/main/java/com/android/tools/r8/assistant/InstrumentedReflectiveMethodList.java
+++ b/src/main/java/com/android/tools/r8/assistant/InstrumentedReflectiveMethodList.java
@@ -144,6 +144,25 @@
factory.classType,
factory.stringType));
+ builder.put(
+ factory.serviceLoaderMethods.load,
+ getMethodReferenceWithClassParameter("onServiceLoaderLoad"));
+ builder.put(
+ factory.serviceLoaderMethods.loadWithClassLoader,
+ getMethodReferenceWithParameterTypes(
+ "onServiceLoaderLoadWithClassLoader", factory.classType, factory.classLoaderType));
+ builder.put(
+ factory.serviceLoaderMethods.loadInstalled,
+ getMethodReferenceWithClassParameter("onServiceLoaderLoadInstalled"));
+
+ builder.put(
+ factory.proxyMethods.newProxyInstance,
+ getMethodReferenceWithParameterTypes(
+ "onProxyNewProxyInstance",
+ factory.classLoaderType,
+ factory.classArrayType,
+ factory.invocationHandlerType));
+
return builder.build();
}
diff --git a/src/test/java/com/android/tools/r8/assistant/ProxyTest.java b/src/test/java/com/android/tools/r8/assistant/ProxyTest.java
new file mode 100644
index 0000000..6aabd15
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/assistant/ProxyTest.java
@@ -0,0 +1,55 @@
+// Copyright (c) 2025, 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.assistant;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.assistant.runtime.EmptyReflectiveOperationReceiver;
+import com.android.tools.r8.assistant.runtime.ReflectiveOracle.Stack;
+import java.lang.reflect.InvocationHandler;
+import java.util.Arrays;
+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 ProxyTest extends TestBase {
+
+ @Parameter(0)
+ public TestParameters parameters;
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withNativeMultidexDexRuntimes().withMaximumApiLevel().build();
+ }
+
+ @Test
+ public void testInstrumentationWithCustomOracle() throws Exception {
+ testForAssistant()
+ .addProgramClassesAndInnerClasses(ProxyTestClass.class)
+ .addInstrumentationClasses(Instrumentation.class)
+ .setCustomReflectiveOperationReceiver(Instrumentation.class)
+ .setMinApi(parameters)
+ .compile()
+ .run(parameters.getRuntime(), ProxyTestClass.class)
+ .assertSuccessWithOutputLines(
+ "[interface com.android.tools.r8.assistant.ProxyTestClass$F]");
+ }
+
+ public static class Instrumentation extends EmptyReflectiveOperationReceiver {
+
+ @Override
+ public void onProxyNewProxyInstance(
+ Stack stack,
+ ClassLoader classLoader,
+ Class<?>[] interfaces,
+ InvocationHandler invocationHandler) {
+ System.out.println(Arrays.toString(interfaces));
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/assistant/ProxyTestClass.java b/src/test/java/com/android/tools/r8/assistant/ProxyTestClass.java
new file mode 100644
index 0000000..008f591
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/assistant/ProxyTestClass.java
@@ -0,0 +1,30 @@
+// Copyright (c) 2025, 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.assistant;
+
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+
+public class ProxyTestClass {
+
+ public static void main(String[] args) {
+ InvocationHandler handler =
+ new InvocationHandler() {
+ @Override
+ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+ return null;
+ }
+ };
+
+ Class<?>[] interfaces = new Class<?>[1];
+ interfaces[0] = F.class;
+ Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), interfaces, handler);
+ }
+
+ interface F {
+ int getInt();
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/assistant/ReflectiveCallExtractor.java b/src/test/java/com/android/tools/r8/assistant/ReflectiveCallExtractor.java
index e57caf9..b7c09d6 100644
--- a/src/test/java/com/android/tools/r8/assistant/ReflectiveCallExtractor.java
+++ b/src/test/java/com/android/tools/r8/assistant/ReflectiveCallExtractor.java
@@ -77,6 +77,9 @@
if (factory.serviceLoaderMethods.isLoadMethod(method)) {
return true;
}
+ if (factory.proxyMethods.newProxyInstance.isIdenticalTo(method)) {
+ return true;
+ }
DexType type = method.getHolderType();
String name = method.getName().toString();
if (type.isIdenticalTo(factory.classType)) {
@@ -91,12 +94,6 @@
}
return true;
}
- if (type.isIdenticalTo(factory.proxyType)) {
- if (name.equals("getProxyClass") || name.equals("newProxyInstance")) {
- return true;
- }
- return false;
- }
if (type.isIdenticalTo(factory.unsafeType)) {
// We assume all offset based methods are called using the right method below to compute the
// offset in the object, we do not support cases where programmers would compute the
@@ -119,9 +116,12 @@
List<DexType> types = new ArrayList<>(methods.keySet());
types.sort(Comparator.naturalOrder());
for (DexType type : types) {
- sb.append("+++ ").append(type).append(" +++").append("\n");
- for (DexMethod dexMethod : methods.get(type)) {
- sb.append(dexMethod).append("\n");
+ Collection<DexMethod> typeMethods = methods.get(type);
+ if (!typeMethods.isEmpty()) {
+ sb.append("+++ ").append(type).append(" +++").append("\n");
+ for (DexMethod dexMethod : typeMethods) {
+ sb.append(dexMethod).append("\n");
+ }
}
}
return sb.toString();
diff --git a/src/test/java/com/android/tools/r8/assistant/ReflectiveCallExtractorTest.java b/src/test/java/com/android/tools/r8/assistant/ReflectiveCallExtractorTest.java
index 374407f..e7f88f0 100644
--- a/src/test/java/com/android/tools/r8/assistant/ReflectiveCallExtractorTest.java
+++ b/src/test/java/com/android/tools/r8/assistant/ReflectiveCallExtractorTest.java
@@ -45,7 +45,7 @@
@Test
public void testGuava() throws Exception {
- test(ToolHelper.GUAVA_JRE, 21, 17);
+ test(ToolHelper.GUAVA_JRE, 22, 16);
}
@Test
@@ -64,7 +64,7 @@
"dump_app.zip");
Path programArchive =
CompilerDump.fromArchive(zip, temp.newFolder().toPath()).getProgramArchive();
- test(programArchive, 26, 23);
+ test(programArchive, 28, 21);
}
private void test(Path jar, int success, int failure) throws Exception {
diff --git a/src/test/java/com/android/tools/r8/assistant/ServiceLoaderTest.java b/src/test/java/com/android/tools/r8/assistant/ServiceLoaderTest.java
new file mode 100644
index 0000000..c5ad5a3d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/assistant/ServiceLoaderTest.java
@@ -0,0 +1,51 @@
+// Copyright (c) 2025, 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.assistant;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.assistant.runtime.EmptyReflectiveOperationReceiver;
+import com.android.tools.r8.assistant.runtime.ReflectiveOracle.Stack;
+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 ServiceLoaderTest extends TestBase {
+
+ @Parameter(0)
+ public TestParameters parameters;
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withNativeMultidexDexRuntimes().withMaximumApiLevel().build();
+ }
+
+ @Test
+ public void testInstrumentationWithCustomOracle() throws Exception {
+ testForAssistant()
+ .addProgramClassesAndInnerClasses(ServiceLoaderTestClass.class)
+ .addInstrumentationClasses(Instrumentation.class)
+ .setCustomReflectiveOperationReceiver(Instrumentation.class)
+ .setMinApi(parameters)
+ .compile()
+ .run(parameters.getRuntime(), ServiceLoaderTestClass.class)
+ .assertSuccessWithOutputLines(
+ "com.android.tools.r8.assistant.ServiceLoaderTestClass$NameService",
+ "com.android.tools.r8.assistant.ServiceLoaderTestClass$NameService",
+ "com.android.tools.r8.assistant.ServiceLoaderTestClass$NameService");
+ }
+
+ public static class Instrumentation extends EmptyReflectiveOperationReceiver {
+
+ @Override
+ public void onServiceLoaderLoad(Stack stack, Class<?> clazz, ClassLoader classLoader) {
+ System.out.println(clazz.getName());
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/assistant/ServiceLoaderTestClass.java b/src/test/java/com/android/tools/r8/assistant/ServiceLoaderTestClass.java
new file mode 100644
index 0000000..8e5861d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/assistant/ServiceLoaderTestClass.java
@@ -0,0 +1,45 @@
+// Copyright (c) 2025, 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.assistant;
+
+import java.util.ServiceLoader;
+
+public class ServiceLoaderTestClass {
+
+ volatile int i;
+ volatile long l;
+ volatile Object o;
+
+ public static void main(String[] args) {
+ ServiceLoader<NameService> loader = ServiceLoader.load(NameService.class);
+ for (NameService nameService : loader) {
+ System.out.println(nameService.getName());
+ }
+
+ ServiceLoader<NameService> loader2 = ServiceLoader.loadInstalled(NameService.class);
+ for (NameService nameService : loader2) {
+ System.out.println(nameService.getName());
+ }
+
+ ServiceLoader<NameService> loader3 =
+ ServiceLoader.load(NameService.class, ServiceLoaderTestClass.class.getClassLoader());
+ for (NameService nameService : loader3) {
+ System.out.println(nameService.getName());
+ }
+ }
+
+ public interface NameService {
+
+ String getName();
+ }
+
+ public class DummyService implements NameService {
+
+ @Override
+ public String getName() {
+ return "dummy";
+ }
+ }
+}