Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 611dfbee authored by John Wu's avatar John Wu
Browse files

[HostStubGen] Improve class load and method call hooks

- Instead of going through a trampoline in HostTestUtils, make hsg
  generate bytecode to directly call the hook methods.
- Add new tests for method call hooks.
- Remove the "ext" variant of tiny-framework, always enable the default
  class load hook and method call hooks.
- Make sure ProcessAsKeep annotation is only injected when the method is
  marked as "@Keep"

Bug: 292141694
Flag: EXEMPT host side change only
Test: f/b/r/scripts/run-ravenwood-tests.sh
Change-Id: Ie09762c7b869507d2103e6a9b270d9edc3a32998
parent 2f3c697c
Loading
Loading
Loading
Loading
+5 −1
Original line number Diff line number Diff line
@@ -25,7 +25,11 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Annotation injected to all classes/methods/fields that are kept in the processes jar.
 * Annotation injected to all classes/methods/fields with a reason why they're kept.
 *
 * For classes and fields, all of them should have this annotation unless it's removed.
 * For methods, only methods containing the original body will have it.
 * Methods that are processed as "ignore", "throw", "substitute", etc. won't have it.
 */
@Target({TYPE, METHOD, CONSTRUCTOR, FIELD})
@Retention(RetentionPolicy.RUNTIME)
+0 −160
Original line number Diff line number Diff line
@@ -15,11 +15,7 @@
 */
package com.android.hoststubgen.hosthelper;

import java.io.PrintStream;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;

