You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by su...@apache.org on 2026/09/07 22:24:57 UTC

(groovy) branch master updated: GROOVY-12364: Indy: dispatch caller-sensitive targets reflectively on AOT-linked sites

This is an automated email from the ASF dual-hosted git repository.

daniellansun pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git


The following commit(s) were added to refs/heads/master by this push:
     new fe0f8c2232 GROOVY-12364: Indy: dispatch caller-sensitive targets reflectively on AOT-linked sites
fe0f8c2232 is described below

commit fe0f8c2232e9e0b0d1453559840bb51070a430ab
Author: Paul King <pa...@asert.com.au>
AuthorDate: Sun Sep 6 13:18:53 2026 +1000

    GROOVY-12364: Indy: dispatch caller-sensitive targets reflectively on AOT-linked sites
    
    Lookup.unreflect returns a caller-bound handle for a @CallerSensitive
    method, which is exactly right on HotSpot but cannot be executed by
    GraalVM's MethodHandle interpreter: a native image aborts with the
    VM-fatal "Cannot invoke method that has a @CallerSensitiveAdapter
    without an explicit caller" as soon as dynamic code calls e.g.
    Logger.getLogger or MethodHandles.lookup. Reflection handles such
    methods fine there, so an AOT-linked site now builds the target handle
    around Method.invoke instead, shaped like the unreflected handle so the
    guarded chain is unchanged. The observed caller then becomes Groovy's
    runtime rather than the calling class, which is harmless for logger and
    bundle lookups in a single-module image; MethodHandles.lookup() should
    be static-compiled in native code.
---
 .../v8/ColdReflectiveMethodHandleWrapper.java      |  2 +-
 .../v8/IndyGuardsFiltersAndSignatures.java         | 23 +++++++++++++-
 .../org/codehaus/groovy/vmplugin/v8/Selector.java  | 36 ++++++++++++++++++++++
 .../groovy/runtime/indy/AotLinkModeTest.groovy     | 27 ++++++++++++++++
 4 files changed, 86 insertions(+), 2 deletions(-)

