Add ReflectiveCallExtractor
- Allows one to extract all reflective calls
done from a jar.
Change-Id: Iae5aa812aa435f57230972079fef089ae5bac1fc
diff --git a/src/main/java/com/android/tools/r8/assistant/InstrumentedReflectiveMethodList.java b/src/main/java/com/android/tools/r8/assistant/InstrumentedReflectiveMethodList.java
new file mode 100644
index 0000000..8702d0a
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/assistant/InstrumentedReflectiveMethodList.java
@@ -0,0 +1,80 @@
+// 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.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+import com.google.common.collect.ImmutableMap;
+import java.util.Map;
+import java.util.Set;
+
+public class InstrumentedReflectiveMethodList {
+
+ private DexItemFactory factory;
+ private ReflectiveReferences reflectiveReferences;
+
+ public InstrumentedReflectiveMethodList(DexItemFactory factory) {
+ this.factory = factory;
+ reflectiveReferences = new ReflectiveReferences(factory);
+ }
+
+ public Set<DexMethod> getInstrumentedMethodsForTesting() {
+ return getInstrumentedMethodsAndTargets().keySet();
+ }
+
+ Map<DexMethod, DexMethod> getInstrumentedMethodsAndTargets() {
+ return ImmutableMap.of(
+ factory.classMethods.newInstance,
+ getMethodReferenceWithClassParameter("onClassNewInstance"),
+ factory.classMethods.getDeclaredMethod,
+ getMethodReferenceWithClassMethodNameAndParameters("onClassGetDeclaredMethod"),
+ factory.classMethods.forName,
+ getMethodReferenceWithStringParameter("onClassForName"),
+ factory.classMethods.getDeclaredField,
+ getMethodReferenceWithClassAndStringParameter("onClassGetDeclaredField"),
+ factory.createMethod(
+ factory.classType,
+ factory.createProto(factory.createArrayType(1, factory.methodType)),
+ "getDeclaredMethods"),
+ getMethodReferenceWithClassParameter("onClassGetDeclaredMethods"),
+ factory.classMethods.getName,
+ getMethodReferenceWithClassParameter("onClassGetName"),
+ factory.classMethods.getCanonicalName,
+ getMethodReferenceWithClassParameter("onClassGetCanonicalName"),
+ factory.classMethods.getSimpleName,
+ getMethodReferenceWithClassParameter("onClassGetSimpleName"),
+ factory.classMethods.getTypeName,
+ getMethodReferenceWithClassParameter("onClassGetTypeName"),
+ factory.classMethods.getSuperclass,
+ getMethodReferenceWithClassParameter("onClassGetSuperclass"));
+ }
+
+ private DexMethod getMethodReferenceWithClassParameter(String name) {
+ return getMethodReferenceWithParameterTypes(name, factory.classType);
+ }
+
+ private DexMethod getMethodReferenceWithClassAndStringParameter(String name) {
+ return getMethodReferenceWithParameterTypes(name, factory.classType, factory.stringType);
+ }
+
+ private DexMethod getMethodReferenceWithStringParameter(String name) {
+ return getMethodReferenceWithParameterTypes(name, factory.stringType);
+ }
+
+ private DexMethod getMethodReferenceWithParameterTypes(String name, DexType... dexTypes) {
+ return factory.createMethod(
+ reflectiveReferences.reflectiveOracleType,
+ factory.createProto(factory.voidType, dexTypes),
+ name);
+ }
+
+ private DexMethod getMethodReferenceWithClassMethodNameAndParameters(String name) {
+ return factory.createMethod(
+ reflectiveReferences.reflectiveOracleType,
+ factory.createProto(
+ factory.voidType, factory.classType, factory.stringType, factory.classArrayType),
+ name);
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/assistant/ReflectiveInstrumentation.java b/src/main/java/com/android/tools/r8/assistant/ReflectiveInstrumentation.java
index d3567a1..e2dc6ad 100644
--- a/src/main/java/com/android/tools/r8/assistant/ReflectiveInstrumentation.java
+++ b/src/main/java/com/android/tools/r8/assistant/ReflectiveInstrumentation.java
@@ -25,7 +25,7 @@
import com.android.tools.r8.ir.optimize.info.OptimizationFeedback;
import com.android.tools.r8.utils.timing.Timing;
import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
+import java.util.Map;
public class ReflectiveInstrumentation {
@@ -72,8 +72,8 @@
// TODO(b/394013779): Do this in parallel.
public void instrumentClasses() {
- ImmutableMap<DexMethod, DexMethod> instrumentedMethodsAndTargets =
- getInstrumentedMethodsAndTargets();
+ Map<DexMethod, DexMethod> instrumentedMethodsAndTargets =
+ new InstrumentedReflectiveMethodList(dexItemFactory).getInstrumentedMethodsAndTargets();
for (DexProgramClass clazz : appView.appInfo().classes()) {
clazz.forEachProgramMethodMatching(
method -> method.hasCode() && method.getCode().isDexCode(),
@@ -109,65 +109,6 @@
}
}
- private ImmutableMap<DexMethod, DexMethod> getInstrumentedMethodsAndTargets() {
- return ImmutableMap.of(
- dexItemFactory.classMethods.newInstance,
- getMethodReferenceWithClassParameter("onClassNewInstance"),
- dexItemFactory.classMethods.getDeclaredMethod,
- getMethodReferenceWithClassMethodNameAndParameters("onClassGetDeclaredMethod"),
- dexItemFactory.classMethods.forName,
- getMethodReferenceWithStringParameter("onClassForName"),
- dexItemFactory.classMethods.getDeclaredField,
- getMethodReferenceWithClassAndStringParameter("onClassGetDeclaredField"),
- dexItemFactory.createMethod(
- dexItemFactory.classType,
- dexItemFactory.createProto(
- dexItemFactory.createArrayType(1, dexItemFactory.methodType)),
- "getDeclaredMethods"),
- getMethodReferenceWithClassParameter("onClassGetDeclaredMethods"),
- dexItemFactory.classMethods.getName,
- getMethodReferenceWithClassParameter("onClassGetName"),
- dexItemFactory.classMethods.getCanonicalName,
- getMethodReferenceWithClassParameter("onClassGetCanonicalName"),
- dexItemFactory.classMethods.getSimpleName,
- getMethodReferenceWithClassParameter("onClassGetSimpleName"),
- dexItemFactory.classMethods.getTypeName,
- getMethodReferenceWithClassParameter("onClassGetTypeName"),
- dexItemFactory.classMethods.getSuperclass,
- getMethodReferenceWithClassParameter("onClassGetSuperclass"));
- }
-
- private DexMethod getMethodReferenceWithClassParameter(String name) {
- return getMethodReferenceWithParameterTypes(name, dexItemFactory.classType);
- }
-
- private DexMethod getMethodReferenceWithClassAndStringParameter(String name) {
- return getMethodReferenceWithParameterTypes(
- name, dexItemFactory.classType, dexItemFactory.stringType);
- }
-
- private DexMethod getMethodReferenceWithStringParameter(String name) {
- return getMethodReferenceWithParameterTypes(name, dexItemFactory.stringType);
- }
-
- private DexMethod getMethodReferenceWithParameterTypes(String name, DexType... dexTypes) {
- return dexItemFactory.createMethod(
- reflectiveReferences.reflectiveOracleType,
- dexItemFactory.createProto(dexItemFactory.voidType, dexTypes),
- name);
- }
-
- private DexMethod getMethodReferenceWithClassMethodNameAndParameters(String name) {
- return dexItemFactory.createMethod(
- reflectiveReferences.reflectiveOracleType,
- dexItemFactory.createProto(
- dexItemFactory.voidType,
- dexItemFactory.classType,
- dexItemFactory.stringType,
- dexItemFactory.classArrayType),
- name);
- }
-
private void insertCallToMethod(
DexMethod method,
IRCode code,
diff --git a/src/test/java/com/android/tools/r8/assistant/GsonReflectiveCallExtractorTest.java b/src/test/java/com/android/tools/r8/assistant/GsonReflectiveCallExtractorTest.java
new file mode 100644
index 0000000..02a40a5
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/assistant/GsonReflectiveCallExtractorTest.java
@@ -0,0 +1,63 @@
+// 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 static com.android.tools.r8.assistant.ReflectiveCallExtractor.extractReflectiveCalls;
+import static com.android.tools.r8.assistant.ReflectiveCallExtractor.printMethods;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+import org.junit.Assert;
+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 GsonReflectiveCallExtractorTest extends TestBase {
+
+ @Parameter(0)
+ public TestParameters parameters;
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withNoneRuntime().build();
+ }
+
+ @Test
+ public void testGson() throws Exception {
+ DexItemFactory factory = new DexItemFactory();
+ Map<DexType, Collection<DexMethod>> reflectiveMethods =
+ extractReflectiveCalls(ToolHelper.GSON, factory);
+ Set<DexMethod> instrumentedMethodsForTesting =
+ new InstrumentedReflectiveMethodList(factory).getInstrumentedMethodsForTesting();
+ int supported = 0;
+ int unsupported = 0;
+ for (DexType dexType : reflectiveMethods.keySet()) {
+ Collection<DexMethod> methods = reflectiveMethods.get(dexType);
+ ArrayList<DexMethod> toRemove = new ArrayList<>();
+ for (DexMethod dexMethod : methods) {
+ if (instrumentedMethodsForTesting.contains(dexMethod)) {
+ toRemove.add(dexMethod);
+ supported++;
+ } else {
+ unsupported++;
+ }
+ }
+ methods.removeAll(toRemove);
+ }
+ Assert.assertEquals(6, supported);
+ Assert.assertEquals("Missing :\n" + printMethods(reflectiveMethods), 69, unsupported);
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/assistant/ReflectiveCallExtractor.java b/src/test/java/com/android/tools/r8/assistant/ReflectiveCallExtractor.java
new file mode 100644
index 0000000..4dd7e86
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/assistant/ReflectiveCallExtractor.java
@@ -0,0 +1,94 @@
+// 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.ArchiveProgramResourceProvider;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.cf.code.CfInstruction;
+import com.android.tools.r8.cf.code.CfInvoke;
+import com.android.tools.r8.dex.ApplicationReader;
+import com.android.tools.r8.graph.AppInfo;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexEncodedMethod;
+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.DexType;
+import com.android.tools.r8.graph.LazyLoadedDexApplication;
+import com.android.tools.r8.synthesis.SyntheticItems.GlobalSyntheticsStrategy;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.Reporter;
+import com.android.tools.r8.utils.ThreadUtils;
+import com.android.tools.r8.utils.timing.Timing;
+import com.google.common.collect.Sets;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+
+public class ReflectiveCallExtractor {
+
+ public static Map<DexType, Collection<DexMethod>> extractReflectiveCalls(
+ Path jar, DexItemFactory factory) throws IOException {
+ AndroidApp app =
+ AndroidApp.builder()
+ .addProgramResourceProvider(ArchiveProgramResourceProvider.fromArchive(jar))
+ .addLibraryFile(ToolHelper.getAndroidJar(AndroidApiLevel.V))
+ .build();
+ InternalOptions options = new InternalOptions(factory, new Reporter());
+ ApplicationReader applicationReader = new ApplicationReader(app, options, Timing.empty());
+ LazyLoadedDexApplication dexApplication =
+ applicationReader.read(ThreadUtils.getExecutorService(options));
+ AppInfo appInfo =
+ AppInfo.createInitialAppInfo(dexApplication, GlobalSyntheticsStrategy.forNonSynthesizing());
+ Map<DexType, Collection<DexMethod>> methods = new IdentityHashMap<>();
+ for (DexProgramClass programClass : appInfo.classes()) {
+ for (DexEncodedMethod method : programClass.methods()) {
+ if (method.hasCode()) {
+ for (CfInstruction instruction : method.getCode().asCfCode().getInstructions()) {
+ if (instruction.isInvoke()) {
+ CfInvoke cfInvoke = instruction.asInvoke();
+ DexMethod theMethod = cfInvoke.getMethod();
+ DexType holderType = theMethod.getHolderType();
+ DexClass def = appInfo.definitionFor(holderType);
+ if (!holderType.isArrayType() && def.isLibraryClass()) {
+ if (holderType.toSourceString().startsWith("java.lang.reflect")
+ || holderType.isIdenticalTo(appInfo.dexItemFactory().classType)) {
+ methods
+ .computeIfAbsent(holderType, t -> Sets.newIdentityHashSet())
+ .add(theMethod);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ for (DexType dexType : methods.keySet()) {
+ ArrayList<DexMethod> sorted = new ArrayList<>(methods.get(dexType));
+ sorted.sort(Comparator.naturalOrder());
+ methods.put(dexType, sorted);
+ }
+ return methods;
+ }
+
+ public static String printMethods(Map<DexType, Collection<DexMethod>> methods) {
+ StringBuilder sb = new StringBuilder();
+ 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");
+ }
+ }
+ return sb.toString();
+ }
+}