Skip to content

[wasm] Use GetDelegateCtor for CoreCLR R2R delegate construction - #134611

Merged
lewing merged 17 commits into
mainfrom
lewing-wasm-r2r-delegate-lowering
Oct 1, 2026
Merged

lewing merged 17 commits into
mainfrom
lewing-wasm-r2r-delegate-lowering

Conversation

@lewing

@lewing lewing commented Sep 24, 2026 •

Copy link
Copy Markdown
Member

Summary

Browser-Wasm CoreCLR ReadyToRun skipped delegate-constructor lowering. Wasm can't use the dynamically composed READYTORUN_DELEGATE_CTOR helpers, and because ReadyToRun is IsAot(), it also skipped the GetDelegateCtor path the JIT uses. As a result, every new D(target, ftn) called the original runtime-implemented .ctor(object, native int) through the interpreter.

With this change, Wasm ReadyToRun uses the same GetDelegateCtor transformation as the JIT. Crossgen2 selects the CoreLib constructor:

Selected constructor Used for
Delegate.CtorClosed Closed instance targets on reference types, with the same explicit arity as Invoke (including virtual and generic targets)
Delegate.DelegateConstruct Every other shape
  • DelegateConstruct is the managed method the runtime already maps every delegate constructor to (ecall.cpp), so the fallback matches today's behavior for all shapes.
  • The CtorClosed shapes match those for which COMDelegate::GetDelegateCtor picks CtorClosed. The caller has already resolved the method pointer (ldftn/ldvirtftn, including instantiating stubs), so CtorClosed only stores the target and the pointer.
  • Generated code references the two CoreLib methods by name, the same way runtime async references AsyncHelpers methods such as RestoreInlinedFrameContexts. The JIT synthesizes these calls, so the caller's IL has no token for them. Crossgen2 therefore pre-seeds their manifest tokens, mirroring AddNecessaryAsyncReferences.

Changes

  • flowgraph.cpp:
    • On TARGET_WASM, the ReadyToRun branch of fgOptimizeDelegateConstructor is taken only for NativeAOT, so Wasm ReadyToRun falls through to GetDelegateCtor.
    • After retargeting the call, the AOT entry point is recomputed with getFunctionEntryPoint, because the importer cached the original constructor's entry point.
    • Non-Wasm targets and NativeAOT are unchanged.
  • Crossgen2:
    • GetDelegateCtor is implemented in the ReadyToRun partial; the RyuJit partial throws NotImplementedException, as before.
    • Manifest tokens for CtorClosed and DelegateConstruct are seeded lazily on Wasm.
  • No R2R format, JIT-EE interface, or VM changes. The image version stays 30.0.

Validation

Local runs on macOS arm64, branch merged with current main:

  • Builds: Crossgen2, the cross-targeting Wasm JIT, the osx JIT and SuperPMI; browser-Wasm clr+libs+host+packs Release.

  • Image tests: ILCompiler.ReadyToRun.Tests with the Wasm target: 123 passed, 0 failed. WasmDelegateConstructors asserts that no DelegateCtor fixups remain and that both CoreLib constructors are imported.

  • Runtime test: readytorun/wasm/WasmInterpreterTransitions passes in these configurations:

    Configuration Result
    Composite (harness default; CoreLib outside the bubble) Pass; also passes with DOTNET_GCStress=0xC
    Per-assembly, no cross-module inlining Pass; also passes with DOTNET_GCStress=0xC
    Per-assembly with --opt-cross-module:* Pass; CtorClosed is inlined under a CHECK_IL_BODY/VERIFY_IL_BODY fixup, and the inlined null-receiver throw calls the private CoreLib helper through a manifest reference
    Composite with --inputbubble Pass

    It covers the open static, open instance (via an IL helper), closed instance, closed static, virtual (including a derived override), generic owner, shared and unshared generic method, and null-receiver shapes, plus open- and closed-static retbuf shapes. Each is value-checked, with Target/Method identity and repeated construction.

  • Image dumps:

    • The composite and per-assembly images call both constructors through METHOD_ENTRY_REF_TOKEN manifest references.
    • CoreLib still contains compiled bodies for both constructors without any extra rooting.
  • Other: jitformat and git diff --check; independent code review with no findings.

Performance

100,000 constructions per sample; median of 3 runs × 4 samples, from the same build:

Shape Interpreter only R2R R2R + --opt-cross-module:*
Open static 9.83 ms 6.61 ms 6.55 ms
Closed instance 6.15 ms 2.50 ms 2.22 ms

Before this change, R2R construction took ~44–46 ms for both shapes in an earlier build (directional only). Open static might get faster later from a dedicated CtorOpen path, which would need the shuffle thunk and collectible data; that would be a follow-up.

Related work

Resolves #134564

Note

