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

Commit 8634de38 authored by John Wu's avatar John Wu Committed by Android (Google) Code Review
Browse files

Merge changes I69036be5,Ie09762c7 into main

* changes:
  [HostStubGen] Support experimental APIs
  [HostStubGen] Improve class load and method call hooks
parents 11494baa 039691a4
Loading
Loading
Loading
Loading
+35 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.hoststubgen.hosthelper;

import static java.lang.annotation.ElementType.METHOD;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Annotation injected to all methods that are processed as "experimental".
 */
@Target({METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface HostStubGenProcessedAsExperimental {
    String CLASS_INTERNAL_NAME = HostTestUtils.getInternalName(
            HostStubGenProcessedAsExperimental.class);
    String CLASS_DESCRIPTOR = "L" + CLASS_INTERNAL_NAME + ";";

    String reason() default "";
}
+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".
+2 −1
Original line number Diff line number Diff line
@@ -74,7 +74,8 @@ class HostStubGenClassProcessor(
            deleteClassFinals = options.deleteFinals.get,
            deleteMethodFinals = options.deleteFinals.get,
            throwExceptionType = options.throwExceptionType.get,
            annotationsToMakeVisible = options.allAnnotationSet
            annotationsToMakeVisible = options.allAnnotationSet,
            experimentalMethodCallHook = options.experimentalMethodCallHook.get,
        )

        val verbosePrinter = PrintWriter(log.getWriter(LogLevel.Verbose))
+5 −0
Original line number Diff line number Diff line
@@ -55,6 +55,7 @@ open class HostStubGenClassProcessorOptions(

    val defaultClassLoadHook: SetOnce<String?> = SetOnce(null),
    val defaultMethodCallHook: SetOnce<String?> = SetOnce(null),
    val experimentalMethodCallHook: SetOnce<String?> = SetOnce(null),

    val policyOverrideFiles: MutableList<FileOrResource> = mutableListOf(),

@@ -146,6 +147,9 @@ open class HostStubGenClassProcessorOptions(
            "--default-method-call-hook" ->
                defaultMethodCallHook.set(nextArg())

            "--experimental-method-call-hook" ->
                experimentalMethodCallHook.set(nextArg())

            "--delete-finals" -> deleteFinals.set(true)

            "--throw-exception" -> throwExceptionType.set(nextArg())
@@ -185,6 +189,7 @@ open class HostStubGenClassProcessorOptions(
            annotationAllowedClassesFile=$annotationAllowedClassesFile,
            defaultClassLoadHook=$defaultClassLoadHook,
            defaultMethodCallHook=$defaultMethodCallHook,
            experimentalMethodCallHook=$experimentalMethodCallHook,
            policyOverrideFiles=${policyOverrideFiles.toTypedArray().contentToString()},
            defaultPolicy=$defaultPolicy,
            deleteFinals=$deleteFinals,
Loading