diff --git a/src/main/java/org/codehaus/groovy/vmplugin/v8/ColdReflectiveMethodHandleWrapper.java b/src/main/java/org/codehaus/groovy/vmplugin/v8/ColdReflectiveMethodHandleWrapper.java
index 71fd9dbd92..c53a46285d 100644
--- a/src/main/java/org/codehaus/groovy/vmplugin/v8/ColdReflectiveMethodHandleWrapper.java
+++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/ColdReflectiveMethodHandleWrapper.java
@@ -177,7 +177,7 @@ class ColdReflectiveMethodHandleWrapper extends MethodHandleWrapper {
      * (defaults) carry their own annotations, so they are probed directly.
      * When a probe cannot decide, the method is treated as sensitive.
      */
-    private static boolean isCallerSensitive(CachedMethod cm, Class<?> receiverClass) {
+    static boolean isCallerSensitive(CachedMethod cm, Class<?> receiverClass) {
         if (cm.isCallerSensitive()) return true;
         Method method = cm.getCachedMethod();
         if (Modifier.isAbstract(method.getModifiers())) {
diff --git a/src/main/java/org/codehaus/groovy/vmplugin/v8/IndyGuardsFiltersAndSignatures.java b/src/main/java/org/codehaus/groovy/vmplugin/v8/IndyGuardsFiltersAndSignatures.java
index 2641d01660..5b1ed962c9 100644
--- a/src/main/java/org/codehaus/groovy/vmplugin/v8/IndyGuardsFiltersAndSignatures.java
+++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/IndyGuardsFiltersAndSignatures.java
@@ -27,6 +27,7 @@ import groovy.lang.MetaObjectProtocol;
 import groovy.lang.MetaProperty;
 import groovy.lang.MissingMethodException;
 import org.codehaus.groovy.GroovyBugError;
+import org.codehaus.groovy.reflection.CachedMethod;
 import org.codehaus.groovy.reflection.stdclasses.CachedSAMClass;
 import org.codehaus.groovy.runtime.GroovyCategorySupport;
 import org.codehaus.groovy.runtime.InvokerHelper;
@@ -73,7 +74,7 @@ public class IndyGuardsFiltersAndSignatures {
             SLOW_META_CLASS_FIND,
             MOP_GET, MOP_SET, SET_PROPERTY, MOP_INVOKE_CONSTRUCTOR, MOP_INVOKE_METHOD,
             INTERCEPTABLE_INVOKER,
-            BOOLEAN_IDENTITY, CLASS_FOR_NAME,
+            BOOLEAN_IDENTITY, CLASS_FOR_NAME, INVOKE_REFLECTIVELY,
             DTT_CAST_TO_TYPE, SAM_CONVERSION,
             HASHSET_CONSTRUCTOR, ARRAYLIST_CONSTRUCTOR,
             GROOVY_CAST_EXCEPTION,
@@ -110,6 +111,7 @@ public class IndyGuardsFiltersAndSignatures {
 
             BOOLEAN_IDENTITY                     = MethodHandles.identity(Boolean.class);
             CLASS_FOR_NAME                       = LOOKUP.findStatic(Class.class, "forName", MethodType.methodType(Class.class, String.class, boolean.class, ClassLoader.class));
+            INVOKE_REFLECTIVELY                  = LOOKUP.findStatic(IndyGuardsFiltersAndSignatures.class, "invokeReflectively", MethodType.methodType(Object.class, CachedMethod.class, Object.class, Object[].class));
             DTT_CAST_TO_TYPE                     = LOOKUP.findStatic(DefaultTypeTransformation.class, "castToType", MethodType.methodType(Object.class, Object.class, Class.class));
             SAM_CONVERSION                       = LOOKUP.findStatic(CachedSAMClass.class, "coerceToSAM", MethodType.methodType(Object.class, Closure.class, Method.class, Class.class));
             HASHSET_CONSTRUCTOR                  = LOOKUP.findConstructor(HashSet.class, MethodType.methodType(void.class, Collection.class));
@@ -209,6 +211,25 @@ public class IndyGuardsFiltersAndSignatures {
         return w.unwrap();
     }
 
+    /**
+     * Reflective leaf for a caller-sensitive target on an AOT-linked site
+     * (GROOVY-12364): classic {@code doMethodInvoke} semantics with the same
+     * {@code GroovyRuntimeException} unwrapping as the reflective cold tier.
+     *
+     * @param method the selected method
+     * @param receiver the receiver, ignored (may be {@code null}) for static methods
+     * @param arguments the call arguments
+     * @return the method's result
+     * @throws Throwable the target's exception, unwrapped
+     */
+    public static Object invokeReflectively(CachedMethod method, Object receiver, Object[] arguments) throws Throwable {
+        try {
+            return method.doMethodInvoke(receiver, arguments);
+        } catch (GroovyRuntimeException gre) {
+            throw ScriptBytecodeAdapter.unwrap(gre);
+        }
+    }
+
     /**
      * Guard to check if the argument is null.
      * This method is called by the handle to check
diff --git a/src/main/java/org/codehaus/groovy/vmplugin/v8/Selector.java b/src/main/java/org/codehaus/groovy/vmplugin/v8/Selector.java
index a0372a362f..80cc62761a 100644
--- a/src/main/java/org/codehaus/groovy/vmplugin/v8/Selector.java
+++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/Selector.java
@@ -76,6 +76,7 @@ import java.util.function.Predicate;
 import static org.codehaus.groovy.vmplugin.v8.IndyGuardsFiltersAndSignatures.ARRAYLIST_CONSTRUCTOR;
 import static org.codehaus.groovy.vmplugin.v8.IndyGuardsFiltersAndSignatures.BEAN_CONSTRUCTOR_PROPERTY_SETTER;
 import static org.codehaus.groovy.vmplugin.v8.IndyGuardsFiltersAndSignatures.CLASS_FOR_NAME;
+import static org.codehaus.groovy.vmplugin.v8.IndyGuardsFiltersAndSignatures.INVOKE_REFLECTIVELY;
 import static org.codehaus.groovy.vmplugin.v8.IndyGuardsFiltersAndSignatures.DTT_CAST_TO_TYPE;
 import static org.codehaus.groovy.vmplugin.v8.IndyGuardsFiltersAndSignatures.EQUALS;
 import static org.codehaus.groovy.vmplugin.v8.IndyGuardsFiltersAndSignatures.GROOVY_CAST_EXCEPTION;
@@ -1042,6 +1043,16 @@ public abstract class Selector {
                     }
                 } else if (parameterCount == 1 && "forName".equals(name) && declaringClass == Class.class) {
                     handle = MethodHandles.insertArguments(CLASS_FOR_NAME, 1, Boolean.TRUE, sender.getClassLoader());
+                } else if (callSite.isAotLinked()
+                        && ColdReflectiveMethodHandleWrapper.isCallerSensitive(cm, getCorrectedReceiver().getClass())) {
+                    // GROOVY-12364: a caller-bound handle from unreflect() is exactly right on
+                    // HotSpot, but GraalVM's MethodHandle interpreter cannot invoke it ("Cannot
+                    // invoke method that has a @CallerSensitiveAdapter without an explicit
+                    // caller", a VM-fatal error) because Groovy builds the handle at run time.
+                    // Reflection works there, so an AOT-linked site invokes such targets
+                    // through Method.invoke; the observed caller is then Groovy's runtime.
+                    handle = reflectiveHandle(cm);
+                    if (LOG_ENABLED) LOG.info("caller-sensitive target on an AOT-linked site dispatched reflectively: " + name);
                 } else {
                     handle = unreflect(cm.getCachedMethod());
                 }
@@ -1070,6 +1081,31 @@ public abstract class Selector {
             return false;
         }
 
+        /**
+         * Builds a handle with the target method's own type whose leaf is
+         * {@code Method.invoke} rather than the method itself (GROOVY-12364).
+         * Shaped like {@link #unreflect}'s result, including the varargs
+         * collector flag, so the rest of the chain is unaffected.
+         *
+         * @param cm the selected method
+         * @return a handle of the method's natural type dispatching reflectively
+         */
+        private static MethodHandle reflectiveHandle(CachedMethod cm) {
+            Method method = cm.getCachedMethod();
+            MethodType natural = MethodType.methodType(method.getReturnType(), method.getParameterTypes());
+            MethodHandle mh = MethodHandles.insertArguments(INVOKE_REFLECTIVELY, 0, cm); // (Object receiver, Object[] args)Object
+            if (Modifier.isStatic(method.getModifiers())) {
+                mh = MethodHandles.insertArguments(mh, 0, (Object) null);
+            } else {
+                natural = natural.insertParameterTypes(0, method.getDeclaringClass());
+            }
+            mh = mh.asCollector(Object[].class, method.getParameterCount()).asType(natural);
+            if (method.isVarArgs()) {
+                mh = mh.asVarargsCollector(method.getParameterTypes()[method.getParameterCount() - 1]);
+            }
+            return mh;
+        }
+
         /**
          * Unreflects a cached reflective method against the call-site lookup.
          *
diff --git a/src/test/groovy/org/apache/groovy/runtime/indy/AotLinkModeTest.groovy b/src/test/groovy/org/apache/groovy/runtime/indy/AotLinkModeTest.groovy
index a254f5a3ee..6124cb8a12 100644
--- a/src/test/groovy/org/apache/groovy/runtime/indy/AotLinkModeTest.groovy
+++ b/src/test/groovy/org/apache/groovy/runtime/indy/AotLinkModeTest.groovy
@@ -18,6 +18,7 @@
  */
 package org.apache.groovy.runtime.indy
 
+import org.codehaus.groovy.reflection.CachedMethod
 import org.codehaus.groovy.vmplugin.v8.CacheableCallSite
 import org.junit.jupiter.api.Test
 import org.junit.jupiter.api.parallel.ResourceLock
@@ -64,6 +65,32 @@ final class AotLinkModeTest {
         }
     }
 
+    /**
+     * GROOVY-12364: a caller-bound handle from {@code Lookup.unreflect} is what an
+     * ordinary site uses for a {@code @CallerSensitive} target, but GraalVM's
+     * MethodHandle interpreter aborts the process on it ("Cannot invoke method that
+     * has a @CallerSensitiveAdapter without an explicit caller"), while reflection
+     * works there. An AOT-linked site therefore dispatches such targets through
+     * {@code Method.invoke}. On HotSpot the route is observable through the caller
+     * the JDK reports: reflection attributes the call to Groovy's own
+     * {@code CachedMethod}, the handle path to the calling script.
+     */
+    @Test
+    void 'caller-sensitive targets dispatch reflectively on AOT-linked sites'() {
+        String script = 'java.lang.invoke.MethodHandles.lookup().lookupClass()'
+        def viaHandle = new GroovyShell().evaluate(script)
+        assert viaHandle != CachedMethod
+        assert viaHandle.name.startsWith('Script')
+
+        def viaReflection = evaluateAotLinked(script)
+        assert viaReflection == CachedMethod
+
+        // the everyday case: a static caller-sensitive JDK method with an argument
+        assert evaluateAotLinked("java.util.logging.Logger.getLogger('groovy12364').name") == 'groovy12364'
+        // instance caller-sensitive target with varargs, via the same route
+        assert evaluateAotLinked("String.getMethod('valueOf', int).name") == 'valueOf'
+    }
+
     /**
      * Every dispatch shape links and runs on AOT-linked sites. The hot loop runs past
      * {@code groovy.indy.fallback.threshold} and {@code groovy.indy.optimize.threshold}