Skip to content

Enable ReadyToRun for CoreCLR browser-wasm - #132339

Draft
pavelsavara wants to merge 3 commits into
dotnet:mainfrom
pavelsavara:browsehost_load_r2r_3
Draft

Enable ReadyToRun for CoreCLR browser-wasm#132339
pavelsavara wants to merge 3 commits into
dotnet:mainfrom
pavelsavara:browsehost_load_r2r_3

Conversation

@pavelsavara

@pavelsavara pavelsavara commented Aug 14, 2026

Copy link
Copy Markdown
Member

Enables ReadyToRun (R2R) for the CoreCLR runtime flavor on browser-wasm (experimental in .NET 11).
R2R images are webcil-in-wasm modules: a wasm module whose data carries the assembly's ECMA metadata
(the per-assembly manifest) plus the crossgen2-produced R2R native code. Non-composite (per-assembly)
only; composite is out of scope but nothing here precludes it.

Related dotnet/sdk#55785

Commits (incremental review)

The change is split into three commits so the first two can stand alone as pre-step PRs if preferred:

  1. Enable crossgen2 to target browser-wasm/wasi-wasmjit/CMakeLists.txt + Crossgen2Tasks. Tooling enablement, no app-facing behavior.
  2. Ship prebuilt framework R2R images in the runtime pack — producer only; nothing consumes the images yet.
  3. Consume R2R images in the CoreCLR app build and publish — the task + SDK targets integration that depends on 1 and 2.

Two R2R flavors, one staging path

Both flavors are consumed by the existing ConvertDllsToWebcil task through a new
PrebuiltR2RDirectories parameter, so R2R images ride the same Static Web Assets (SWA) pipeline as a
plain webcil — they are just a larger _framework/<name>.wasm.

