Skip to content

[wasm] Make CoreCLR WebCIL images self-installing (wrapper version 2) - #134312

Merged
lewing merged 22 commits into
dotnet:mainfrom
lewing:lewing-wasm-self-installing-segments
Sep 28, 2026
Merged

lewing merged 22 commits into
dotnet:mainfrom
lewing:lewing-wasm-self-installing-segments

Conversation

@lewing

@lewing lewing commented Sep 20, 2026 •

Copy link
Copy Markdown
Member

Summary

Make all CoreCLR WebAssembly WebCIL images self-installing, so WASI R2R can be enabled without specialized splice/probe tooling and CoreCLR hosts no longer install payloads by hand.

Previously the WebCIL payload and function table were passive segments that the host copied in by calling getWebcilPayload and fillWebcilTable, which also meant the WASI publishing flow needed extra tooling to splice those segments into the host module. With this change every CoreCLR image carries active segments placed at host-supplied base globals, so the engine installs the payload and table slice at instantiation.

Wrapper format: version 2

This introduces WebCIL wrapper version 2 (the webcilVersion global); the payload header itself is unchanged at version 1.

  • All CoreCLR images use it: IL-only, single-assembly R2R, composite R2R, and composite component forwarding stubs.
  • Payload: data segment 1 is an active segment at (global.get __memory_base).
  • Function table (R2R only): an active element segment at (global.get __table_base).
  • Size metadata: data segment 0 stays passive and still holds payloadSize (and tableSize for R2R), so hosts can reserve the aligned memory and table ranges before instantiation.
  • Exports: getWebcilSize, plus patchWebcilHeader for R2R images. It writes the header's TableBase field, which the runtime reads from linear memory and no segment can supply. getWebcilPayload and fillWebcilTable are removed.
  • Wrapper version 1 is retired: CoreCLR hosts accept only version 2 and reject other versions with a clear error, rather than supporting the old import names.
  • Mono is unchanged and keeps the passive version 0 wrapper.

Changes

  • crossgen2 (WebCilObjectWriter): always emits active payload/element segments and the two stubs; passive-only encoding paths removed.
  • IL-only wrapper (WebcilWasmWrapper, WebcilConverter): emits the version 2 wrapper for version 1 payloads (CoreCLR) and keeps the passive version 0 wrapper for Mono.
  • Hosts (browser host, corerun): require wrapper version 2, always supply __memory_base, and call patchWebcilHeader for R2R images. Allocation from boot-config payloadSize/tableSize and streaming instantiation are unchanged, and neither host parses the module layout before instantiation.
  • Readers: the task-side WebcilReader and the R2R WebcilImageReader accept active payloads, including the memory-indexed active form (flag 2).
  • Docs: docs/design/mono/webcil.md documents wrapper version 2, the host ABI, and the matching-artifact contract.

This is the focused prerequisite for enabling WASI R2R without the previous custom splice/probe pipeline. #133265's WASI composer currently tells component stubs from composites by passive vs. active segments, so it needs a small follow-up.

Validation

  • clr.crossarchtools+libs.sfx prerequisites, then clr.r2rtests (browser-wasm): WasmCompositeModule and WebcilSegmentAlignment pass. They check active payload/table placement, exact size and table-index metadata, exports, the wrapper version, alignment, and the emitted component stubs.
  • WebcilInWasmSizesTests (22 tests): converter output for wrapper versions 0 and 2, active payloads with memory indices 0 and 128 (flag 2), metadata and MVID extraction, and stale prebuilt-image rejection.
  • Native JS: tsc, ESLint, and Release Rollup bundles pass.
  • Generated IL-only, composite, and component-stub modules pass wasm-tools validate and instantiate through the host sequence in Node: the installed payload is byte-identical, the table slice is filled, and TableBase is patched.

Note

This PR description was drafted with GitHub Copilot.

lewing and others added 5 commits September 20, 2026 12:48
A wasm R2R image previously emitted its webcil payload and R2R function
table as passive segments and imported seven things under short names, so
the WASI splice pipeline had to rewrite the segments and rename the imports
after linking. Emit both correctly in the first place.

An image that carries code - a composite or a single-assembly R2R image -
now emits the payload as an active data segment at (global.get __memory_base)
and the function table as an active element segment at (global.get __table_base),
so the engine installs both at instantiation. Such an image exports
patchWebcilHeader in place of getWebcilPayload/fillWebcilTable: memory.init
and table.init against an active segment trap, because an active segment is
implicitly dropped once applied, and the only remaining work is the header's
tableBase field, which the runtime reads from the mapped image.

Per-assembly component forwarding stubs keep the passive shape. A stub may be
parsed from its file rather than instantiated, and the WASI host locates its
payload by passive data segment index.

Import names now match what wasm-ld exports, collapsing the R2R/NativeAOT
split in WasmWellKnownGlobalSymbolNode rather than adding a second one, so a
merged image resolves by name with no renaming step.

