Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
113 changes: 109 additions & 4 deletions src/main/java/org/codehaus/groovy/runtime/GeneratedDispatcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -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}.
Expand Down Expand Up @@ -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 <em>constant bootstrap
* arguments</em> ({@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}.
* <p>
* 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");
}
});
}
}
14 changes: 14 additions & 0 deletions src/main/java/org/codehaus/groovy/vmplugin/v8/IndyInterface.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> 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<String> 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> T withProperty(String name, String value, Closure<T> 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)
}
}
}
}
Loading