This pull request description was generated with GitHub Copilot.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 4dd9dfb0-13d5-4a8c-9419-3f808cbd9b8b
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 4dd9dfb0-13d5-4a8c-9419-3f808cbd9b8b
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 4dd9dfb0-13d5-4a8c-9419-3f808cbd9b8b
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 5 pipeline(s).
11 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/crossgen-contrib
See info in area-owners.md if you want to be subscribed.

@lewing lewing added the arch-wasm WebAssembly architecture label Sep 24, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to 'arch-wasm': @lewing, @pavelsavara
See info in area-owners.md if you want to be subscribed.

@lewing
lewing requested review from radekdoulik and a lite review from Copilot September 24, 2026 20:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Three moderate review issues remain unresolved.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 Medium severity

Open (1)
What changed in this PR

Enables browser-Wasm ReadyToRun delegate-constructor lowering with shared DC0/DC1 thunks and portable fixup metadata.

Changes:

  • Adds Wasm fixup handling, constructor rooting, and interpreter-transition dependencies.
  • Emits shared delegate-constructor thunks with GC argument tracking.
  • Adds runtime, IL, and ReadyToRun image coverage.

Review findings:

  • Moderate: Image tests do not verify the matching interpreter-thunk keys.
  • Moderate: The fallback condition excludes open-static delegates with hidden return buffers.
  • Moderate: The closed-static return-buffer test does not invoke the delegate.
File Description
src/​tests/​readytorun/​wasm/​WasmInterpreterTransitions/​WasmInterpreterTransitions.csproj Adds the IL helper project reference.
src/​tests/​readytorun/​wasm/​WasmInterpreterTransitions/​WasmInterpreterTransitions.cs Adds delegate runtime coverage.
src/​tests/​readytorun/​wasm/​WasmInterpreterTransitions/​OpenInstanceDelegateHelper.ilproj Defines the helper IL project.
src/​tests/​readytorun/​wasm/​WasmInterpreterTransitions/​OpenInstanceDelegateHelper.il Provides the open-instance delegate fixture.
src/​coreclr/​vm/​wasm/​helpers.hpp Declares return-buffer classification.
src/​coreclr/​vm/​wasm/​helpers.cpp Implements Wasm return-buffer classification.
src/​coreclr/​vm/​wasm/​dynamichelpers.cpp Adds the Wasm delay-load bridge.
src/​coreclr/​vm/​prestub.cpp Resolves delegate-constructor fixups.
src/​coreclr/​vm/​precode_portable.hpp Defines portable constructor entry-point data.
src/​coreclr/​vm/​method.hpp Declares the constructor R2R publication helper.
src/​coreclr/​vm/​frames.cpp Tracks Wasm object argument roots.
src/​coreclr/​vm/​cgensys.h Declares the typed Wasm helper entry point.
src/​coreclr/​tools/​aot/​ILCompiler.ReadyToRun/​ILCompiler.ReadyToRun.csproj Includes the new compiler components.
src/​coreclr/​tools/​aot/​ILCompiler.ReadyToRun/​Compiler/​ReadyToRunDelegateCtorRootProvider.cs Roots delegate constructors.
src/​coreclr/​tools/​aot/​ILCompiler.ReadyToRun/​Compiler/​ReadyToRunCodegenCompilationBuilder.cs Registers Wasm constructor roots.
src/​coreclr/​tools/​aot/​ILCompiler.ReadyToRun/​Compiler/​DependencyAnalysis/​ReadyToRunCodegenNodeFactory.cs Caches constructor thunk nodes.
src/​coreclr/​tools/​aot/​ILCompiler.ReadyToRun/​Compiler/​DependencyAnalysis/​ReadyToRun/​WasmImportThunkPortableEntrypoint.cs Connects delegate fixups to shared thunks.
src/​coreclr/​tools/​aot/​ILCompiler.ReadyToRun/​Compiler/​DependencyAnalysis/​ReadyToRun/​WasmDelegateCtorThunkNode.cs Emits DC0 and DC1 Wasm thunks.
src/​coreclr/​tools/​aot/​ILCompiler.ReadyToRun.Tests/​TestCases/​Webcil/​WasmDelegateConstructors.cs Adds ReadyToRun image fixtures.
src/​coreclr/​tools/​aot/​ILCompiler.ReadyToRun.Tests/​TestCases/​R2RTestSuites.cs Validates delegate fixups and thunks.
src/​coreclr/​jit/​flowgraph.cpp Enables the Wasm R2R optimization.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 4dd9dfb0-13d5-4a8c-9419-3f808cbd9b8b
Copilot AI review requested due to automatic review settings September 25, 2026 16:16
@lewing lewing changed the title [wasm][R2R] Enable delegate constructor lowering [wasm][R2R] Call delegate construction helpers directly Sep 25, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

The cross-layer Wasm, JIT, and ReadyToRun changes warrant final human review; one documentation nit also remains.

Review effort: Lite
Findings: 1 Low severity