Flavor When Source of the R2R image
Prebuilt framework R2R dev-loop dotnet build, and untrimmed publish runtime pack native/r2r/*.wasm (all 181 shared-framework assemblies, shipped in the pack)
Per-app crossgen R2R trimmed publish crossgen2 over the trimmed ILLink closure → $(IntermediateOutputPath)R2R/*.dll

The prebuilt images are untrimmed, so they may only substitute for an untrimmed input. A build is
never trimmed, so it always uses the pack images. A trimmed publish must crossgen the trimmed
closure per-app (an untrimmed pack image can reference types the trimmer removed).

PublishReadyToRun defaults to true for CoreCLR browser (mirrors AppleBuild.props); set it
false for a pure-IL webcil app, byte-identical to today.

Producer — runtime pack

Microsoft.NETCore.App.Runtime.CoreCLR.sfxproj crossgens each shared-framework assembly to a
webcil-in-wasm R2R image and packs it under runtimes/browser-wasm/native/r2r/, keeping the IL
lib/net11.0/*.dll in place
for trimming and reference resolution. This is the CoreLib model
(crossgen-corelib.proj) generalized to the whole framework.

  • The SDK's in-place R2R cannot be used here: it replaces lib/*.dll with the R2R image and drops
    the IL, which trimming and per-app crossgen still need.
  • native/r2r/ is a dedicated subfolder so the raw images never collide with dotnet.native.wasm or
    a served _framework webcil, and so the prune hooks below match on one path prefix (no name-based
    dotnet.native special-casing).
  • Routed via LibrariesRuntimeFiles ... IsNative="true" NativeSubDirectory="r2r";
    Microsoft.NETCore.App.Runtime.props already maps %(NativeSubDirectory) to
    native/<sub>, so no packaging-layer change is needed.
  • System.Private.CoreLib.wasm is produced by the coreclr build (crossgen-corelib), not by this
    loop (CoreLib is not in the libraries shared-framework dir), so it is picked up from
    $(CoreCLRArtifactsPath) explicitly.
  • Each image gets a name-only PlatformManifestFileEntry (required for RuntimePack file validation).
  • WasmFrameworkR2RSubset (;-delimited assembly names) crossgens only a subset, for bring-up.
  • The images are also copied into the in-tree runtime-pack layout
    ($(MicrosoftNetCoreAppRuntimePackNativeDir)r2r/) because that layout is assembled by
    binplace + libs.pretest, not from the pack nupkg, so local dev/test builds need them copied in.
  • NYI/SIMD methods that crossgen2 cannot emit into R2R fall back to the runtime JIT/interpreter
    (--codegenopt:JitWasm{,Simd}NyiToR2RUnsupported=1).

Measured (Release): 181 images, 90.6 MB R2R vs 15.4 MB IL (CoreLib 29 MB dominates). The crossgen
loop is 181 serial Exec processes; a parallel wrapper is a possible follow-up.

Consumer — SDK targets

All CoreCLR logic lives in a new
Microsoft.NET.Sdk.WebAssembly.Browser.CoreCLR.targets, imported at the end of
Microsoft.NET.Sdk.WebAssembly.Browser.targets with Condition="'$(UseMonoRuntime)' == 'false'", so
Mono is never touched.

  • It must be a .targets, not .props. The shipped SDK defaults UseMonoRuntime=true and imports
    the WebAssembly .props before the project body sets UseMonoRuntime=false, so any .props
    condition on UseMonoRuntime would be permanently dead. The property defaults land at targets time,
    which is still before ProcessFrameworkReferences (a target) emits the crossgen2 PackageDownload.
  • The import condition is '$(UseMonoRuntime)' == 'false' (not != 'true') because Mono browser apps
    commonly leave UseMonoRuntime unset.

Property defaults (when PublishReadyToRun): PublishReadyToRunContainerFormat=wasm (the net11+ SDK
defaults it to pe, which the wasm runtime can't load), PublishReadyToRunComposite=false, and the
NYI codegen args. The residual edit to the shared Browser.targets is the one import plus
PrebuiltR2RDirectories="…" on the two ConvertDllsToWebcil calls (empty and inert for Mono).

Targets in CoreCLR.targets:

  • _WasmCoreClrSelectR2RDirectories — computes the build vs publish probe directories (separate
    properties: build always uses the pack dir; publish uses the per-app crossgen dir when trimming,
    else the pack dir). _WasmPackR2RDir is computed at target time because it depends on
    _RuntimePackNativeDir, which _ResolveGlobalizationConfiguration only sets at target time.
  • _WasmCoreClrPrunePackR2RFromBuild / _WasmCoreClrPruneR2RFromPublish — remove the raw pack
    native/r2r/*.wasm from ReferenceCopyLocalPaths / ResolvedFileToPublish so the 90 MB untrimmed
    closure never lands loose in bin/ or the publish root. The managed .dll candidate stays;
    ConvertDllsToWebcil stages the matching R2R image in its place.
  • _WasmFeedReadyToRunCompileList / _WasmEnsureReadyToRunBeforePublish — see "SWA vs iOS" below.

Task — ConvertDllsToWebcil

New optional string[] PrebuiltR2RDirectories. For a managed, non-culture .dll candidate whose
R2RWebcilPath metadata is empty, probe each directory for a matching image and stage (copy) the
first hit instead of converting the IL. Existing R2RWebcilPath metadata still wins.

Probe order is .dll before .wasm: per-app crossgen (--obj-format:wasm --out:<name>.dll)
writes the R2R image to <name>.dll and can leave a same-named <name>.wasm that is not the R2R
image. Pack images are named <name>.wasm with no sibling .dll, so they still resolve.

SWA vs iOS — the practical difference

The crossgen step is identical to iOS: the wasm publish reuses the stock SDK
_PrepareForReadyToRunCompilation → _CreateR2RImages → CreateReadyToRunImages chain. The divergence
is entirely in how the produced R2R images reach the output.

iOS / standard publish wasm
R2R images in the output CreateReadyToRunImages swaps them into ResolvedFileToPublish, copied as native files pruned from ResolvedFileToPublish, fed to ConvertDllsToWebcilDefineStaticWebAssets
fingerprint / SRI integrity / brotli+gzip / boot manifest not applied to R2R native files applied by SWA (a superset for web delivery)
native symbols (_CreateR2RSymbols), Mach-O link (_LinkReadyToRunMachO), apphost/.app emitted / linked not applicable

Telemetry is preserved. Because the wasm publish reuses CreateReadyToRunImages, the SDK's
AllowEmptyTelemetry EventName="ReadyToRun" (crossgen2 pack version, container format, compile-list
count, failure count) still fires, as do the PublishReadyToRun/PublishReadyToRunComposite
target-framework telemetry properties and the crossgen failure→NETSdkError path.

The cost of the SWA route is two hooks that re-add what the standard ResolvedFileToPublish-based path
gives for free, because on the Blazor/wasm route managed assemblies flow ILLink → SWA and never
surface as ResolvedFileToPublish with PostprocessAssembly=true:

  • _WasmFeedReadyToRunCompileList — the mainline builds the R2R compile list from
    PostprocessAssembly=true items on ResolvedFileToPublish. On the SWA route ILLink stamps that on
    its own collection instead, so the mainline list is empty and crossgen2 never runs. This target
    bridges the trimmed IL from the linker output dir into ResolvedFileToPublish as the compile +
    reference set, and clears any stale base-SDK R2R mark on the untrimmed runtime-pack copy (e.g.
    CoreLib under native/) so crossgen2 can resolve against the trimmed closure.
  • _WasmEnsureReadyToRunBeforePublishProcessPublishFilesForWasm runs AfterTargets="ILLink",
    which in a Blazor publish schedules it before CreateReadyToRunImages; without forcing crossgen
    first, the publish would convert trimmed IL and the R2R images (produced later) would never ship.

In-tree

  • WasmApp.InTree.props keeps only the two props-time hooks: AfterMicrosoftNETSdkTargets appends
    WasmApp.ReadyToRun.targets (which overrides the ResolveReadyToRunCompilers target to use the
    in-build crossgen2, mirroring AppleBuild.ReadyToRun.targets) ahead of the SDK targets import, plus
    the Crossgen2SdkOverridePropsPath import. Both inert unless PublishReadyToRun.
  • BrowserWasmApp.CoreCLR.targets remaps _WasmManagedAssemblies to the trimmed IL from
    $(IntermediateLinkDir): with R2R the published .dll are webcil-in-wasm images that
    MetadataLoadContext (in ManagedToNativeGenerator) cannot parse, so the IL closure is scanned
    instead.

JIT packaging

jit/CMakeLists.txt dual-installs clrjit_universal_wasm_* into both wasmjit and alljits, so the
shipped Microsoft.NETCore.App.Crossgen2.<hostRID> pack carries the wasm cross-JIT. crossgen2 v6+
auto-loads clrjit_universal_wasm_<host> from its own tools/ dir (no --jitpath).

Related dotnet/sdk change

ResolveReadyToRunCompilers (kept in sync in src/tasks/Crossgen2Tasks/) gains wasm architecture
and browser/wasi OS support so the shipped SDK can resolve the crossgen2 pack for wasm. The
matching dotnet/sdk PR also stops forcing --composite for the wasm container format (that produced
a composite-of-one image with no per-assembly manifest → runtime COR_E_BADIMAGEFORMAT).

Validation

  • Mono unchanged: build + publish src/mono/sample/wasm/browser before/after and diff the executed
    targets and the _framework listing.
  • CoreCLR sample: dotnet build (pack R2R staged) and trimmed dotnet publish (per-app crossgen);
    confirm _framework/<name>.wasm sizes match the source and nothing lands loose.
  • PublishReadyToRun=false: pure-IL webcil, byte-identical to today.
  • Out-of-tree Blazor publish (untrimmed): renders with per-app R2R CoreLib (per-assembly manifest
    present); the app boots and executes managed code.

Note

This pull request was drafted with assistance from GitHub Copilot.

ResolveReadyToRunCompilers: add wasm architecture and browser/wasi OS so the SDK can resolve the crossgen2 pack for wasm. Dual-install clrjit_universal_wasm_* into the alljits component so the shipped Microsoft.NETCore.App.Crossgen2.<hostRID> pack carries the wasm cross-JIT (crossgen2 v6+ auto-loads it from tools/).

Assisted-by: GitHub Copilot
… pack

Crossgen each shared-framework assembly to a webcil-in-wasm R2R image under runtimes/browser-wasm/native/r2r/, keeping the IL in lib/ (the CoreLib model generalized to the whole framework). CoreLib.wasm comes from the coreclr build. WasmFrameworkR2RSubset limits the set for bring-up. Nothing consumes these yet; the SDK integration is a follow-up commit. See dotnet#121257.

Assisted-by: GitHub Copilot
…ublish

ConvertDllsToWebcil gains PrebuiltR2RDirectories to stage prebuilt/per-app R2R webcils in place of IL. New Browser.CoreCLR.targets (imported only for UseMonoRuntime=false) defaults R2R on, forces wasm container format + non-composite, prunes raw pack images, feeds the trimmed closure to crossgen2, and orders R2R before the wasm publish. In-tree hooks override ResolveReadyToRunCompilers to the in-build crossgen2. Design and SWA-vs-iOS notes in PR.md.

Assisted-by: GitHub Copilot
Copilot AI lite review requested due to automatic review settings August 14, 2026 19:20
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 6 pipeline(s).
10 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 arch-wasm WebAssembly architecture area-ReadyToRun os-browser Browser variant of arch-wasm and removed area-Build-mono labels Aug 14, 2026
@pavelsavara pavelsavara added this to the 12.0.0 milestone Aug 14, 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.

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.

Pull request overview

This PR adds end-to-end build/pack/SDK plumbing to enable ReadyToRun (R2R) for the CoreCLR browser-wasm runtime flavor by producing per-assembly “webcil-in-wasm” R2R images and teaching the WebAssembly SDK pipeline to stage/consume them through the existing Static Web Assets + ConvertDllsToWebcil flow.

Changes:

  • Extend ConvertDllsToWebcil and the WebAssembly Browser targets to optionally probe and stage prebuilt R2R images (pack-provided or per-app crossgen output) instead of converting IL.
  • Update Crossgen2Tasks R2R compiler resolution logic to recognize browser/wasi target OS and wasm architecture.
  • Add new CoreCLR-specific WebAssembly SDK targets + runtime-pack producer wiring (including packaging of wasm cross-JIT and producing native/r2r/*.wasm payloads).

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/ConvertDllsToWebCil.cs Adds PrebuiltR2RDirectories probing and staging of prebuilt R2R webcil-in-wasm images.
src/tasks/Crossgen2Tasks/RunReadyToRunCompiler.cs Introduces a new Crossgen2 task parameter for composite extra args (currently unused).
src/tasks/Crossgen2Tasks/ResolveReadyToRunCompilers.cs Adds browser/wasi OS and wasm architecture support for Crossgen2 pack resolution.
src/mono/sample/wasm/console-node/Wasm.Console.Node.Sample.csproj Sets PublishReadyToRun in the sample project.
src/mono/sample/wasm/browser-advanced/Wasm.Advanced.Sample.csproj Adds a commented-out PublishReadyToRun line in the example block.
src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets Wires PrebuiltR2RDirectories into ConvertDllsToWebcil and imports CoreCLR-specific targets when UseMonoRuntime == false.
src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.CoreCLR.targets New: CoreCLR-only R2R defaults, selection of probe dirs, pruning raw pack R2R from outputs, and publish ordering hooks.
src/mono/browser/build/WasmApp.ReadyToRun.targets New: in-tree override of ResolveReadyToRunCompilers to use in-build crossgen2.
src/mono/browser/build/WasmApp.InTree.props Adds props-time hooks to import the in-tree R2R override for CoreCLR browser builds.
src/mono/browser/build/BrowserWasmApp.CoreCLR.targets Remaps managed assembly scanning to linker output when R2R webcil-in-wasm images are present.
src/installer/pkg/sfx/Microsoft.NETCore.App/Microsoft.NETCore.App.Runtime.CoreCLR.sfxproj Adds producer-side crossgen loop to emit and pack native/r2r/*.wasm framework images for browser/wasi.
src/coreclr/jit/CMakeLists.txt Ensures the universal wasm JIT is installed into alljits (packaging for crossgen2).

Comment on lines 23 to 27
public bool ShowCompilerWarnings { get; set; }
public bool UseCrossgen2 { get; set; }
public string Crossgen2ExtraCommandLineArgs { get; set; }
public string Crossgen2CompositeExtraCommandLineArgs { get; set; }
public ITaskItem[] Crossgen2PgoFiles { get; set; }
Comment on lines +19 to +24
<PropertyGroup Condition="'$(PublishReadyToRun)' == 'true'">
<!-- Force over SDK defaults: net11+ defaults ContainerFormat=pe (unloadable on wasm); staging requires non-composite. -->
<PublishReadyToRunContainerFormat Condition="'$(PublishReadyToRunContainerFormat)' != 'wasm'">wasm</PublishReadyToRunContainerFormat>
<PublishReadyToRunComposite Condition="'$(PublishReadyToRunComposite)' != 'false'">false</PublishReadyToRunComposite>
<!-- NYI/SIMD methods crossgen2 can't emit into R2R fall back to the runtime JIT. -->
<PublishReadyToRunCrossgen2ExtraArgs>$(PublishReadyToRunCrossgen2ExtraArgs);--codegenopt:JitWasmNyiToR2RUnsupported=1;--codegenopt:JitWasmSimdNyiToR2RUnsupported=1</PublishReadyToRunCrossgen2ExtraArgs>
Comment on lines +91 to +106
<PropertyGroup>
<_WasmCoreLibIL Condition="Exists('$(CoreCLRArtifactsPath)System.Private.CoreLib.dll')">$(CoreCLRArtifactsPath)System.Private.CoreLib.dll</_WasmCoreLibIL>
<_WasmCoreLibIL Condition="'$(_WasmCoreLibIL)' == '' and Exists('$(CoreCLRArtifactsPath)IL\System.Private.CoreLib.dll')">$(CoreCLRArtifactsPath)IL\System.Private.CoreLib.dll</_WasmCoreLibIL>
<_WasmCrossgen2Exe>$([MSBuild]::NormalizePath('$(Crossgen2InBuildDir)', 'crossgen2$(ExeSuffix)'))</_WasmCrossgen2Exe>
</PropertyGroup>
</Target>

<Target Name="_CrossgenBrowserFrameworkR2R"
Condition="'$(WasmEnableFrameworkR2R)' == 'true'"
DependsOnTargets="_ComputeBrowserFrameworkR2RInputs"
Inputs="@(_WasmFrameworkR2RInput);$(_WasmCoreLibIL)"
Outputs="@(_WasmFrameworkR2RInput->'%(OutputWasm)')">
<MakeDir Directories="$(_WasmFrameworkR2RDir)" />
<Message Importance="High" Text="Crossgen R2R (wasm): %(_WasmFrameworkR2RInput.FileName)" />
<Exec Condition="Exists('$(_WasmCrossgen2Exe)')"
Command="&quot;$(_WasmCrossgen2Exe)&quot; -o:&quot;%(_WasmFrameworkR2RInput.OutputWasm)&quot; -r:&quot;$(LibrariesSharedFrameworkBinArtifactsPath)*.dll&quot; -r:&quot;$(_WasmCoreLibIL)&quot; --targetarch:$(TargetArchitecture) --targetos:$(TargetOS) --obj-format:wasm --codegenopt:JitWasmNyiToR2RUnsupported=1 --codegenopt:JitWasmSimdNyiToR2RUnsupported=1 -O &quot;%(_WasmFrameworkR2RInput.FullPath)&quot;" />
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 os-browser Browser variant of arch-wasm

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants