Skip to content

GROOVY-12227: GeneratedDispatcher: avoid runtime class definition so packed closures work in native images - #2765

Merged
paulk-asert merged 1 commit into
apache:masterfrom
paulk-asert:packed-dispatcher-lmf-in-host
Aug 10, 2026
Merged

GROOVY-12227: GeneratedDispatcher: avoid runtime class definition so packed closures work in native images#2765
paulk-asert merged 1 commit into
apache:masterfrom
paulk-asert:packed-dispatcher-lmf-in-host

Conversation

@paulk-asert

@paulk-asert paulk-asert commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Two layered changes to the GROOVY-12151 packed-closure machinery (GEP-27):

  1. ClosureWriter now emits into each packed hosting class a private
    static $packedDispatchersFactory$ whose body adapts the class's three
    private static dispatch tables to their functional interfaces through
    ordinary bytecode-level LambdaMetafactory invokedynamic sites and
    returns them as one Bundle. The dispatcher accessor's bootstrap becomes
    the new four-argument IndyInterface.packedDispatchers, which receives
    that factory as a CONSTANT_MethodHandle bootstrap argument.

  2. GeneratedDispatcher.bootstrap (four-argument form) just invokes the
    factory into a ConstantCallSite: linking needs neither a runtime
    Lookup.findStatic nor a programmatic LambdaMetafactory call — an
    undeclared reflective lookup and a run-time class definition, the two
    operations ahead-of-time runtimes restrict, so the linkage suits any
    such environment. Under GraalVM native image, the verified case, the
    factory's sites are pre-processed at image build time and its method
    references reach the class's own private tables without reflection
    metadata.

On a regular JVM the linkage is equivalent: the VM spins the same three
hidden classes when it links the factory's sites, with the cost moving
from bootstrap-time programmatic LMF to first-invocation indy linkage
(per packed class: one extra synthetic method, three indy sites, one
bootstrap argument; the accessor shape is unchanged). The Bundle
constructor is publicized because the compiler-emitted factory in the
hosting class's own package constructs it directly.

The previous three-argument bootstrap is retained verbatim for class
files emitted by earlier 6.0 pre-releases, which keep linking through
findStatic plus programmatic LambdaMetafactory. Conversely, class files
in the new format need this runtime to link; both directions only
concern unreleased 6.0 snapshots.

Verified on GraalVM 25.2.4 (native-image 25.0.4): the packed repro that
previously failed with 'Classes cannot be defined at runtime ...
M$$Lambda...' now runs correctly (single emitted class, 30MB image,
~12ms total run time), and the tracing agent records zero
packedDispatch entries for the new bytecode.

PackedDispatcherFactoryTest covers every dispatch shape and the
propagation of undeclared checked exceptions through both class-file
formats -- the legacy format recreated by downgrading a freshly
compiled accessor's bootstrap reference from the four-argument to the
three-argument form, the only difference between the two formats. The
test serializes its system-property mutation against other
property-touching tests with @ResourceLock(SYSTEM_PROPERTIES).

@codecov-commenter

codecov-commenter commented Aug 4, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 70.0025%. Comparing base (32d6a70) to head (6fcc0a8).
⚠️ Report is 2 commits behind head on master.

Additional details and impacted files

Impacted file tree graph

@@                Coverage Diff                 @@
##               master      #2765        +/-   ##
==================================================
+ Coverage     69.9972%   70.0025%   +0.0053%     
- Complexity      35557      35560         +3     
==================================================
  Files            1558       1558                
  Lines          131731     131751        +20     
  Branches        24178      24178                
==================================================
+ Hits            92208      92229        +21     
  Misses          31176      31176                
+ Partials         8347       8346         -1     
Files with missing lines Coverage Δ
...rg/codehaus/groovy/classgen/asm/ClosureWriter.java 90.0404% <100.0000%> (+0.2473%) ⬆️
...g/codehaus/groovy/runtime/GeneratedDispatcher.java 87.5000% <100.0000%> (+0.5435%) ⬆️
...org/codehaus/groovy/vmplugin/v8/IndyInterface.java 84.0659% <100.0000%> (+0.0880%) ⬆️

... and 7 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@testlens-app

This comment has been minimized.

@paulk-asert
paulk-asert force-pushed the packed-dispatcher-lmf-in-host branch from b5b5692 to 0c7e5e7 Compare August 10, 2026 07:04
@paulk-asert paulk-asert changed the title GROOVY-12227: GeneratedDispatcher: avoid runtime class definition so … GROOVY-12227: GeneratedDispatcher: avoid runtime class definition so packed closures work in native images Aug 10, 2026
packed closures work in native images

Two layered changes to the GROOVY-12151 packed-closure machinery (GEP-27):

1. ClosureWriter now emits into each packed hosting class a private
static $packedDispatchersFactory$ whose body adapts the class's three
private static dispatch tables to their functional interfaces through
ordinary bytecode-level LambdaMetafactory invokedynamic sites and
returns them as one Bundle. The dispatcher accessor's bootstrap becomes
the new four-argument IndyInterface.packedDispatchers, which receives
that factory as a CONSTANT_MethodHandle bootstrap argument.

2. GeneratedDispatcher.bootstrap (four-argument form) just invokes the
factory into a ConstantCallSite: linking needs neither a runtime
Lookup.findStatic nor a programmatic LambdaMetafactory call — an
undeclared reflective lookup and a run-time class definition, the two
operations ahead-of-time runtimes restrict, so the linkage suits any
such environment. Under GraalVM native image, the verified case, the
factory's sites are pre-processed at image build time and its method
references reach the class's own private tables without reflection
metadata.

On a regular JVM the linkage is equivalent: the VM spins the same three
hidden classes when it links the factory's sites, with the cost moving
from bootstrap-time programmatic LMF to first-invocation indy linkage
(per packed class: one extra synthetic method, three indy sites, one
bootstrap argument; the accessor shape is unchanged). The Bundle
constructor is publicized because the compiler-emitted factory in the
hosting class's own package constructs it directly.

The previous three-argument bootstrap is retained verbatim for class
files emitted by earlier 6.0 pre-releases, which keep linking through
findStatic plus programmatic LambdaMetafactory. Conversely, class files
in the new format need this runtime to link; both directions only
concern unreleased 6.0 snapshots.

Verified on GraalVM 25.2.4 (native-image 25.0.4): the packed repro that
previously failed with 'Classes cannot be defined at runtime ...
M$$Lambda...' now runs correctly (single emitted class, 30MB image,
~12ms total run time), and the tracing agent records zero
packedDispatch entries for the new bytecode.

PackedDispatcherFactoryTest covers every dispatch shape and the
propagation of undeclared checked exceptions through both class-file
formats -- the legacy format recreated by downgrading a freshly
compiled accessor's bootstrap reference from the four-argument to the
three-argument form, the only difference between the two formats. The
test serializes its system-property mutation against other
property-touching tests with @ResourceLock(SYSTEM_PROPERTIES).
@paulk-asert
paulk-asert force-pushed the packed-dispatcher-lmf-in-host branch from 0c7e5e7 to 6fcc0a8 Compare August 10, 2026 07:07
@paulk-asert
paulk-asert merged commit 8c02dbd into apache:master Aug 10, 2026
30 checks passed
@paulk-asert
paulk-asert deleted the packed-dispatcher-lmf-in-host branch August 10, 2026 07:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants