Skip to content

[browser][coreCLR] browserhost to load R2R#129634

Draft
pavelsavara wants to merge 24 commits into
dotnet:mainfrom
pavelsavara:browsehost_load_r2r
Draft

[browser][coreCLR] browserhost to load R2R#129634
pavelsavara wants to merge 24 commits into
dotnet:mainfrom
pavelsavara:browsehost_load_r2r

Conversation

@pavelsavara

Copy link
Copy Markdown
Member

No description provided.

@pavelsavara pavelsavara added this to the 11.0.0 milestone Jun 19, 2026
@pavelsavara pavelsavara self-assigned this Jun 19, 2026
Copilot AI review requested due to automatic review settings June 19, 2026 19:59
@pavelsavara pavelsavara added arch-wasm WebAssembly architecture area-Host os-browser Browser variant of arch-wasm labels Jun 19, 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 extends the WebAssembly build/publish pipeline to support ReadyToRun (R2R) “webcil-in-wasm” assemblies by staging prebuilt R2R .wasm images, emitting webcil payload/table sizing into the boot config, and updating the JS loader/host to instantiate webcil modules using those sizes (enabling streaming instantiation and avoiding runtime-side wasm parsing).

Changes:

  • Add an MSBuild path to crossgen selected assemblies to R2R webcil-in-wasm and feed those artifacts into webcil conversion/staging.
  • Emit payloadSize (and tableSize for R2R) into the boot config assets so the loader can instantiate without calling getWebcilSize / parsing wasm.
  • Update the JS loader/host surface and typings to pass these sizes through to instantiateWebcilModule.

Reviewed changes

Copilot reviewed 15 out of 16 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/GenerateWasmBootJson.cs Plumbs webcil size metadata into boot config generation and passes it to asset transformation.
src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/ConvertDllsToWebCil.cs Adds optional staging of prebuilt R2R .wasm replacements and emits a WebcilSizes output by parsing wasm.
src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonData.cs Extends boot asset schema with tableSize/payloadSize fields.
src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonBuilderHelper.cs Writes payloadSize/tableSize onto boot assets during resources→assets transformation.
src/native/libs/Common/JavaScript/types/public-api.ts Exposes tableSize/payloadSize on AssemblyAsset in public TS types.
src/native/libs/Common/JavaScript/types/internal.ts Adds internal asset fields for tableSize/payloadSize.
src/native/libs/Common/JavaScript/types/ems-ambient.ts Extends ambient emscripten symbol typing for wasm exports needed by R2R webcil imports.
src/native/libs/Common/JavaScript/loader/dotnet.d.ts Updates loader .d.ts for AssemblyAsset to include tableSize/payloadSize.
src/native/libs/Common/JavaScript/loader/assets.ts Passes tableSize/payloadSize to browser-host instantiateWebcilModule.
src/native/libs/Common/JavaScript/host/assets.ts Reworks instantiateWebcilModule to allocate payload and instantiate webcil wasm (streaming when possible) using boot-config sizes.
src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets Wires R2R candidates into ConvertDllsToWebcil, propagates WebcilSizes, and adds a _WasmCrossgenReadyToRunAssemblies target.
src/mono/browser/build/WasmApp.InTree.props Defines in-tree crossgen2/jit/reference paths consumed by the new R2R target.
src/mono/sample/wasm/browser-R2R/Wasm.Browser.R2R.csproj Adds a new sample that opts its app assembly into R2R cross-compilation.
src/mono/sample/wasm/browser-R2R/Program.cs Sample app entrypoint.
src/mono/sample/wasm/browser-R2R/wwwroot/index.html Sample web page host.
src/mono/sample/wasm/browser-R2R/wwwroot/main.js Sample boot/run script using dotnet.js.

Comment thread src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/ConvertDllsToWebCil.cs Outdated
Comment thread src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/ConvertDllsToWebCil.cs Outdated
Comment thread src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/GenerateWasmBootJson.cs Outdated
Comment thread src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonData.cs
Comment thread src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/ConvertDllsToWebCil.cs Outdated
Comment thread src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/ConvertDllsToWebCil.cs Outdated
Comment thread src/native/libs/Common/JavaScript/types/public-api.ts
Copilot AI review requested due to automatic review settings June 20, 2026 10:59

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

Copilot reviewed 18 out of 19 changed files in this pull request and generated 5 comments.

Comment on lines +258 to +265
// Key by the logical assembly name (".dll"), not the produced ".wasm" file name: the boot
// config lists webcil assemblies under their logical ".dll" name, and GenerateWasmBootJson
// looks these sizes up by that name (resourceName). Using ".wasm" here would never match.
// Keying by ".dll" also avoids colliding with same-stem assets (e.g. a "X.pdb" never matches
// "X.dll").
string fileName = Path.ChangeExtension(Path.GetFileName(webcilPath), ".dll");
string key = string.IsNullOrEmpty(culture) ? fileName : culture + "/" + fileName;
var item = new TaskItem(key);
Comment on lines +49 to +55
export async function instantiateWebcilModule(webcilPromise: Promise<Response>, memory: WebAssembly.Memory, virtualPath: string, tableSize?: number, payloadSize?: number): Promise<void> {
// The boot config carries payloadSize for every webcil asset (and tableSize for R2R images), so
// the loader never buffers the bytes, parses the data section or calls getWebcilSize. Assets
// without a tableSize are plain (Webcil wrapper version 0) images.
if (typeof payloadSize !== "number" || payloadSize === 0) {
throw new Error(`Webcil asset '${virtualPath}' is missing payloadSize in the boot config.`);
}
Comment on lines +424 to +433
<ConvertDllsToWebcil
Candidates="@(_WasmDllBuildCandidates)"
IntermediateOutputPath="$(_WasmBuildTmpWebcilPath)"
OutputPath="$(_WasmBuildWebcilPath)"
IsEnabled="$(_WasmEnableWebcil)"
WebcilVersion="$(_WasmWebcilVersion)"
R2RWebcilCandidates="@(WasmR2RWebcilCandidate)">
<Output TaskParameter="FileWrites" ItemName="FileWrites" />
<Output TaskParameter="FileWrites" ItemName="_WasmConvertedWebcilOutputs" />
<Output TaskParameter="WebcilSizes" ItemName="_WasmWebcilSizes" />
Comment on lines +89 to +94
/// <summary>
/// Payload/table sizes for each webcil (keyed by the logical assembly name, e.g. "App.dll"),
/// produced by ConvertDllsToWebcil. Emitted into the boot config so the runtime loader doesn't
/// parse the wasm or call getWebcilSize.
/// </summary>
public ITaskItem[] WebcilSizes { get; set; }
Comment on lines +40 to +47
/// <summary>
/// Payload/table sizes for each produced webcil, keyed by the logical assembly name as it
/// appears in the boot config: the file name (e.g. "System.Console.dll"), or
/// "{culture}/{name}.dll" for satellite assemblies so that same-named satellites in different
/// cultures don't collide. Lets the boot config carry the sizes so the runtime loader doesn't
/// buffer/parse the wasm. PayloadSize is set for every webcil; TableSize is non-zero only for
/// R2R images.
/// </summary>
Copilot AI review requested due to automatic review settings June 23, 2026 13: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.

Pull request overview

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

Comment on lines 339 to 342
extern "C" UINT_PTR STDCALL GetCurrentIP(void)
{
PORTABILITY_ASSERT("GetCurrentIP is not implemented on wasm");
// WASM-TODO: Implement this function to return the current instruction pointer in the interpreter.
return 0;
Comment on lines +326 to +336
int want = dataLength >= 8 ? 8 : 4;
byte[] sizes = new byte[8];
if (!TryFill(fs, sizes, want))
{
failureReason = "data segment 0 was truncated before the sizes could be read";
return false;
}

payloadSize = (int)ReadUInt32LE(sizes, 0);
tableSize = want == 8 ? (int)ReadUInt32LE(sizes, 4) : 0;
return true;
Comment on lines +6 to +20
function displayMeaning(meaning) {
console.log(`Meaning of life is ${meaning}`);
document.getElementById("out").innerHTML = `${meaning}`;
}

function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}

try {
const { setModuleImports, getAssemblyExports } = await dotnet
.withConfig({ appendElementOnExit: true, exitOnUnhandledError: true, forwardConsole: true, logExitCode: true })
.withDiagnosticTracing(true)
.withApplicationArguments("--meaning", "42")
.create();
…thods

CallDescrWorkerInternal (the wasm reflection/CallDescr invoke path) resolved the
interpreter byte code via MethodDesc::GetInterpreterCode() and, after a DoPrestub
retry, passed the pointer straight to ExecuteInterpretedMethodWithArgs. When the
target method has no interpreter byte code because it was compiled to native (R2R)
code, GetInterpreterCode() returns NULL. On wasm a NULL pointer reads zeroed low
linear memory instead of faulting, so the interpreter dispatched the NULL byte code
as INTOP_INVALID and aborted with Unimplemented or invalid interpreter opcode
plus a secondary stackwalk assert.

Add the InvokeManagedMethod fallback (the interpreter->R2R thunk) when there is no
interpreter code, mirroring the existing pattern in
ExecuteInterpretedMethodWithArgs_PortableEntryPoint_Complex and the
CALL_INTERP_METHOD path in InterpExecMethod.

Also harden the sibling entry points: ExecuteInterpretedMethodFromUnmanaged now
takes the same fallback, and ExecuteInterpretedMethodWithArgs asserts a non-NULL
targetIp so future misuse fails loudly at the point of error.
pavelsavara and others added 5 commits June 24, 2026 11:18
Address PR dotnet#129766 review feedback: resolve R2R-compiled native code for

UnmanagedCallersOnly methods directly instead of going through the interpreter

reverse-thunk fallback.

- GetUnmanagedCallersOnlyThunk (wasm/helpers.cpp): run DoPrestub under GCX_PREEMP

  to publish code, then return the native (R2R) entrypoint via PortableEntryPoint

  when present; the g_ReverseThunks fallback is only for interpreted methods.

- ExecuteInterpretedMethodFromUnmanaged (prestub.cpp): R2R methods are now

  dispatched directly and never reach this path, so replace the InvokeManagedMethod

  fallback with an assert that the interpreter byte code exists.

- Widen contracts for the prestub-on-resolve path: EnsureCodeForUnmanagedCallersOnly

  (precode_portable.cpp) is THROWS/GC_TRIGGERS/MODE_PREEMPTIVE;

  GetSingleCallableAddrOfCodeForUnmanagedCallersOnly (method.cpp) is GC_TRIGGERS

  under FEATURE_PORTABLE_ENTRYPOINTS.
8-byte locals were only 4-byte aligned on wasm, so JS interop reads through
the 8-byte HEAP64/HEAPF64 views landed on the wrong word. Align >=8 byte
locals to 8 and round the frame to STACK_ALIGN. Fixes dotnet#129802.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 25, 2026 11:38

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

Copilot reviewed 24 out of 25 changed files in this pull request and generated 4 comments.

Comment on lines +144 to +150
if (!int.TryParse(s.GetMetadata("PayloadSize"), NumberStyles.Integer, CultureInfo.InvariantCulture, out int ps) || ps <= 0)
{
Log.LogError($"Webcil asset '{s.ItemSpec}' has missing or invalid PayloadSize metadata; the runtime loader requires it.");
continue;
}
int.TryParse(s.GetMetadata("TableSize"), NumberStyles.Integer, CultureInfo.InvariantCulture, out int ts);
webcilSizeByName[s.ItemSpec] = (ts, ps);
Comment on lines +23 to +25
<!-- ReadyToRun (R2R) for WebAssembly: tool/reference paths consumed by the SDK's
_WasmCrossgenReadyToRunAssemblies target when a project opts in via @(WasmReadyToRunAssembly).
These point at the in-tree build outputs; a shipping SDK would resolve crossgen2 from its pack. -->
Comment on lines +20 to +22
<!-- Opt the app assembly into ReadyToRun (R2R) cross-compilation. The WebAssembly SDK crossgens
each listed assembly to an R2R webcil-in-wasm image before webcil conversion. See the
_WasmCrossgenReadyToRunAssemblies target in Microsoft.NET.Sdk.WebAssembly.Browser.targets. -->
Comment on lines +400 to +414
/// <summary>
/// For ReadyToRun (R2R) webcil-in-wasm images: the number of table entries the module needs.
/// When present (non-null) the loader grows the table before instantiation. Only R2R images set
/// this; it is omitted for plain (non-R2R) webcil.
/// </summary>
[DataMember(EmitDefaultValue = false)]
public int? tableSize { get; set; }

/// <summary>
/// The size in bytes of the Webcil payload to allocate before instantiation. Emitted for every
/// webcil-in-wasm assembly (the loader requires it to avoid parsing the wasm data section), not
/// just R2R images. For R2R images it is paired with <see cref="tableSize"/>.
/// </summary>
[DataMember(EmitDefaultValue = false)]
public int? payloadSize { get; set; }

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

Copilot reviewed 29 out of 30 changed files in this pull request and generated 4 comments.

Comment on lines 149 to 152
WebcilCandidates = webcilCandidates.ToArray();
PassThroughCandidates = passThroughCandidates.ToArray();
WebcilSizes = _webcilSizes.ToArray();
return true;
Comment on lines +177 to +183
CONTRACTL
{
THROWS;
GC_TRIGGERS;
MODE_PREEMPTIVE;
}
CONTRACTL_END;
Comment on lines +988 to +991
<PropertyGroup>
<_WasmCrossgen2NuGetRoot Condition="'$(_WasmCrossgen2NuGetRoot)' == ''">$(NuGetPackageRoot)</_WasmCrossgen2NuGetRoot>
<_WasmCrossgen2NuGetRoot Condition="'$(_WasmCrossgen2NuGetRoot)' == ''">$(NuGetPackageFolders)</_WasmCrossgen2NuGetRoot>
</PropertyGroup>
Comment on lines +1076 to +1085
<Error Condition="'$(_WasmCrossgen2Path)' == '' or !Exists('$(_WasmCrossgen2Path)')"
Text="ReadyToRun for WebAssembly requires crossgen2, but it was not found at '$(_WasmCrossgen2Path)'. Set the %24(_WasmCrossgen2Path) property." />
<Error Condition="'$(_WasmCrossgen2JitPath)' == '' or !Exists('$(_WasmCrossgen2JitPath)')"
Text="ReadyToRun for WebAssembly requires the wasm JIT, but it was not found at '$(_WasmCrossgen2JitPath)'. Set the %24(_WasmCrossgen2JitPath) property." />

<PropertyGroup>
<_WasmR2RLinkDir>$([MSBuild]::NormalizeDirectory($(IntermediateOutputPath), 'linked'))</_WasmR2RLinkDir>
<_WasmR2ROutDir>$([MSBuild]::NormalizeDirectory($(IntermediateOutputPath), 'r2r', 'publish'))</_WasmR2ROutDir>
<_WasmR2RRspPath>$(_WasmR2ROutDir)crossgen2.rsp</_WasmR2RRspPath>
</PropertyGroup>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

arch-wasm WebAssembly architecture area-Host os-browser Browser variant of arch-wasm

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants