diff --git a/src/main/java/org/codehaus/groovy/classgen/asm/ClosureWriter.java b/src/main/java/org/codehaus/groovy/classgen/asm/ClosureWriter.java index 4462f7aad5d..efc05550a86 100644 --- a/src/main/java/org/codehaus/groovy/classgen/asm/ClosureWriter.java +++ b/src/main/java/org/codehaus/groovy/classgen/asm/ClosureWriter.java @@ -1087,11 +1087,18 @@ public void writePackedDispatcher() { // and every later call returns the constant bundle, so the accessor is also the cache MethodVisitor mv = cv.visitMethod(ACC_PRIVATE | ACC_STATIC | ACC_SYNTHETIC, DISPATCHERS_GETTER, DISPATCHERS_GETTER_DESC, null, null); mv.visitCode(); + // The tables travel as constant bootstrap arguments (CONSTANT_MethodHandle), so the + // bootstrap needs no runtime Lookup.findStatic — which, under GraalVM native image, + // would demand per-class reflection metadata (GROOVY-12227). Handle bootstrap = new Handle( H_INVOKESTATIC, INDY_INTERFACE_TYPE, "packedDispatchers", - "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;", + "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;" + + "Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodHandle;)Ljava/lang/invoke/CallSite;", false); - mv.visitInvokeDynamicInsn("packedDispatchers", DISPATCHERS_GETTER_DESC, bootstrap); + mv.visitInvokeDynamicInsn("packedDispatchers", DISPATCHERS_GETTER_DESC, bootstrap, + new Handle(H_INVOKESTATIC, internal, DISPATCH_METHOD, DISPATCH_DESC, false), + new Handle(H_INVOKESTATIC, internal, DISPATCH1_METHOD, DISPATCH1_DESC, false), + new Handle(H_INVOKESTATIC, internal, DISPATCH2_METHOD, DISPATCH2_DESC, false)); mv.visitInsn(ARETURN); mv.visitMaxs(0, 0); mv.visitEnd(); diff --git a/src/main/java/org/codehaus/groovy/runtime/GeneratedDispatcher.java b/src/main/java/org/codehaus/groovy/runtime/GeneratedDispatcher.java index 779aefec635..c8adc632447 100644 --- a/src/main/java/org/codehaus/groovy/runtime/GeneratedDispatcher.java +++ b/src/main/java/org/codehaus/groovy/runtime/GeneratedDispatcher.java @@ -21,9 +21,13 @@ import java.lang.invoke.CallSite; import java.lang.invoke.ConstantCallSite; import java.lang.invoke.LambdaMetafactory; +import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; +import org.apache.groovy.internal.util.UncheckedThrow; +import org.apache.groovy.util.SystemUtil; + /** * A per-class table of compiler-generated dispatch targets, reached by a compact * integer id instead of a {@link java.lang.invoke.MethodHandle}. @@ -171,15 +175,116 @@ static CallSite bootstrap(final MethodHandles.Lookup caller, final String name, MethodType arrayType = MethodType.methodType(Object.class, int.class, Object[].class); MethodType oneType = MethodType.methodType(Object.class, int.class, Object.class, Object.class); MethodType twoType = MethodType.methodType(Object.class, int.class, Object.class, Object.class, Object.class); + return bootstrap(caller, name, type, + caller.findStatic(host, TABLE_METHOD, arrayType), + caller.findStatic(host, TABLE1_METHOD, oneType), + caller.findStatic(host, TABLE2_METHOD, twoType)); + } + + /** + * Preferred bootstrap overload: the three dispatch tables arrive as constant bootstrap + * arguments ({@code CONSTANT_MethodHandle} entries resolved by the VM's constant pool + * machinery), so linking needs no {@code Lookup.findStatic} — which, under GraalVM native + * image, would require per-class reflection metadata. Emitted bytecode reaches this through + * {@code org.codehaus.groovy.vmplugin.v8.IndyInterface#packedDispatchers}. + *
+ * The bundle's dispatch shapes are adapted from the tables one of two ways. On a regular JVM,
+ * {@code LambdaMetafactory} spins one hidden class per shape, whose interface call inlines
+ * under the JIT (see the class javadoc for why that matters). Under a runtime that cannot
+ * define classes — a GraalVM native image, detected per call so a build-time-initialized
+ * class cannot bake in the wrong answer — the tables are wrapped in method-handle-invoking
+ * adapters instead: ahead-of-time-compiled lambdas of this class, so no class definition
+ * happens at run time. There is no JIT in such runtimes, so the inlining rationale for the
+ * hidden-class path does not apply. The wrapper path can be forced on a regular JVM with
+ * {@code -Dgroovy.packed.dispatch.handles=true} (a diagnostic knob, for testing parity).
+ *
+ * @param caller the hosting class's lookup (supplied by the JVM)
+ * @param name the invoked name (unused)
+ * @param type the accessor's type (see the three-argument overload)
+ * @param table the array-shaped dispatch table, {@code (int, Object[]) -> Object}
+ * @param table1 the one-value table, {@code (int, Object, Object) -> Object}
+ * @param table2 the two-value table, {@code (int, Object, Object, Object) -> Object}
+ * @return a constant call site producing the bundle
+ * @throws Throwable if the tables cannot be linked (a compiler bug)
+ */
+ static CallSite bootstrap(final MethodHandles.Lookup caller, final String name, final MethodType type,
+ final MethodHandle table, final MethodHandle table1, final MethodHandle table2) throws Throwable {
+ Bundle bundle;
+ if (handleBundlesRequested()) {
+ bundle = handleBundle(table, table1, table2);
+ } else {
+ try {
+ bundle = hiddenClassBundle(caller, table, table1, table2);
+ } catch (Throwable cannotDefineClasses) {
+ // belt and braces for AOT runtimes not caught by the property probe:
+ // hidden-class definition is the only fallible step past a valid compile
+ bundle = handleBundle(table, table1, table2);
+ }
+ }
+ return new ConstantCallSite(MethodHandles.constant(type.returnType(), bundle));
+ }
+
+ /**
+ * Whether to skip hidden-class adapters in favour of method-handle wrappers. Evaluated per
+ * link (not cached in a static): under native image this class may be initialized at build
+ * time, where {@code org.graalvm.nativeimage.imagecode} reports {@code buildtime} — caching
+ * would bake the wrong answer into the image heap.
+ */
+ private static boolean handleBundlesRequested() {
+ return "runtime".equals(System.getProperty("org.graalvm.nativeimage.imagecode"))
+ || SystemUtil.getBooleanSafe("groovy.packed.dispatch.handles");
+ }
+
+ /** The JIT-friendly path: one hidden class per dispatch shape, via {@code LambdaMetafactory}. */
+ private static Bundle hiddenClassBundle(final MethodHandles.Lookup caller,
+ final MethodHandle table, final MethodHandle table1, final MethodHandle table2) throws Throwable {
+ MethodType arrayType = MethodType.methodType(Object.class, int.class, Object[].class);
+ MethodType oneType = MethodType.methodType(Object.class, int.class, Object.class, Object.class);
+ MethodType twoType = MethodType.methodType(Object.class, int.class, Object.class, Object.class, Object.class);
GeneratedDispatcher dispatcher = (GeneratedDispatcher) LambdaMetafactory.metafactory(
caller, "dispatch", MethodType.methodType(GeneratedDispatcher.class),
- arrayType, caller.findStatic(host, TABLE_METHOD, arrayType), arrayType).getTarget().invokeExact();
+ arrayType, table, arrayType).getTarget().invokeExact();
Arity1 arity1 = (Arity1) LambdaMetafactory.metafactory(
caller, "dispatch1", MethodType.methodType(Arity1.class),
- oneType, caller.findStatic(host, TABLE1_METHOD, oneType), oneType).getTarget().invokeExact();
+ oneType, table1, oneType).getTarget().invokeExact();
Arity2 arity2 = (Arity2) LambdaMetafactory.metafactory(
caller, "dispatch2", MethodType.methodType(Arity2.class),
- twoType, caller.findStatic(host, TABLE2_METHOD, twoType), twoType).getTarget().invokeExact();
- return new ConstantCallSite(MethodHandles.constant(type.returnType(), new Bundle(dispatcher, arity1, arity2)));
+ twoType, table2, twoType).getTarget().invokeExact();
+ return new Bundle(dispatcher, arity1, arity2);
+ }
+
+ /**
+ * The class-definition-free path: each dispatch shape invokes its table through the exact
+ * method handle. The lambdas below are ordinary bytecode of this class — under native image
+ * they are pre-processed at build time, so no class is defined at run time. Targets may
+ * throw checked exceptions the interfaces do not declare (the hidden-class path propagates
+ * them transparently), so parity requires the unchecked rethrow.
+ */
+ private static Bundle handleBundle(final MethodHandle table, final MethodHandle table1, final MethodHandle table2) {
+ return new Bundle(
+ (id, args) -> {
+ try {
+ return table.invokeExact(id, args);
+ } catch (Throwable t) {
+ UncheckedThrow.rethrow(t);
+ throw new AssertionError("unreachable");
+ }
+ },
+ (id, owner, a) -> {
+ try {
+ return table1.invokeExact(id, owner, a);
+ } catch (Throwable t) {
+ UncheckedThrow.rethrow(t);
+ throw new AssertionError("unreachable");
+ }
+ },
+ (id, owner, a, b) -> {
+ try {
+ return table2.invokeExact(id, owner, a, b);
+ } catch (Throwable t) {
+ UncheckedThrow.rethrow(t);
+ throw new AssertionError("unreachable");
+ }
+ });
}
}
diff --git a/src/main/java/org/codehaus/groovy/vmplugin/v8/IndyInterface.java b/src/main/java/org/codehaus/groovy/vmplugin/v8/IndyInterface.java
index 05cd8761505..0cb2299b777 100644
--- a/src/main/java/org/codehaus/groovy/vmplugin/v8/IndyInterface.java
+++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/IndyInterface.java
@@ -651,6 +651,20 @@ public static CallSite packedDispatchers(MethodHandles.Lookup caller, String nam
return GeneratedDispatcher.bootstrap(caller, name, type);
}
+ /**
+ * Preferred overload of {@link #packedDispatchers(MethodHandles.Lookup, String, MethodType)}:
+ * the dispatch tables arrive as constant bootstrap arguments, resolved by the VM's constant
+ * pool machinery rather than a runtime {@code Lookup.findStatic} — which lets the linkage
+ * work under GraalVM native image without per-class reflection metadata (GROOVY-12227).
+ * The three-argument form remains for class files emitted by earlier 6.0 snapshots.
+ *
+ * @since 6.0.0
+ */
+ public static CallSite packedDispatchers(MethodHandles.Lookup caller, String name, MethodType type,
+ MethodHandle table, MethodHandle table1, MethodHandle table2) throws Throwable {
+ return GeneratedDispatcher.bootstrap(caller, name, type, table, table1, table2);
+ }
+
/**
* Constant-dynamic bootstrap for a packed closure literal's declared parameter types
* (GROOVY-12151): decodes a method descriptor into a {@code Class[]} resolved once per
diff --git a/src/test/groovy/org/codehaus/groovy/runtime/PackedDispatcherHandleBundleTest.groovy b/src/test/groovy/org/codehaus/groovy/runtime/PackedDispatcherHandleBundleTest.groovy
new file mode 100644
index 00000000000..c2639f3e211
--- /dev/null
+++ b/src/test/groovy/org/codehaus/groovy/runtime/PackedDispatcherHandleBundleTest.groovy
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.codehaus.groovy.runtime
+
+import org.codehaus.groovy.control.CompilerConfiguration
+import org.junit.jupiter.api.Test
+
+import static org.junit.jupiter.api.Assertions.assertEquals
+import static org.junit.jupiter.api.Assertions.assertThrows
+
+/**
+ * Parity between the two ways {@link GeneratedDispatcher#bootstrap} adapts a class's dispatch
+ * tables: the default {@code LambdaMetafactory} hidden classes, and the method-handle wrappers
+ * used where classes cannot be defined at run time (GraalVM native image). The wrapper path is
+ * forced on a regular JVM with {@code -Dgroovy.packed.dispatch.handles=true}; linkage happens
+ * once per loaded class, so each compilation below (a fresh class in a fresh loader) observes
+ * the property's value at its own first dispatch.
+ */
+final class PackedDispatcherHandleBundleTest {
+
+ private static final String FORCE = 'groovy.packed.dispatch.handles'
+
+ /** Exercises every dispatch shape: array (3 values), arity-1, arity-2, and a checked throw. */
+ private static final String SRC = '''
+ class Host {
+ static List