/**
 * Utilities used in the host side test environment.
@@ -36,162 +32,6 @@ public class HostTestUtils {
        return clazz.getName().replace('.', '/');
    }

    public static final String CLASS_INTERNAL_NAME = getInternalName(HostTestUtils.class);

    /** If true, we skip all method call hooks */
    private static final boolean SKIP_METHOD_CALL_HOOK = "1".equals(System.getenv(
            "HOSTTEST_SKIP_METHOD_CALL_HOOK"));

    /** If true, we won't print method call log. */
    private static final boolean SKIP_METHOD_LOG =
            "1".equals(System.getenv("HOSTTEST_SKIP_METHOD_LOG"))
            || "1".equals(System.getenv("RAVENWOOD_NO_METHOD_LOG"));

    /** If true, we won't print class load log. */
    private static final boolean SKIP_CLASS_LOG = "1".equals(System.getenv(
            "HOSTTEST_SKIP_CLASS_LOG"));

    /** If true, we won't perform non-stub method direct call check. */
    private static final boolean SKIP_NON_STUB_METHOD_CHECK = "1".equals(System.getenv(
            "HOSTTEST_SKIP_NON_STUB_METHOD_CHECK"));


    /**
     * Method call log will be printed to it.
     */
    public static PrintStream logPrintStream = System.out;

    private static final Class<?>[] sMethodHookArgTypes =
            { Class.class, String.class, String.class};

    /**
     * Trampoline method for method-call-hook.
     */
    public static void callMethodCallHook(
            Class<?> methodClass,
            String methodName,
            String methodDescriptor,
            String callbackMethod
    ) {
        if (SKIP_METHOD_CALL_HOOK) {
            return;
        }
        callStaticMethodByName(callbackMethod, "method call hook", sMethodHookArgTypes,
                methodClass, methodName, methodDescriptor);
    }

    /**
     * Simple implementation of method call hook, which just prints the information of the
     * method. This is just for basic testing. We don't use it in Ravenwood, because this would
     * be way too noisy as it prints every single method, even trivial ones. (iterator methods,
     * etc..)
     *
     * I can be used as
     * {@code --default-method-call-hook
     * com.android.hoststubgen.hosthelper.HostTestUtils.logMethodCall}.
     */
    public static void logMethodCall(
            Class<?> methodClass,
            String methodName,
            String methodDescriptor
    ) {
        if (SKIP_METHOD_LOG) {
            return;
        }
        logPrintStream.println("# method called: " + methodClass.getCanonicalName() + "."
                + methodName + methodDescriptor);
    }

    private static final Class<?>[] sClassLoadHookArgTypes = { Class.class };

    /**
     * Called when any top level class (not nested classes) in the impl jar is loaded.
     *
     * When HostStubGen inject a class-load hook, it's always a call to this method, with the
     * actual method name as the second argument.
     *
     * This method discovers the hook method with reflections and call it.
     *
     * TODO: Add a unit test.
     */
    public static void onClassLoaded(Class<?> loadedClass, String callbackMethod) {
        logPrintStream.println("! Class loaded: " + loadedClass.getCanonicalName()
                + " calling hook " + callbackMethod);

        callStaticMethodByName(
                callbackMethod, "class load hook", sClassLoadHookArgTypes, loadedClass);
    }

    private static void callStaticMethodByName(String classAndMethodName,
            String description, Class<?>[] argTypes, Object... args) {
        // Forward the call to callbackMethod.
        final int lastPeriod = classAndMethodName.lastIndexOf(".");

        if ((lastPeriod) < 0 || (lastPeriod == classAndMethodName.length() - 1)) {
            throw new HostTestException(String.format(
                    "Unable to find %s: malformed method name \"%s\"",
                    description,
                    classAndMethodName));
        }

        final String className = classAndMethodName.substring(0, lastPeriod);
        final String methodName = classAndMethodName.substring(lastPeriod + 1);

        Class<?> clazz = null;
        try {
            clazz = Class.forName(className);
        } catch (Exception e) {
            throw new HostTestException(String.format(
                    "Unable to find %s: Class %s not found",
                    description,
                    className), e);
        }
        if (!Modifier.isPublic(clazz.getModifiers())) {
            throw new HostTestException(String.format(
                    "Unable to find %s: Class %s must be public",
                    description,
                    className));
        }

        Method method = null;
        try {
            method = clazz.getMethod(methodName, argTypes);
        } catch (Exception e) {
            throw new HostTestException(String.format(
                    "Unable to find %s: class %s doesn't have method %s"
                    + " Method must be public static, and arg types must be: "
                    + Arrays.toString(argTypes),
                    description, className, methodName), e);
        }
        if (!(Modifier.isPublic(method.getModifiers())
                && Modifier.isStatic(method.getModifiers()))) {
            throw new HostTestException(String.format(
                    "Unable to find %s: Method %s in class %s must be public static",
                    description, methodName, className));
        }
        try {
            method.invoke(null, args);
        } catch (Exception e) {
            throw new HostTestException(String.format(
                    "Unable to invoke %s %s.%s",
                    description, className, methodName), e);
        }
    }

    /**
     * I can be used as
     * {@code --default-class-load-hook
     * com.android.hoststubgen.hosthelper.HostTestUtils.logClassLoaded}.
     *
     * It logs every loaded class.
     */
    public static void logClassLoaded(Class<?> clazz) {
        if (SKIP_CLASS_LOG) {
            return;
        }
        logPrintStream.println("# class loaded: " + clazz.getCanonicalName());
    }

    /**
     * Find any of the HostStubGenProcessedAsXxx annotations from a given element and
     * return its "reason".
+30 −33
Original line number Diff line number Diff line
@@ -36,13 +36,12 @@ import com.android.hoststubgen.filters.FilterPolicyWithReason
import com.android.hoststubgen.filters.OutputFilter
import com.android.hoststubgen.hosthelper.HostStubGenProcessedAsIgnore
import com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
import com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep.CLASS_DESCRIPTOR
import com.android.hoststubgen.hosthelper.HostStubGenProcessedAsSubstitute
import com.android.hoststubgen.hosthelper.HostStubGenProcessedAsThrow
import com.android.hoststubgen.hosthelper.HostStubGenProcessedAsThrowButSupported
import com.android.hoststubgen.hosthelper.HostTestUtils
import com.android.hoststubgen.log
import com.android.hoststubgen.utils.ClassDescriptorSet
import java.lang.annotation.RetentionPolicy
import org.objectweb.asm.AnnotationVisitor
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.FieldVisitor
@@ -53,7 +52,6 @@ import org.objectweb.asm.Opcodes.INVOKESPECIAL
import org.objectweb.asm.Opcodes.INVOKESTATIC
import org.objectweb.asm.Opcodes.INVOKEVIRTUAL
import org.objectweb.asm.Type
import java.lang.annotation.RetentionPolicy

const val OPCODE_VERSION = Opcodes.ASM9

@@ -138,7 +136,8 @@ class ImplGeneratingAdapter(
        log.v("Emitting class: %s", name)
        log.indent()
        // Inject annotations to generated classes.
        UnifiedVisitor.on(this).visitAnnotation(CLASS_DESCRIPTOR, true, reasonParam(classPolicy.reason))
        UnifiedVisitor.on(this).visitAnnotation(
            HostStubGenProcessedAsKeep.CLASS_DESCRIPTOR, true, reasonParam(classPolicy.reason))

        classLoadHooks = filter.getClassLoadHooks(currentClassName)

@@ -201,6 +200,19 @@ class ImplGeneratingAdapter(
        }
    }

    /**
     * Split a class + method string into internal class name + method name
     * e.g.: "com.example.MyClass.myMethod" -> ("com/example/MyClass", "myMethod")
     */
    private fun String.parseClassMethod(): Pair<String, String> {
        val split = lastIndexOf('.')
        if (split < 0) {
            options.errors.onErrorFound(
                "Unable to find class and method name: malformed string \"%s\"".format(this))
        }
        return substring(0, split).replace('.', '/') to substring(split + 1)
    }

    private fun injectClassLoadHook() {
        writeRawMembers {
            // Create a class initializer to call onClassLoaded().
@@ -231,17 +243,9 @@ class ImplGeneratingAdapter(
            // First argument: the class type.
            mv.visitLdcInsn(Type.getType("L$currentClassName;"))

            // Second argument: method name
            mv.visitLdcInsn(classLoadHook)

            // Call HostTestUtils.onClassLoaded().
            mv.visitMethodInsn(
                INVOKESTATIC,
                HostTestUtils.CLASS_INTERNAL_NAME,
                "onClassLoaded",
                "(Ljava/lang/Class;Ljava/lang/String;)V",
                false
            )
            // Call classLoadHook
            val (clazz, method) = classLoadHook.parseClassMethod()
            mv.visitMethodInsn(INVOKESTATIC, clazz, method, "(Ljava/lang/Class;)V", false)
        }
    }

@@ -357,11 +361,6 @@ class ImplGeneratingAdapter(
                super.visitMethod(newAccess, newName, descriptor, signature, exceptions)
            )

            ret?.let {
                UnifiedVisitor.on(ret)
                    .visitAnnotation(HostStubGenProcessedAsKeep.CLASS_DESCRIPTOR, true, reasonParam(p.reason))
            }

            return ForceMethodAnnotationVisibilityVisitor(ret)
        }
    }
@@ -431,20 +430,21 @@ class ImplGeneratingAdapter(
                    return RedirectMethodAdapter(
                        access, name, descriptor,
                        forceCreateBody, innerVisitor
                    )
                        .withAnnotation(HostStubGenProcessedAsSubstitute.CLASS_DESCRIPTOR, policy.reason)
                    ).withAnnotation(HostStubGenProcessedAsSubstitute.CLASS_DESCRIPTOR, policy.reason)
                }
                else -> {
                    if (substituted) {
                        innerVisitor?.withAnnotation(HostStubGenProcessedAsSubstitute.CLASS_DESCRIPTOR, policy.reason)
                    } else {
                        innerVisitor?.withAnnotation(HostStubGenProcessedAsKeep.CLASS_DESCRIPTOR, policy.reason)
                    }
                }
                else -> {}
            }
        }

        if (filter.hasAnyMethodCallReplace()) {
            innerVisitor = MethodCallReplacingAdapter(name, innerVisitor)
        }
        if (substituted) {
            // We don't have a reason in this case.
            innerVisitor?.withAnnotation(HostStubGenProcessedAsSubstitute.CLASS_DESCRIPTOR, "")
        }

        return innerVisitor
    }
@@ -574,13 +574,10 @@ class ImplGeneratingAdapter(
                mv.visitLdcInsn(Type.getType("L$currentClassName;"))
                visitLdcInsn(name)
                visitLdcInsn(descriptor)
                visitLdcInsn(hook)

                visitMethodInsn(
                    INVOKESTATIC,
                    HostTestUtils.CLASS_INTERNAL_NAME,
                    "callMethodCallHook",
                    "(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V",
                val (clazz, method) = hook.parseClassMethod()
                visitMethodInsn(INVOKESTATIC, clazz, method,
                    "(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;)V",
                    false
                )
            }
+2 −32
Original line number Diff line number Diff line
@@ -97,24 +97,6 @@ java_genrule_host {
    visibility: ["//visibility:private"],
}

// Same as "hoststubgen-test-tiny-framework-host", but with more options, to test more hoststubgen
// features.
java_genrule_host {
    name: "hoststubgen-test-tiny-framework-host-ext-base",
    defaults: ["tf_hsg_base_with_out-defaults"],
    cmd: "$(location hoststubgen) " + tf_hsg_base_options_with_out +
    "--default-method-call-hook com.android.hoststubgen.hosthelper.HostTestUtils.logMethodCall " +
    "--default-class-load-hook com.android.hoststubgen.hosthelper.HostTestUtils.logClassLoaded ",
}

java_genrule_host {
    name: "hoststubgen-test-tiny-framework-host-ext",
    cmd: "cp $(in) $(out)",
    srcs: [":hoststubgen-test-tiny-framework-host-ext-base{host.jar}"],
    out: ["host.jar"],
    visibility: ["//visibility:private"],
}

// Compile the test jar, using 2 rules.
// 1. Build the test against the original framework.
java_library_host {
@@ -163,7 +145,7 @@ java_genrule_host {
        ":hoststubgen-test-tiny-framework",
    ],
    out: [
        "01-hoststubgen-test-tiny-framework-orig-dump.txt",
        "hoststubgen-test-tiny-framework-orig-dump.txt",
    ],
}

@@ -174,18 +156,7 @@ java_genrule_host {
        ":hoststubgen-test-tiny-framework-host",
    ],
    out: [
        "03-hoststubgen-test-tiny-framework-host-dump.txt",
    ],
}

java_genrule_host {
    name: "hoststubgen-test-tiny-framework-host-ext-dump",
    defaults: ["hoststubgen-jar-dump-defaults"],
    srcs: [
        ":hoststubgen-test-tiny-framework-host-ext",
    ],
    out: [
        "13-hoststubgen-test-tiny-framework-host-ext-dump.txt",
        "hoststubgen-test-tiny-framework-host-dump.txt",
    ],
}

@@ -266,7 +237,6 @@ python_test_host {
    java_data: [
        "hoststubgen-test-tiny-framework-orig-dump",
        "hoststubgen-test-tiny-framework-host-dump",
        "hoststubgen-test-tiny-framework-host-ext-dump",
        "hoststubgen-test-tiny-framework-stats-flatten",
    ],
    test_suites: ["general-tests"],
+0 −4866

File deleted.

Preview size limit exceeded, changes collapsed.

Loading