Open (1)
Resolved since last review (1)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 4dd9dfb0-13d5-4a8c-9419-3f808cbd9b8b
Copilot AI review requested due to automatic review settings September 25, 2026 16:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Virtual targets may be incorrectly routed to CtorClosed, and coverage for excluded generic/value-type shapes is incomplete.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 High severity · 1 Medium severity

Open (2)
Resolved since last review (1)

@davidwrighton davidwrighton left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets use an R2R helper for this case. We'll need to modify the jit interface, but that's ok.

Comment thread src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs Outdated
@lewing
lewing marked this pull request as draft September 26, 2026 00:04
lewing and others added 3 commits September 27, 2026 12:10
…ate-lowering

# Conflicts:
#	src/tests/readytorun/wasm/WasmInterpreterTransitions/WasmInterpreterTransitions.cs
Replace the Crossgen GetDelegateCtor override (which returned CoreLib
MethodDescs across module boundaries) with a new JIT-EE callback,
getDelegateCtorHelper, that selects a fixed managed helper:

- CORINFO_HELP_DELEGATE_CTOR_CLOSED (Delegate.CtorClosed) for closed
  non-virtual, non-generic reference-type instance targets.
- CORINFO_HELP_DELEGATE_CONSTRUCT (Delegate.DelegateConstruct) for all
  other shapes; this is the managed implementation of every delegate ctor.

The helpers are exposed as READYTORUN_HELPER_DelegateConstruct/
DelegateCtorClosed (R2R 30.1), so images stay valid across split
composite and non-cross-module-inlining builds. Removes the dedicated
root provider and manifest token pre-creation.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4dd9dfb0-13d5-4a8c-9419-3f808cbd9b8b
@lewing lewing changed the title [wasm][R2R] Call delegate construction helpers directly [wasm] Lower CoreCLR R2R delegate constructors to R2R helpers Sep 27, 2026
Comment thread src/coreclr/inc/corinfo.h Outdated
lewing and others added 3 commits September 30, 2026 13:58
Revert the R2R-helper lowering (b970b2f) and go back to having Wasm
ReadyToRun use the same GetDelegateCtor transformation as the JIT, with
Crossgen2 selecting Delegate.CtorClosed or Delegate.DelegateConstruct.
The CoreLib method references are pre-seeded in the manifest the same way
as the runtime-async helpers the JIT synthesizes calls to.

Compared to the earlier version of this approach:
- Keep main's structure in fgOptimizeDelegateConstructor; Wasm only skips
  the dynamically composed ReadyToRun helper branch.
- Implement GetDelegateCtor in the ReadyToRun/RyuJit partial files.
- Exclude virtual targets from CtorClosed.
- Seed the manifest tokens lazily, alongside the async references.
- Drop the dedicated root provider; CoreLib compiles these bodies anyway.
- Assert the selected constructor per method in the image test.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4dd9dfb0-13d5-4a8c-9419-3f808cbd9b8b
…ate-lowering

# Conflicts:
#	src/tests/readytorun/wasm/WasmInterpreterTransitions/WasmInterpreterTransitions.cs
#	src/tests/readytorun/wasm/WasmInterpreterTransitions/WasmInterpreterTransitions.csproj
Delegate constructor calls use lazily bound delay-load call cells, which
are not part of a method's eager fixup list, so the constructor selected
for a given method cannot be attributed from the image. Check the image's
import signatures instead; the runtime test covers per-shape behavior.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4dd9dfb0-13d5-4a8c-9419-3f808cbd9b8b
@lewing lewing changed the title [wasm] Lower CoreCLR R2R delegate constructors to R2R helpers [wasm] Use GetDelegateCtor for CoreCLR R2R delegate construction Sep 30, 2026
@lewing
lewing marked this pull request as ready for review September 30, 2026 23:18
Match COMDelegate::GetDelegateCtor: closed delegates over instance
methods on reference types use CtorClosed regardless of whether the
target is virtual or generic. The method pointer has already been
resolved by the caller (ldftn/ldvirtftn, including instantiating stubs),
so CtorClosed only stores the target and pointer.

Add runtime test coverage for a virtual target bound through an override
and for shared and unshared generic method targets.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4dd9dfb0-13d5-4a8c-9419-3f808cbd9b8b
@lewing
lewing requested a review from davidwrighton October 1, 2026 00:09
@lewing
lewing enabled auto-merge (squash) October 1, 2026 00:55
@lewing
lewing merged commit 1863c9f into main Oct 1, 2026
143 of 145 checks passed
@lewing
lewing deleted the lewing-wasm-r2r-delegate-lowering branch October 1, 2026 03:52
@dotnet-milestone-bot dotnet-milestone-bot Bot added this to the 12.0-preview1 milestone Oct 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

arch-wasm WebAssembly architecture area-ReadyToRun tenet-performance Performance related issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[wasm][coreclr][R2R] Delegate construction skips alternate-constructor lowering and remains interpreted

5 participants