Skip to content

[wasm][coreclr] Dispatch R2R-compiled UnmanagedCallersOnly callbacks to their native entrypoint - #134355

Open
pavelsavara wants to merge 14 commits into
dotnet:mainfrom
pavelsavara:wasm_UCO_TO_R2R_dispatch
Open

pavelsavara wants to merge 14 commits into
dotnet:mainfrom
pavelsavara:wasm_UCO_TO_R2R_dispatch

Conversation

@pavelsavara

@pavelsavara pavelsavara commented Sep 21, 2026 •

Copy link
Copy Markdown
Member

Problem

Under a partial ReadyToRun image, an [UnmanagedCallersOnly] callback can be crossgen'd to native (R2R) code. The generated native-to-interpreter reverse thunk (callhelpers-reverse.cpp) routed it unconditionally through ExecuteInterpretedMethodFromUnmanaged, which:

The design already intends R2R-compiled [UnmanagedCallersOnly] methods to be dispatched directly to their native entrypoint (GetUnmanagedCallersOnlyThunk does this), but generated export reverse thunks bypassed that path. The browser event loop calls native C exports directly, for example SystemJS_ExecuteBackgroundJobCallback to System.Threading.ThreadPool.BackgroundJobHandler, which can be R2R-compiled under a partial image.

Fix

The call-helpers generator now emits a direct-R2R dispatch in each supported reverse thunk: resolve and cache the R2R native entrypoint with GetR2RNativeCodeForUnmanagedCallersOnly, call it with the native ABI, and fall back to the interpreter path only when there is no R2R body.

void* r2r = __atomic_load_n(&R2RCode_..., __ATOMIC_ACQUIRE);
if (r2r == (void*)(intptr_t)-1)
{
    r2r = GetR2RNativeCodeForUnmanagedCallersOnly(MD_...);
    __atomic_store_n(&R2RCode_..., r2r, __ATOMIC_RELEASE);
}
if (r2r != nullptr)
{
    ((void(*)())r2r)();
    return;
}
ExecuteInterpretedMethodFromUnmanaged(MD_..., ...);

Additional hardening from review:

  • Multi-slot UCO signatures such as Int128 parameters or returns are explicitly rejected before generating a wrapper whose C function type would not match the lowered WASM signature.
  • Vector128<T> parameters and returns using the v128 ABI are explicitly rejected because the generated C wrapper does not yet model that value type.
  • Aggregate returns that require a hidden return buffer are explicitly rejected because the generated native wrapper does not yet model that ABI shape.
  • Cached entrypoint publication emits acquire/release atomic builtins directly, so VM-compiled and app-linked helpers have identical thread-safe behavior.
  • HasNativeEntryPoint excludes interpreter-preferred entrypoints, so the generated interpreter fallback thunk cannot be returned recursively as R2R code.

GetUnmanagedCallersOnlyThunk is refactored onto the same shared lookup helper. The checked-in browser and WASI call helpers are regenerated.

End-to-end coverage

WasmInterpreterTransitions now exercises:

R2R managed code -> P/Invoke -> echo.c -> generated UCO export -> R2R managed callback

The native round trips cover:

  • int32_t -> int32_t
  • mixed int64_t, float, double -> double
  • int32_t*, int32_t -> void
  • a single-field int32_t struct return, scalarized to i32 by the WASM C ABI

Runtime-test support now compiles raw .c/.cpp NativeFileReference items, includes their module names in generated P/Invoke tables, and links their objects into the test-specific browser corerun.

Validation

  • The original partial-R2R repro crashed before the fix and runs to completion after it.
  • Removing only the generated direct-R2R branch makes the new native round-trip test fail at ExecuteInterpretedMethodFromUnmanaged with targetIp != NULL; restoring it passes.
  • Browser/CoreCLR clr+libs+host build: zero warnings and errors.
  • ILCompiler.ReadyToRun.Tests WasmArgumentLayoutTests: 72 passed, 0 failed.
  • Canonical Browser and WASI helper regeneration completed; both generated reverse-helper sets use direct atomic publication.
  • Browser/CoreCLR runtime rebuild and WASI/CoreCLR runtime compilation with the regenerated helpers: zero warnings and errors.
  • Targeted Browser/CoreCLR WasmInterpreterTransitions build: zero warnings and errors.
  • Node execution of WasmInterpreterTransitions: expected 100, actual 100.

Main files

  • src/coreclr/tools/aot/ILCompiler.ReadyToRun/PortableCallHelpers/PInvokeTableGenerator.cs - emits direct R2R dispatch and rejects unsupported multi-slot callbacks
  • src/coreclr/vm/wasm/helpers.cpp - shared R2R UCO entrypoint lookup
  • src/mono/browser/build/coreclr_compat.h - app-link compatibility declarations for generated helpers
  • src/coreclr/vm/wasm/browser/callhelpers-reverse.cpp, src/coreclr/vm/wasm/wasi/callhelpers-reverse.cpp - regenerated helpers
  • src/tests/Common/CLRTest.WasmCorerun.targets - native source support for Browser/CoreCLR runtime tests
  • src/tests/readytorun/wasm/WasmInterpreterTransitions/ - managed and native end-to-end coverage

Related

Addresses the native-export reverse-thunk manifestation of #134200 by keeping R2R-compiled [UnmanagedCallersOnly] callbacks off the interp-to-R2R path entirely. Part of the browser-wasm CoreCLR ReadyToRun work (#134337).

Note

This PR description was updated with GitHub Copilot assistance and reviewed by the author.

…to their native entrypoint

A partial R2R image can crossgen an [UnmanagedCallersOnly] callback to native code. The generated native->interpreter reverse thunk routed it unconditionally through ExecuteInterpretedMethodFromUnmanaged, which either handed a null interpreter body to the interpreter (fatal 'Unimplemented or invalid interpreter opcode') or, via InvokeManagedMethod, hit the interp->R2R 'null function or function signature mismatch' trap (dotnet#134200).

Repro: the browser event loop calls the SystemJS_ExecuteBackgroundJobCallback export -> ThreadPool.BackgroundJobHandler, which is R2R-compiled under a partial image.

Fix: the call-helpers generator now emits a direct-R2R dispatch in each reverse thunk - resolve the R2R native entrypoint via GetR2RNativeCodeForUnmanagedCallersOnly and call it with the native ABI, falling back to the interpreter only when there is no R2R body. GetUnmanagedCallersOnlyThunk is refactored onto the same shared helper. Regenerated the checked-in browser and wasi call helpers.
@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.

@pavelsavara pavelsavara added the arch-wasm WebAssembly architecture label Sep 21, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @BrzVlad, @janvorli
See info in area-owners.md if you want to be subscribed.

@pavelsavara pavelsavara added this to the 12.0.0 milestone Sep 21, 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 commented Sep 21, 2026

Copy link
Copy Markdown
Member

you'll want to remove

<!-- ActiveIssue https://github.com/dotnet/runtime/issues/134200 -->
<ProjectExclusions Include="$(MSBuildThisFileDirectory)Microsoft.CSharp\tests\Microsoft.CSharp.Tests.csproj" />
<ProjectExclusions Include="$(MSBuildThisFileDirectory)System.Runtime\tests\System.Dynamic.Runtime.Tests\System.Dynamic.Runtime.Tests.csproj" />

@pavelsavara

Copy link
Copy Markdown
Member Author

you'll want to remove
#134200

That's still failing after this PR, it's slightly different.
See #134200 (comment)

@pavelsavara
pavelsavara marked this pull request as ready for review September 22, 2026 09:25
Copilot AI lite review requested due to automatic review settings September 22, 2026 09:25

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

Address the lazy-R2R retry issue and add generator-level coverage for direct dispatch generation.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 Low severity

Open (1)
What changed in this PR

Fixes WebAssembly reverse thunks so R2R-compiled [UnmanagedCallersOnly] callbacks dispatch directly to native entrypoints, with interpreter fallback.

Changes:

  • Adds shared R2R entrypoint resolution.
  • Updates callback generation and regenerates WASI helpers.
  • Preserves interpreter fallback for non-R2R callbacks.
File Summary
src/​coreclr/​vm/​wasm/​wasi/​callhelpers-reverse.cpp Regenerated WASI reverse callback helpers.
src/​coreclr/​vm/​wasm/​helpers.cpp Adds shared R2R callback resolution. Moderate concern (1 vote): lazy R2R may not be retried after interpreter preparation.
src/​coreclr/​tools/​aot/​ILCompiler.ReadyToRun/​PortableCallHelpers/​PInvokeTableGenerator.cs Emits direct R2R dispatch. Nit (3 votes): add generator coverage for emitted R2R and fallback branches.

Comment thread src/coreclr/vm/wasm/browser/callhelpers-reverse.cpp Outdated
Comment thread src/coreclr/vm/wasm/browser/callhelpers-reverse.cpp
Addresses PR review: cache GetR2RNativeCodeForUnmanagedCallersOnly result in a per-callback static (like MD_*) so it runs at most once, and move the blank line to separate the R2R fast path from the interpreter fallback.
Copilot AI review requested due to automatic review settings September 23, 2026 08:41

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 ABI and concurrency findings, plus missing regression coverage, remain unresolved.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 2 High severity

Open (2)
Resolved since last review (1)

…sh cache

Addresses PR review: skip the direct R2R call for callbacks whose return is passed by a hidden buffer (their wasm function type would not match the declared cast), and publish the cached entrypoint with VolatileLoad/VolatileStore so concurrent first calls cannot tear the pointer.
Copilot AI review requested due to automatic review settings September 23, 2026 10:44

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

Direct dispatch can mishandle multi-slot Wasm signatures, and null R2R results may be cached permanently.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 High severity

Open (1)
Resolved since last review (2)

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

Unresolved critical dispatch and concurrency issues, plus ABI and regression-test gaps, block approval.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 3 High severity

Open (3)

Comment thread src/coreclr/vm/wasm/helpers.cpp
Comment thread src/mono/browser/build/coreclr_compat.h Outdated

@radekdoulik radekdoulik 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.

LGTM, the MT issue and retbuf are pre-existing. we can fix them as follow up or open issues. it would be nice to have regression test for this case

pavelsavara and others added 3 commits September 24, 2026 15:51
Teach the runtime-test corerun harness to compile NativeFileReference sources, then exercise an R2R P/Invoke that calls back through a generated UnmanagedCallersOnly export.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Reject multi-slot callback signatures before generating native wrappers, document why interpreter fallback thunks cannot recurse through R2R lookup, and use acquire/release atomics for cached entrypoint publication.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Exercise mixed integer and floating-point arguments, a void pointer callback, and a scalarized single-field struct return through the native-to-R2R export path.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 24, 2026 14:40

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

One or more issues must be addressed before approval.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 High severity

Open (1)
Resolved since last review (3)

Reject UnmanagedCallersOnly callbacks whose aggregate return requires a hidden return buffer instead of emitting an ABI-incompatible native wrapper, and cover the diagnostic in generator tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 24, 2026 15:01

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 ABI validation is missing for v128 callback signatures, risking invalid calls or corrupted data.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 High severity

Open (1)
Resolved since last review (1)

Reject Vector128 UnmanagedCallersOnly parameters and returns before generating native wrappers whose C declarations cannot represent the v128 ABI, and cover both signature positions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 24, 2026 15:58

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

One or more issues must be addressed before approval.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 High severity

Open (1)
Resolved since last review (1)

Generate acquire/release builtins directly so both VM-compiled and app-linked reverse helpers publish cached R2R entrypoints atomically.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 24, 2026 16:42

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

One or more issues must be addressed before approval.

Get a fresh assessment by requesting another Copilot review.

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

Open (2)
Resolved since last review (1)

Comment thread src/tests/Common/CLRTest.WasmCorerun.targets Outdated
User NativeFileReference .c/.cpp now use a dedicated corerun-compile-user.rsp that carries only the shared compile flags (optimization, exception model, SIMD, GEN_PINVOKE) and omits the coreclr_compat.h force-include and kit header path, mirroring the app build's _EmccCFlags/_EmccCFlagsGenerated split so raw sources don't inherit CoreCLR typedefs/macros. Also correct a stale generator comment that still referenced a volatile store.

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

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 changes span runtime dispatch, generated native ABI wrappers, and test-host linking paths, warranting final human review.

Review effort: Lite
Findings: None

Resolved since last review (2)

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants