Skip to content

[wasm][R2R] Fix closed static delegate struct returns - #134108

Merged
lewing merged 17 commits into
dotnet:mainfrom
lewing:lewing-investigate-inspect-6983
Sep 25, 2026
Merged

lewing merged 17 commits into
dotnet:mainfrom
lewing:lewing-investigate-inspect-6983

Conversation

@lewing

@lewing lewing commented Sep 16, 2026 •

Copy link
Copy Markdown
Member

Summary

Fix closed static delegates returning structs through a hidden return buffer on CoreCLR WebAssembly ReadyToRun.

A closed static delegate receives arguments using the instance Delegate.Invoke shape:

(sp, capturedTarget, retbuf, args..., delegatePEP)

The static target follows the Wasm aggregate-return ABI and expects:

(sp, retbuf, capturedTarget, args..., targetPEP)

Both pointer arguments lower to i32, so the previous direct dispatch passed Wasm type validation but silently exchanged the captured object and return-buffer address. Structs containing object references consequently returned null or invalid references.

Approach

Add a portable equivalent of ThisPtrRetBufPrecode, following the existing portable unboxing-stub design:

  • ClosedStaticRetBufPortableEntryPoint stores the target portable entrypoint immediately before its embedded PEP.
  • Crossgen2 emits a D-prefixed WasmClosedStaticRetBufThunkNode, shared by physical Wasm signature. It swaps the captured target and return-buffer parameters, then dispatches through the target PEP.
  • Both call-signature recording paths independently root the corresponding static-target R2R-to-interpreter thunk for each full interpreter-layout signature. Distinct aggregate sizes can share D code without losing their distinct I-thunk dependencies.
  • If either signature thunk is not loaded yet, the specialized PEP remains interpreter-preferred and is registered with the existing pending-thunk lifecycle. Later R2R module injection patches the same PEP in place.
  • Pending adapter registration is skipped when ReadyToRun is disabled because no later R2R string-thunk injection can resolve it; the interpreter fallback remains unchanged.
  • Pending adapter registration uses an O(1) flag on the portable entrypoint instead of scanning the loader allocator's list. The flag is published only after the fallible list appends complete, so cache-hit registration retries remain possible after an allocation failure.
  • The pending method and adapter lists share one SArray<void*> scan/compact helper driven by resolver function pointers. Resolution runs in two global passes—target method PEPs first, then closed-static adapters—so dependencies owned by separate loader allocators can complete during the same module injection.
  • Delegate construction prepares the target PEP before consulting the adapter cache, handling recycled LCG DynamicMethodDesc generations. Cached unresolved adapters also retry pending registration, including after a previous registration failure.
  • Shared generic targets arrive through instantiating wrappers, which carry their generic context without changing the delegate-compatible physical signature.
  • The runtime uses the same Wasm lowering classifier as GetSignatureKey to select only indirect aggregate returns, independently of whether ReadyToRun is enabled. Async targets are excluded because their hidden continuation parameter requires a different adapter shape.
  • Adapters are cached per target and delegate Invoke method, with collectible loader-allocator dependencies preserved.

This keeps the normal closed-delegate representation and preserves Target, Method, equality, and reverse MethodDesc mapping. Interpreted calls continue to use INTOP_CALLDELEGATE; compiled calls use the stable adapter PEP once its prerequisites are available. No generated IL adapter is needed.

Validation

After merging current main, including #133656, this PR removes the single temporary #133618 quarantine from System.Tests.DelegateTests.ClosedStaticDelegate. The test body and all assertions remain unchanged, so the trimmed browser CoreCLR ReadyToRun CI lane now exercises the original object-reference struct-return failure directly. Static verification confirmed that no other #133618 suppression exists in source. Live execution of the newly re-enabled library test is delegated to this PR's CI rather than repeated locally.

Latest stabilization pass:

  • CoreCLR tools/Crossgen2 Release, browser CoreCLR runtime Release and Checked, native CoreCLR Debug, and the focused test build succeeded with zero warnings/errors.
  • Browser-Wasm R2R WasmInterpreterTransitions: expected 100, actual 100. Coverage includes interpreted/R2R creation and targets, runtime-generated targets, generic wrappers, object-reference returns, direct-return controls, 32 LCG recycling generations with required collection, and colliding physical signatures with different aggregate argument layouts.
  • Signature-collision negative control: the new S8/S12 dynamic-target case traps with the previous compiler and passes with the fix. Emitted-image inspection confirms one shared D adapter and both layout-specific target I thunks. The shared adapter's generated instructions are unchanged.
  • Missing-thunk coverage: the emitted dynamic-only signature's D and I keys are absent from both the test and CoreLib images. Temporarily rejecting missing-thunk fallback fails specifically in VerifyDynamicClosedStaticDelegate; restoring fallback passes.
  • Registration-failure negative control: injecting a one-shot OOM immediately before the pending-list append leaves the old flag-first implementation with a cached but unregistered adapter and a later function signature mismatch. Publishing the flag after the append resolves the cached adapter on retry. Fault injection was removed before the final build.
  • Cross-loader ordering negative control: with the adapter allocator registered first, the old per-loader sweep resolved the target PEP (target-native=64190) but left the adapter unresolved (adapter-native=0). The two global passes resolved both (target-native=64190, adapter-native=64186) during the same module injection and the delegate invocation passed.
  • Local-only independent-assembly loading experiment: the same delegate invokes successfully before and after loading its separately compiled R2R caller on the final runtime.
  • With ReadyToRun disabled, basic closed-static invocation and the four affected dynamic/collision/recycling test helpers pass through a local driver.
  • A controlled no-PR comparison showed the earlier Checked startup assertions were caused by mixing a Checked runtime with a Release CoreLib/layout. Both the merge-base and PR runtimes pass with matching Checked artifacts; the PR transition fixture recompiled against that layout also passes.

Earlier revisions also passed desktop System.Runtime.Tests (79,400 total, zero failures, 89 skipped), an adapter-disabled negative control, and the LCG cache-order negative control. The full desktop suite was not rerun for this stabilization commit.

The separate loading, registration-failure, and cross-loader experiments remain local; this PR adds no new test harness or CI lane.

Resolves #133618

Note

This pull request was developed with GitHub Copilot assistance.

Add a portable-entrypoint adapter that reorders the captured target and hidden return buffer before dispatching to a closed static delegate target.

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

Copilot-Session: bd5418a9-09c6-4017-a1e1-edb2cf396b39
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).
13 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 16, 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.

Centralize string-discoverable Wasm thunk key generation and document the closed-static return-buffer signature predicates.

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

Copilot-Session: bd5418a9-09c6-4017-a1e1-edb2cf396b39
Comment thread src/coreclr/vm/comdelegate.cpp
Comment thread src/coreclr/vm/comdelegate.cpp Outdated
Comment thread src/coreclr/vm/comdelegate.cpp Outdated
Fall back to an interpreted IL adapter when no pregenerated closed-static return-buffer thunk exists, and apply the ABI predicate independently of ReadyToRun.

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

Copilot-Session: bd5418a9-09c6-4017-a1e1-edb2cf396b39
@lewing
lewing marked this pull request as ready for review September 17, 2026 00:54
Copilot AI lite review requested due to automatic review settings September 17, 2026 00:54
@azure-pipelines

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

Comment thread src/coreclr/vm/comdelegate.cpp Outdated
Comment thread src/coreclr/vm/comdelegate.cpp Outdated
When no pregenerated closed-static return-buffer thunk is registered, keep the target entrypoint and rely on INTOP_CALLDELEGATE's inline closed-static dispatch instead of generating an IL stub.

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

Copilot-Session: bd5418a9-09c6-4017-a1e1-edb2cf396b39

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 unresolved critical fallback-stub preparation concern and moderate test-coverage gap make automated approval unsafe.

Review effort: Lite
Findings: None

What changed in this PR

This PR fixes CoreCLR WebAssembly ReadyToRun closed static delegates returning aggregate structs by adding return-buffer argument shuffling.

Changes:

  • Adds runtime IL fallback adapters and portable entrypoint support.
  • Emits and roots signature-shared Crossgen2 WebAssembly thunks.
  • Expands R2R, interpreted, generic, and dynamic delegate regression coverage.
File Summary
src/​tests/​readytorun/​wasm/​WasmInterpreterTransitions/​WasmInterpreterTransitions.cs Adds delegate regression coverage. Moderate: fallback tests should validate captured object references in returned structs.
src/​libraries/​System.Runtime/​tests/​System.Runtime.Tests/​System/​DelegateTests.cs Verifies delegate metadata, identity, equality, and object-reference returns.
src/​coreclr/​vm/​wasm/​helpers.hpp Declares Wasm return-buffer classification and thunk lookup helpers.
src/​coreclr/​vm/​wasm/​helpers.cpp Implements Wasm signature classification and thunk lookup.
src/​coreclr/​vm/​precode_portable.hpp Adds portable closed-static thunk entrypoint storage.
src/​coreclr/​vm/​method.hpp Adds the new IL stub type.
src/​coreclr/​vm/​ilstubcache.cpp Registers and names the new stub type.
src/​coreclr/​vm/​fptrstubs.h Adds adapter cache declarations.
src/​coreclr/​vm/​fptrstubs.cpp Implements adapter caching.
src/​coreclr/​vm/​dllimport.h Adds related stub flags and classification support.
src/​coreclr/​vm/​comdelegate.cpp Selects, creates, and caches adapters. Critical: prepare dynamic IL stubs before caching their addresses; nit: remove the dead null branch.
src/​coreclr/​tools/​aot/​ILCompiler.ReadyToRun/​JitInterface/​CorInfoImpl.ReadyToRun.cs Roots thunk dependencies for relevant signatures.
src/​coreclr/​tools/​aot/​ILCompiler.ReadyToRun/​ILCompiler.ReadyToRun.csproj Includes the new thunk node. Nit: preserve alphabetical item ordering.
src/​coreclr/​tools/​aot/​ILCompiler.ReadyToRun/​Compiler/​DependencyAnalysis/​ReadyToRunCodegenNodeFactory.cs Caches and roots generated thunks.
src/​coreclr/​tools/​aot/​ILCompiler.ReadyToRun/​Compiler/​DependencyAnalysis/​ReadyToRun/​WasmVirtualDispatchThunkNode.cs Uses shared lookup-key generation.
src/​coreclr/​tools/​aot/​ILCompiler.ReadyToRun/​Compiler/​DependencyAnalysis/​ReadyToRun/​WasmUnboxingStubNode.cs Uses shared lookup-key generation.
src/​coreclr/​tools/​aot/​ILCompiler.ReadyToRun/​Compiler/​DependencyAnalysis/​ReadyToRun/​WasmClosedStaticRetBufThunkNode.cs Emits the closed-static return-buffer thunk.
src/​coreclr/​tools/​aot/​ILCompiler.ReadyToRun/​Compiler/​DependencyAnalysis/​ReadyToRun/​StringDiscoverableAssemblyStubNode.cs Provides shared Wasm lookup-key generation.
src/​coreclr/​tools/​aot/​ILCompiler.ReadyToRun/​Compiler/​DependencyAnalysis/​ReadyToRun/​DelegateCtorSignature.cs Roots thunks for delegate constructor signatures.

Copilot AI review requested due to automatic review settings September 17, 2026 01:06
@lewing

lewing commented Sep 17, 2026 •

Copy link
Copy Markdown
Member Author

More cleanup coming - Done

Root closed-static return-buffer thunks only from compiled call sites, remove creation-only tests that no longer discriminate correctness, and leave interpreter fallbacks uncached for later R2R module registration.

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

Copilot-Session: bd5418a9-09c6-4017-a1e1-edb2cf396b39
Copilot AI review requested due to automatic review settings September 21, 2026 22:30

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

Critical issues remain in Wasm thunk ordering and unmanaged-signature adapter classification.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 2 High severity

Open (2)
Resolved since last review (1)

Comment thread src/coreclr/vm/comdelegate.cpp
Comment thread src/coreclr/vm/pregeneratedstringthunks.cpp Outdated
Comment thread src/coreclr/vm/pregeneratedstringthunks.cpp Outdated
Remove the unused pending-resolution wrapper, preserve Wasm function-table ordering for the D thunk, and use standard contracts for the resolution-only call chain.

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

Copilot-Session: bd5418a9-09c6-4017-a1e1-edb2cf396b39
Copilot AI review requested due to automatic review settings September 21, 2026 23:30

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 broad runtime/AOT/Wasm change has a moderate (1-vote) request for a checked-in cross-loader regression and warrants human approval.

Review effort: Lite
Findings: 1 High severity

Open (1)
Resolved since last review (1)

@lewing

lewing commented Sep 23, 2026

Copy link
Copy Markdown
Member Author

this code should be updated to use GetClosedStaticDelegateTargetSignatureonce #134497 lands

@pavelsavara

Copy link
Copy Markdown
Member

It would we also good to add the shapes that this fixes into src/tests/readytorun/wasm/WasmInterpreterTransitions/WasmInterpreterTransitions.cs

And maybe also into src/tests/readytorun/wasm/WasmInterpreterTransitions/echo.c after #134355 lands. Does this PR impact/enable native C ABI too ?

@lewing

lewing commented Sep 25, 2026

Copy link
Copy Markdown
Member Author

Thanks. WasmInterpreterTransitions already has the managed closed-static shapes added by this PR: ObjectPairTargetExtensions.GetPair returning a struct with two object references, the S8/S12 argument-layout collision returning S16, runtime-generated targets, and recycled dynamic targets. The D adapter specifically reorders (capturedTarget, retbuf) into the managed static target's (retbuf, capturedTarget) order; it doesn't change the native C ABI.

The adapter can precede a managed-callable P/Invoke stub. I verified that locally with a native-linked closed P/Invoke delegate returning a 16-byte blittable struct: the D adapter returned the expected fields, while temporarily bypassing it returned corrupted fields. That's a useful separate end-to-end case we could put alongside echo.c once #134355 provides the native test asset path, but it isn't a native-to-UCO reverse callback. #134355 currently rejects UCO exports with hidden return buffers, so its existing C -> UCO echo path cannot exercise the ObjectPair/retbuf case without additional native-ABI support.

Note

This reply was generated with GitHub Copilot assistance.

Consume GetClosedStaticDelegateTargetSignature from dotnet#134497 for the closed-static retbuf thunk dependency, removing the duplicate signature construction.

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

Copilot-Session: bd5418a9-09c6-4017-a1e1-edb2cf396b39
Copilot AI review requested due to automatic review settings September 25, 2026 17:01
@lewing

lewing commented Sep 25, 2026

Copy link
Copy Markdown
Member Author

this code should be updated to use GetClosedStaticDelegateTargetSignatureonce #134497 lands

done

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

Automated coverage is still needed for cross-loader resolution ordering and registration-failure retry.

Review effort: Lite
Findings: None

Resolved since last review (1)

@lewing
lewing enabled auto-merge (squash) September 25, 2026 19:25
@lewing
lewing merged commit 181b7ea into dotnet:main Sep 25, 2026
148 of 150 checks passed
@dotnet-milestone-bot dotnet-milestone-bot Bot added this to the 12.0-preview1 milestone Sep 26, 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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[wasm][R2R] Closed static delegate returning a struct with object references fails

6 participants