blob: 9fe3bb4ede86f0c48d73bb08b8d9b5533247032e [file] [log] [blame]
// Copyright (c) 2018, 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.bridgeremoval;
import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertEquals;
import com.android.tools.r8.TestBase;
import com.android.tools.r8.ToolHelper;
import com.android.tools.r8.ToolHelper.ProcessResult;
import com.android.tools.r8.code.InvokeVirtual;
import com.android.tools.r8.code.ReturnVoid;
import com.android.tools.r8.graph.DexCode;
import com.android.tools.r8.jasmin.JasminBuilder;
import com.android.tools.r8.jasmin.JasminBuilder.ClassBuilder;
import com.android.tools.r8.utils.AndroidApp;
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.codeinspector.ClassSubject;
import com.android.tools.r8.utils.codeinspector.CodeInspector;
import com.android.tools.r8.utils.codeinspector.MethodSubject;
import com.google.common.collect.ImmutableList;
import java.nio.file.Path;
import org.junit.Test;
public class B77836766 extends TestBase {
/**
* The below Jasmin code mimics the following Kotlin code:
*
* package java_pkg;
*
* public interface Itf1 {
* void foo(String arg);
* }
*
* public interface Itf2 {
* void foo(Integer arg);
* }
*
* package kt_pkg;
*
* internal abstract class AbsCls<T> {
* void foo(T obj) { ... }
* }
*
* internal class Cls1() : AbsCls<String>(), Itf1
*
* internal class Cls2() : AbsCls<Integer>(), Itf2
*
*
* kotlinc introduced bridge methods Cls?#foo to AbsCls#foo:
*
* class Cls1 extends AbsCls implements Itf1 {
* public bridge synthetic void foo(String arg) {
* invoke-virtual Cls1#foo(Object)V
* }
* }
*
* Note that we can't write such code in Java because javac requires Itf?#foo, which are
* technically abstract methods, to be explicitly overridden.
*/
@Test
public void test_bridgeTargetInBase_differentBridges() throws Exception {
JasminBuilder jasminBuilder = new JasminBuilder();
ClassBuilder absCls = jasminBuilder.addClass("AbsCls");
absCls.setAccess("public abstract");
absCls.addFinalMethod("foo", ImmutableList.of("Ljava/lang/Object;"), "V",
".limit stack 3",
".limit locals 2",
"getstatic java/lang/System/out Ljava/io/PrintStream;",
"aload_1",
"invokevirtual java/io/PrintStream/print(Ljava/lang/Object;)V",
"return");
ClassBuilder itf1 = jasminBuilder.addInterface("Itf1");
itf1.addAbstractMethod("foo", ImmutableList.of("Ljava/lang/String;"), "V");
ClassBuilder cls1 = jasminBuilder.addClass("Cls1", absCls.name, itf1.name);
// Mimic Kotlin's "internal" class
cls1.setAccess("");
cls1.addBridgeMethod("foo", ImmutableList.of("Ljava/lang/String;"), "V",
".limit stack 2",
".limit locals 2",
"aload_0",
"aload_1",
"invokevirtual " + cls1.name + "/foo(Ljava/lang/Object;)V",
"return");
ClassBuilder itf2 = jasminBuilder.addInterface("Itf2");
itf2.addAbstractMethod("foo", ImmutableList.of("Ljava/lang/Integer;"), "V");
ClassBuilder cls2 = jasminBuilder.addClass("Cls2", absCls.name, itf2.name);
// Mimic Kotlin's "internal" class
cls2.setAccess("");
cls2.addBridgeMethod("foo", ImmutableList.of("Ljava/lang/Integer;"), "V",
".limit stack 2",
".limit locals 2",
"aload_0",
"aload_1",
"invokevirtual " + cls2.name + "/foo(Ljava/lang/Object;)V",
"return");
ClassBuilder mainClass = jasminBuilder.addClass("Main");
mainClass.addMainMethod(
".limit stack 5",
".limit locals 2",
"new " + cls1.name,
"dup",
"invokespecial " + cls1.name + "/<init>()V",
"astore_0",
"aload_0",
"ldc \"Hello\"",
"invokevirtual " + cls1.name + "/foo(Ljava/lang/String;)V",
"new " + cls2.name,
"dup",
"invokespecial " + cls2.name + "/<init>()V",
"astore_0",
"aload_0",
"iconst_0",
"invokestatic java/lang/Integer/valueOf(I)Ljava/lang/Integer;",
"invokevirtual " + cls2.name + "/foo(Ljava/lang/Integer;)V",
"return"
);
final String mainClassName = mainClass.name;
String proguardConfig = keepMainProguardConfiguration(mainClass.name, false, false);
AndroidApp processedApp = runAndVerifyOnJvmAndArt(jasminBuilder, mainClassName, proguardConfig);
CodeInspector inspector = new CodeInspector(processedApp);
ClassSubject absSubject = inspector.clazz(absCls.name);
assertThat(absSubject, isPresent());
ClassSubject cls1Subject = inspector.clazz(cls1.name);
assertThat(cls1Subject, isPresent());
ClassSubject cls2Subject = inspector.clazz(cls2.name);
assertThat(cls2Subject, isPresent());
// Cls1#foo and Cls2#foo should not refer to each other.
// They can invoke their own bridge method or AbsCls#foo (via member rebinding).
MethodSubject fooInCls2 =
cls2Subject.method("void", "foo", ImmutableList.of("java.lang.Integer"));
assertThat(fooInCls2, isPresent());
DexCode code = fooInCls2.getMethod().getCode().asDexCode();
checkInstructions(code, ImmutableList.of(InvokeVirtual.class, ReturnVoid.class));
InvokeVirtual invoke = (InvokeVirtual) code.instructions[0];
assertEquals(absSubject.getDexClass().type, invoke.getMethod().holder);
MethodSubject fooInCls1 =
cls1Subject.method("void", "foo", ImmutableList.of("java.lang.String"));
assertThat(fooInCls1, isPresent());
code = fooInCls1.getMethod().getCode().asDexCode();
checkInstructions(code, ImmutableList.of(InvokeVirtual.class, ReturnVoid.class));
invoke = (InvokeVirtual) code.instructions[0];
assertEquals(absSubject.getDexClass().type, invoke.getMethod().holder);
}
/**
* class Base {
* void foo(Object o) {...}
* }
* interface ItfInteger {
* void foo(Integer o);
* }
* class DerivedInteger extends Base implements ItfInteger {
* // Bridge method deferring to Base#foo(Object):
* public bridge synthetic void foo(Integer o) {
* foo((Object) o);
* } }
* class DerivedString extends Base {
* // Regular non-bridge method calling Base#foo(Object):
* public void bar(String o) {
* foo(o);
* } }
*/
@Test
public void test_bridgeTargetInBase_bridgeAndNonBridge() throws Exception {
JasminBuilder jasminBuilder = new JasminBuilder();
ClassBuilder baseCls = jasminBuilder.addClass("Base");
baseCls.addVirtualMethod("foo", ImmutableList.of("Ljava/lang/Object;"), "V",
".limit stack 3",
".limit locals 2",
"getstatic java/lang/System/out Ljava/io/PrintStream;",
"aload_1",
"invokevirtual java/io/PrintStream/print(Ljava/lang/Object;)V",
"return");
ClassBuilder itf = jasminBuilder.addInterface("ItfInteger");
itf.addAbstractMethod("foo", ImmutableList.of("Ljava/lang/Integer;"), "V");
ClassBuilder cls1 = jasminBuilder.addClass("DerivedInteger", baseCls.name, itf.name);
cls1.addBridgeMethod("foo", ImmutableList.of("Ljava/lang/Integer;"), "V",
".limit stack 2",
".limit locals 2",
"aload_0",
"aload_1",
"invokevirtual " + cls1.name + "/foo(Ljava/lang/Object;)V",
"return");
ClassBuilder cls2 = jasminBuilder.addClass("DerivedString", baseCls.name);
cls2.addVirtualMethod("bar", ImmutableList.of("Ljava/lang/String;"), "V",
".limit stack 2",
".limit locals 2",
"aload_0",
"aload_1",
"invokevirtual " + cls2.name + "/foo(Ljava/lang/Object;)V",
"return");
ClassBuilder mainClass = jasminBuilder.addClass("Main");
mainClass.addMainMethod(
".limit stack 5",
".limit locals 2",
"new " + cls1.name,
"dup",
"invokespecial " + cls1.name + "/<init>()V",
"astore_0",
"aload_0",
"iconst_0",
"invokestatic java/lang/Integer/valueOf(I)Ljava/lang/Integer;",
"invokevirtual " + cls1.name + "/foo(Ljava/lang/Integer;)V",
"new " + cls2.name,
"dup",
"invokespecial " + cls2.name + "/<init>()V",
"astore_0",
"aload_0",
"ldc \"Bar\"",
"invokevirtual " + cls2.name + "/bar(Ljava/lang/String;)V",
"return"
);
final String mainClassName = mainClass.name;
String proguardConfig = keepMainProguardConfiguration(mainClass.name, false, false);
AndroidApp processedApp = runAndVerifyOnJvmAndArt(jasminBuilder, mainClassName, proguardConfig);
CodeInspector inspector = new CodeInspector(processedApp);
ClassSubject baseSubject = inspector.clazz(baseCls.name);
assertThat(baseSubject, isPresent());
ClassSubject cls1Subject = inspector.clazz(cls1.name);
assertThat(cls1Subject, isPresent());
ClassSubject cls2Subject = inspector.clazz(cls2.name);
assertThat(cls2Subject, isPresent());
// Cls1#foo and Cls2#bar should refer to Base#foo.
MethodSubject barInCls2 =
cls2Subject.method("void", "bar", ImmutableList.of("java.lang.String"));
assertThat(barInCls2, isPresent());
DexCode code = barInCls2.getMethod().getCode().asDexCode();
checkInstructions(code, ImmutableList.of(InvokeVirtual.class, ReturnVoid.class));
InvokeVirtual invoke = (InvokeVirtual) code.instructions[0];
assertEquals(baseSubject.getDexClass().type, invoke.getMethod().holder);
MethodSubject fooInCls1 =
cls1Subject.method("void", "foo", ImmutableList.of("java.lang.Integer"));
assertThat(fooInCls1, isPresent());
code = fooInCls1.getMethod().getCode().asDexCode();
checkInstructions(code, ImmutableList.of(InvokeVirtual.class, ReturnVoid.class));
invoke = (InvokeVirtual) code.instructions[0];
assertEquals(baseSubject.getDexClass().type, invoke.getMethod().holder);
}
/**
* class Base {
* protected void foo(Object o) { ... }
* // Bridge method deferring to Base#foo(Object):
* public bridge synthetic void foo(Integer o) {
* foo((Object) o);
* } }
* class DerivedString extends Base {
* // Regular non-bridge method calling Base#foo(Object):
* public void bar(String o) {
* foo(o);
* } }
*/
@Test
public void test_nonBridgeInSubType() throws Exception {
JasminBuilder jasminBuilder = new JasminBuilder();
ClassBuilder baseCls = jasminBuilder.addClass("Base");
baseCls.addVirtualMethod("foo", ImmutableList.of("Ljava/lang/Object;"), "V",
".limit stack 3",
".limit locals 2",
"getstatic java/lang/System/out Ljava/io/PrintStream;",
"aload_1",
"invokevirtual java/io/PrintStream/print(Ljava/lang/Object;)V",
"return");
baseCls.addBridgeMethod("foo", ImmutableList.of("Ljava/lang/Integer;"), "V",
".limit stack 2",
".limit locals 2",
"aload_0",
"aload_1",
"invokevirtual " + baseCls.name + "/foo(Ljava/lang/Object;)V",
"return");
ClassBuilder subCls = jasminBuilder.addClass("DerivedString", baseCls.name);
subCls.addVirtualMethod("bar", ImmutableList.of("Ljava/lang/String;"), "V",
".limit stack 2",
".limit locals 2",
"aload_0",
"aload_1",
"invokevirtual " + subCls.name + "/foo(Ljava/lang/Object;)V",
"return");
ClassBuilder mainClass = jasminBuilder.addClass("Main");
mainClass.addMainMethod(
".limit stack 5",
".limit locals 2",
"new " + baseCls.name,
"dup",
"invokespecial " + baseCls.name + "/<init>()V",
"astore_0",
"aload_0",
"iconst_0",
"invokestatic java/lang/Integer/valueOf(I)Ljava/lang/Integer;",
"invokevirtual " + baseCls.name + "/foo(Ljava/lang/Integer;)V",
"new " + subCls.name,
"dup",
"invokespecial " + subCls.name + "/<init>()V",
"astore_0",
"aload_0",
"ldc \"Bar\"",
"invokevirtual " + subCls.name + "/bar(Ljava/lang/String;)V",
"return"
);
final String mainClassName = mainClass.name;
String proguardConfig = keepMainProguardConfiguration(mainClass.name, false, false);
AndroidApp processedApp = runAndVerifyOnJvmAndArt(jasminBuilder, mainClassName, proguardConfig);
CodeInspector inspector = new CodeInspector(processedApp);
ClassSubject baseSubject = inspector.clazz(baseCls.name);
assertThat(baseSubject, isPresent());
ClassSubject subSubject = inspector.clazz(subCls.name);
assertThat(subSubject, isPresent());
// DerivedString2#bar should refer to Base#foo.
MethodSubject barInSub =
subSubject.method("void", "bar", ImmutableList.of("java.lang.String"));
assertThat(barInSub, isPresent());
DexCode code = barInSub.getMethod().getCode().asDexCode();
checkInstructions(code, ImmutableList.of(InvokeVirtual.class, ReturnVoid.class));
InvokeVirtual invoke = (InvokeVirtual) code.instructions[0];
assertEquals(baseSubject.getDexClass().type, invoke.getMethod().holder);
}
/*
* public class Base {
* public bridge void foo(Integer i) { foo((Object) i); }
* public foo(Object o) { print(o); }
* public bar(String s) { foo(s); }
* }
*/
@Test
public void test_bridgeTargetInsideTheSameClass() throws Exception {
JasminBuilder jasminBuilder = new JasminBuilder();
ClassBuilder cls = jasminBuilder.addClass("Base");
cls.addBridgeMethod("foo", ImmutableList.of("Ljava/lang/Integer;"), "V",
".limit stack 2",
".limit locals 2",
"aload_0",
"aload_1",
"invokevirtual " + cls.name + "/foo(Ljava/lang/Object;)V",
"return");
cls.addVirtualMethod("foo", ImmutableList.of("Ljava/lang/Object;"), "V",
".limit stack 3",
".limit locals 2",
"getstatic java/lang/System/out Ljava/io/PrintStream;",
"aload_1",
"invokevirtual java/io/PrintStream/print(Ljava/lang/Object;)V",
"return");
cls.addVirtualMethod("bar", ImmutableList.of("Ljava/lang/String;"), "V",
".limit stack 2",
".limit locals 2",
"aload_0",
"aload_1",
"invokevirtual " + cls.name + "/foo(Ljava/lang/Object;)V",
"return");
ClassBuilder mainClass = jasminBuilder.addClass("Main");
mainClass.addMainMethod(
".limit stack 5",
".limit locals 2",
"new " + cls.name,
"dup",
"invokespecial " + cls.name + "/<init>()V",
"astore_0",
"aload_0",
"iconst_0",
"invokestatic java/lang/Integer/valueOf(I)Ljava/lang/Integer;",
"invokevirtual " + cls.name + "/foo(Ljava/lang/Integer;)V",
"new " + cls.name,
"dup",
"invokespecial " + cls.name + "/<init>()V",
"astore_0",
"aload_0",
"ldc \"Bar\"",
"invokevirtual " + cls.name + "/bar(Ljava/lang/String;)V",
"return"
);
final String mainClassName = mainClass.name;
String proguardConfig = keepMainProguardConfiguration(mainClass.name, false, false);
AndroidApp processedApp = runAndVerifyOnJvmAndArt(jasminBuilder, mainClassName, proguardConfig);
CodeInspector inspector = new CodeInspector(processedApp);
ClassSubject baseSubject = inspector.clazz(cls.name);
assertThat(baseSubject, isPresent());
// Base#bar should remain as-is, i.e., refer to Base#foo(Object).
MethodSubject barInSub =
baseSubject.method("void", "bar", ImmutableList.of("java.lang.String"));
assertThat(barInSub, isPresent());
DexCode code = barInSub.getMethod().getCode().asDexCode();
checkInstructions(code, ImmutableList.of(InvokeVirtual.class, ReturnVoid.class));
InvokeVirtual invoke = (InvokeVirtual) code.instructions[0];
assertEquals(baseSubject.getDexClass().type, invoke.getMethod().holder);
}
private AndroidApp runAndVerifyOnJvmAndArt(
JasminBuilder jasminBuilder, String mainClassName, String proguardConfig) throws Exception {
// Run input program on java.
Path outputDirectory = temp.newFolder().toPath();
jasminBuilder.writeClassFiles(outputDirectory);
ProcessResult javaResult = ToolHelper.runJava(outputDirectory, mainClassName);
assertEquals(0, javaResult.exitCode);
AndroidApp processedApp = compileWithR8(jasminBuilder.build(), proguardConfig, this::configure);
// Run processed (output) program on ART
ProcessResult artResult = runOnArtRaw(processedApp, mainClassName);
assertEquals(javaResult.stdout, artResult.stdout);
assertEquals(-1, artResult.stderr.indexOf("VerifyError"));
return processedApp;
}
private void configure(InternalOptions options) {
// Callees are invoked with a simple constant, e.g., "Bar". Propagating it into the callees
// bothers what the tests want to check, such as exact instructions in the body that include
// invocation kinds, like virtual call to a bridge.
options.enablePropagationOfConstantsAtCallSites = false;
// Disable inlining to avoid the (short) tested method from being inlined and then removed.
options.enableInlining = false;
}
}