The webcilCount segment stays passive in both shapes: the host loader reads
it out of the module bytes before instantiation to size its allocation.

Both JS loaders feature-detect the two shapes rather than assuming one.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
TryFindWebcilInWasm only inspected passive data segments, so it could not
locate the payload of a self-installing image and reported "Unknown file
format". That broke r2rdump and the WasmWebcilModule test, both of which read
single-assembly R2R wasm images through this reader.

Inspect every data segment for the Webcil magic regardless of kind, skipping
an active segment's offset expression first. ParseElemSection already handled
both forms, so only the data side needed it.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
__memory_base and __table_base belong to the emscripten/wasm-ld dynamic
linking ABI, where they carry a per-side-module meaning. Reusing them for the
R2R image and table bases is correct only while the host is a non-PIC main
module; -sMAIN_MODULE would give the linker its own definitions and collide.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
A self-installing image leaves __memory_base and __table_base as imports, and
the two ways a host can satisfy them are not equivalent. Supplying them at
instantiation keeps the segment offsets a global.get of an imported global,
which is a valid constant expression and needs no further processing.
Defining and exporting them, then merging the image into the host,
internalizes the globals; global.get of a defined global is only a constant
expression under the GC proposal, engines disagree, and the merged module has
to have its offsets folded to i32.const to be portable.

Record that the fold is not free, since the pass that performs it also
propagates globals into function bodies, and that a host reserving the table
slice at link time can treat __table_base as the constant 1 while
__memory_base must be read out of the linked host.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The remark named WasiExtractStubPayload, which lives in an offline host that
is not in this tree, so a reader cannot follow the reference. State the
constraint itself instead: a stub may be parsed as a file rather than
instantiated, and an active segment would defeat locating its payload by
passive data segment index.

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

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 4 pipeline(s).
12 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 commented Sep 20, 2026 •

Copy link
Copy Markdown
Member Author

I extracted this from #133265 because it changes the webcil format and is reviewable separately

@lewing

lewing commented Sep 20, 2026

Copy link
Copy Markdown
Member Author

An alternative is dropping wecil for wasi r2r now. The main change here is using well known symbol names that follow the dynamic linking design and work with tooling as a convenient way for the R2R image to be position-independent until the host/layout is known. After composition, the ideal end state is effectively static.

@lewing

lewing commented Sep 20, 2026

Copy link
Copy Markdown
Member Author

cc @dotnet/wasm-contrib

The self-installing WebCIL R2R image shape emits the payload as an active data segment based at __memory_base. Update the existing alignment test to parse and assert that active segment header instead of expecting the old passive segment shape.

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

Copilot-Session: a519827b-7ffd-4499-92f6-8f94d61ea940
DwarfHelper.ReadULEB128 returns ulong, so keep the parsed active payload global index as ulong before comparing it with the WebCIL image base global index.

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

Copilot-Session: a519827b-7ffd-4499-92f6-8f94d61ea940
@lewing lewing added the arch-wasm WebAssembly architecture label 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 marked this pull request as ready for review September 21, 2026 23:55
Copilot AI lite review requested due to automatic review settings September 21, 2026 23:55
@pavelsavara

Copy link
Copy Markdown
Member

We should drop payload buffer allocation in JS and make sure that active segments are aligned in a way that matches the CoreCLR PE expectations.

I think we need to distinguish copying from reserving the destination here. Active segments handle the copy, but for a separately instantiated module sharing the runtime’s memory, something still needs to reserve an aligned range and supply __memory_base. Similarly, an active element segment populates the table but doesn’t reserve or grow its range.

Thank you for explanation, that changes a lot. It means that we need to keep payloadSize and tableSize, so that we don't have to parse the wasm. Because we need streaming instantiation.

Comment thread src/native/libs/Common/JavaScript/host/assets.ts Outdated
Keep boot-config allocation sizes and streaming instantiation for IL-only and R2R images. Remove duplicated runtime layout validation and verify active images and passive component stubs through the existing compiler tests. Document the matching-artifact contract.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a519827b-7ffd-4499-92f6-8f94d61ea940
Copilot AI review requested due to automatic review settings September 23, 2026 16:46

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.

Comment thread src/coreclr/hosts/corerun/wasm/libCorerun.js
Comment thread src/native/libs/Common/JavaScript/host/assets.ts Outdated
Comment thread src/tasks/Microsoft.NET.WebAssembly.Webcil/WasmModuleReader.cs
Consume the explicit memory index for data-segment mode 2 before reading the offset expression. Cover metadata/MVID extraction and stale-image rejection with zero and multi-byte memory indices.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a519827b-7ffd-4499-92f6-8f94d61ea940
Copilot AI review requested due to automatic review settings September 23, 2026 17:26

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.

@pavelsavara

Copy link
Copy Markdown
Member

Would you be comfortable separating the coordinated IL-only/Mono migration from this R2R change

This PR is adding more complexity to CoreCLR host, but it should be removing it.
Let's not have getWebcilPayload and fillWebcilTable in CoreCLR at all.
It's ok to bump the webCIL version.

I'm ok to leave the Mono on copy of old ConvertDllsToWebcil.

@lewing

lewing commented Sep 25, 2026

Copy link
Copy Markdown
Member Author

Let's not have getWebcilPayload and fillWebcilTable in CoreCLR at all.
It's ok to bump the webCIL version.

Thanks, that settles it. Moving forward in this PR with:

  • Wrapper webcilVersion 2 for all CoreCLR WebCIL. IL-only, R2R, and composite component stubs all use an active payload segment at __memory_base, plus an active element segment at __table_base where there is a table.
  • No manual installation in CoreCLR. getWebcilPayload/fillWebcilTable are removed from WebCilObjectWriter, the CoreCLR IL-only wrapper, corerun, and browserhost. Hosts still reserve the aligned ranges from payloadSize/tableSize (keeping streaming), then instantiate. patchWebcilHeader remains only where an image has a table base to record.
  • Explicit versioning instead of import aliases. CoreCLR hosts reject other wrapper versions, so an older v1 R2R image fails with a clear version error.
  • Mono unchanged. It keeps the existing passive version-0 wrapper; its native loader reads the payload from passive data segment 1.

One follow-up for #133265: its WASI composer uses passive vs. active to tell component stubs from composites, so it will need to classify them another way.

Note

This comment was drafted with GitHub Copilot.

Emit every CoreCLR WebCIL image - IL-only, R2R, composite, and composite component stubs - with an active payload segment at __memory_base and, where there is a table, an active element segment at __table_base. Remove getWebcilPayload and fillWebcilTable from the R2R writer, the CoreCLR IL-only wrapper, corerun, and the browser host; hosts now require webcilVersion 2 and call patchWebcilHeader only for R2R images. Allocation from payloadSize/tableSize and streaming instantiation are unchanged. Mono keeps the passive version 0 wrapper.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a519827b-7ffd-4499-92f6-8f94d61ea940
@lewing lewing changed the title [wasm] Emit self-installing WebCIL R2R images [wasm] Make CoreCLR WebCIL images self-installing (wrapper version 2) Sep 25, 2026
@lewing
lewing enabled auto-merge (squash) September 28, 2026 14:30
@lewing
lewing merged commit b965265 into dotnet:main Sep 28, 2026
154 of 156 checks passed
lewing added a commit that referenced this pull request Sep 29, 2026
## Summary

Enable composite ReadyToRun (R2R) publishing for CoreCLR on WASI. This
builds on the self-installing WebCIL R2R images from #134312, which is
now in `main`.

Per review feedback, this PR covers publishing and composition only. The
WASI R2R runtime-test and trimmed library-test CI lanes, their test
harness plumbing, and the related library test quarantines are in
#134813, which is stacked on this PR.

## Implementation

- The WASI app builder compiles the app and framework closure into a
composite R2R image plus per-assembly forwarding stubs using Crossgen2.
The per-app WASI host (`wasihost`) reserves an image buffer and
function-table slice sized to that composite, and exports the symbols
the composite imports.
- A C# `ComposeWasiReadyToRun` WasmAppBuilder task composes the
self-installing composite into the linked host component. It reuses the
WebCIL reader infrastructure, and uses `wasm-tools` and Binaryen for the
post-link merge and global folding. Crossgen2's finished R2R Wasm is not
a relocatable `wasm-ld` input, so this step cannot move into the native
link. Before merging, the task checks the image buffer size and the
table reservation against the host's exported values.
- The `wasihost` external assembly probe serves the embedded composite
and the per-assembly WebCIL stubs that are extracted at build time.
- In-tree publishing acquires pinned `wasm-tools`/Binaryen versions into
the shared wasm tool cache (`eng/AcquireWasiR2RTools.targets`).
- Docs: `docs/workflow/building/coreclr/wasi-r2r.md` and a WASI host
composition section in `docs/design/mono/webcil.md`.

## Validation

- `./build.sh -s clr+libs+packs -os wasi -arch wasm -c Release`: 0
warnings, 0 errors.
- Clean `PublishTrimmed` + `PublishReadyToRun=true` publish of
`src/mono/sample/wasi/console` with an empty tool cache. The pinned
tools were acquired, composition succeeded, and the app ran under
wasmtime. `DOTNET_ReadyToRunLogFile` showed `Ready to Run initialized
successfully` for all six loaded assemblies (CoreLib, the app,
System.Runtime, System.Console, System.Threading,
System.Runtime.InteropServices).
- A non-R2R CoreCLR WASI publish of the same sample still links against
the weak placeholder buffer and runs.

Related: #130129. Follow-up: #134813.

> [!NOTE]
> This pull request description was prepared with assistance from GitHub
Copilot.

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 919fdde7-f547-4bfb-86ad-7ac02c626980
@dotnet-milestone-bot dotnet-milestone-bot Bot added this to the 12.0-preview1 milestone Sep 29, 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.

4 participants