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 run() { + def results = [] + def one = { int a -> a * 2 } // arity-1 table + def two = { int a, int b -> a + b } // arity-2 table + def three = { int a, int b, int c -> a + b + c } // array table + results << one(21).toString() + results << two(20, 22).toString() + results << three(10, 14, 18).toString() + results << [1, 2, 3].collect { it + 1 }.toString() // through the GDK + results + } + static void boom() { + def thrower = { throw new java.io.IOException('checked, undeclared') } + thrower() + } + } + ''' + + private static List runPacked(boolean forceHandles) { + withProperty(CompilerConfiguration.CLOSURE_PACKING, 'true') { + withProperty(FORCE, forceHandles ? 'true' : null) { + def loader = new GroovyClassLoader() + def host = loader.parseClass(SRC, 'Host.groovy') + assert host.declaredMethods.any { it.name == '$packedDispatch$' } : 'packing did not engage' + host.run() + } + } + } + + private static T withProperty(String name, String value, Closure work) { + String previous = System.getProperty(name) + if (value != null) System.setProperty(name, value) else System.clearProperty(name) + try { + work.call() + } finally { + if (previous != null) System.setProperty(name, previous) else System.clearProperty(name) + } + } + + @Test + void 'handle bundles produce the same results as hidden-class bundles'() { + def viaHiddenClasses = runPacked(false) + def viaHandles = runPacked(true) + assertEquals(viaHiddenClasses, viaHandles) + assertEquals(['42', '42', '42', '[2, 3, 4]'], viaHandles) + } + + @Test + void 'undeclared checked exceptions propagate unchanged through handle bundles'() { + withProperty(CompilerConfiguration.CLOSURE_PACKING, 'true') { + withProperty(FORCE, 'true') { + def loader = new GroovyClassLoader() + def host = loader.parseClass(SRC, 'Host.groovy') + def thrown = assertThrows(IOException) { host.boom() } + assertEquals('checked, undeclared', thrown.message) + } + } + } +}