diff --git a/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets
index e0bbf9e31fbcfd..81b488facf4b2e 100644
--- a/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets
+++ b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets
@@ -423,6 +423,7 @@ Copyright (c) .NET Foundation. All rights reserved.
+
$(Nested_RuntimeFlavor)
+ $(Nested_PublishReadyToRun)
diff --git a/src/mono/sample/wasm/Directory.Build.targets b/src/mono/sample/wasm/Directory.Build.targets
index 8c6ae37a6403a0..bd4f2de1aa2de6 100644
--- a/src/mono/sample/wasm/Directory.Build.targets
+++ b/src/mono/sample/wasm/Directory.Build.targets
@@ -33,6 +33,7 @@
+
diff --git a/src/native/libs/Common/JavaScript/host/assets.ts b/src/native/libs/Common/JavaScript/host/assets.ts
index ba3e9c048077c6..19e5847245d46f 100644
--- a/src/native/libs/Common/JavaScript/host/assets.ts
+++ b/src/native/libs/Common/JavaScript/host/assets.ts
@@ -46,50 +46,112 @@ export function registerDllBytes(bytes: Uint8Array, virtualPath: string, shortNa
}
}
-export async function instantiateWebcilModule(webcilPromise: Promise, memory: WebAssembly.Memory, virtualPath: string): Promise {
+export async function instantiateWebcilModule(webcilPromise: Promise, memory: WebAssembly.Memory, virtualPath: string, tableSize?: number, payloadSize?: number): Promise {
+ // 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.`);
+ }
+ const tableEntries = typeof tableSize === "number" ? tableSize : 0;
- const imports: WebAssembly.Imports = {
- webcil: {
- memory,
- }
- };
+ const res = await checkWebcilResponse(webcilPromise, virtualPath);
+ const payloadPtr = allocWebcilPayload(payloadSize);
+ const imports: WebAssembly.Imports = { webcil: buildWebcilImports(memory, payloadPtr, tableEntries) };
- const { instance } = await instantiateWasm(webcilPromise, imports);
- const webcilVersion = (instance.exports.webcilVersion as WebAssembly.Global).value;
- if (webcilVersion !== 0) {
- throw new Error(`Unsupported Webcil version: ${webcilVersion}`);
+ let instance: WebAssembly.Instance;
+ const contentType = res.headers && res.headers.get ? res.headers.get("Content-Type") : undefined;
+ const streamingOk = hasInstantiateStreaming && typeof globalThis.Response === "function" && res instanceof globalThis.Response && contentType === "application/wasm";
+ if (streamingOk) {
+ const instantiated = await WebAssembly.instantiateStreaming(res, imports);
+ instance = instantiated.instance;
+ } else {
+ const data = await res.arrayBuffer();
+ const instantiated = await WebAssembly.instantiate(data, imports);
+ instance = instantiated.instance;
+ }
+ finishWebcilInstance(instance, payloadPtr, payloadSize, tableEntries, virtualPath);
+}
+
+async function checkWebcilResponse(webcilPromise: Promise, virtualPath: string): Promise {
+ const res = await webcilPromise;
+ if (!res || res.ok === false) {
+ throw new Error(`Failed to load Webcil module '${virtualPath}'. HTTP status: ${(res as Response)?.status} ${(res as Response)?.statusText}`);
}
+ return res;
+}
+// Allocates a 16-byte-aligned buffer for the Webcil payload. The pointer is heap memory that
+// outlives the stack frame, so it can be passed as the imageBase import.
+function allocWebcilPayload(payloadSize: number): number {
const sp = _ems_.stackSave();
try {
- const sizePtr = _ems_.stackAlloc(sizeOfPtr);
- const getWebcilSize = instance.exports.getWebcilSize as (destPtr: number) => void;
- getWebcilSize(sizePtr as any);
- const payloadSize = _ems_.HEAPU32[sizePtr as any >>> 2];
-
- if (payloadSize === 0) {
- throw new Error("Webcil payload size is 0");
- }
-
const ptrPtr = _ems_.stackAlloc(sizeOfPtr);
if (_ems_._posix_memalign(ptrPtr as any, 16, payloadSize)) {
throw new Error("posix_memalign failed for Webcil payload");
}
+ return _ems_.HEAPU32[ptrPtr as any >>> 2];
+ } finally {
+ _ems_.stackRestore(sp);
+ }
+}
- const payloadPtr = _ems_.HEAPU32[ptrPtr as any >>> 2];
+// Builds the `webcil` import object. For R2R images (tableSize > 0) the module imports the runtime's
+// stack pointer, exception tag, indirect-call table and base globals; this also grows the table.
+// These import names and the webcilVersion/getWebcilPayload/fillWebcilTable handshake in
+// finishWebcilInstance are the R2R Webcil-in-Wasm host ABI defined by crossgen's WasmObjectWriter
+// (src/coreclr/tools/Common/Compiler/ObjectWriter/WasmObjectWriter.cs, CreateDefaultGlobalImports/
+// WriteExports). Keep in sync with the corerun host
+// (src/coreclr/hosts/corerun/wasm/libCorerun.js, BrowserHost_ExternalAssemblyProbe). Unlike corerun,
+// which parses data segment 0 for payloadSize/tableSize, this loader receives them from boot config.
+function buildWebcilImports(memory: WebAssembly.Memory, payloadPtr: number, tableSize: number): Record {
+ const webcilImports: Record = { memory };
+ if (tableSize > 0) {
+ const stackPointer = _ems_.wasmExports?.__stack_pointer;
+ const rtlRestoreContextTag = _ems_.wasmExports?.__coreclr_wasm_rtlrestorecontext_tag;
+ const asyncContinuation = _ems_.wasmExports?.__async_continuation;
+ if (!stackPointer) {
+ throw new Error("__stack_pointer was not preserved by the linker or optimizer");
+ }
+ if (!rtlRestoreContextTag) {
+ throw new Error("__coreclr_wasm_rtlrestorecontext_tag was not preserved by the linker or optimizer");
+ }
+ if (!asyncContinuation) {
+ throw new Error("__async_continuation was not preserved by the linker or optimizer");
+ }
+ const tableStartIndex = _ems_.wasmTable.length;
+ _ems_.wasmTable.grow(tableSize);
+ webcilImports.stackPointer = stackPointer;
+ webcilImports.rtlRestoreContextTag = rtlRestoreContextTag as unknown as WebAssembly.ImportValue;
+ webcilImports.asyncContinuation = asyncContinuation as unknown as WebAssembly.ImportValue;
+ webcilImports.table = _ems_.wasmTable;
+ webcilImports.tableBase = new WebAssembly.Global({ value: "i32", mutable: false }, tableStartIndex);
+ webcilImports.imageBase = new WebAssembly.Global({ value: "i32", mutable: false }, payloadPtr);
+ }
+ return webcilImports;
+}
- const getWebcilPayload = instance.exports.getWebcilPayload as (ptr: number, size: number) => void;
- getWebcilPayload(payloadPtr, payloadSize);
+// Copies the payload into the allocated buffer, fills the R2R table (if any) and registers the
+// loaded image for BrowserHost_ExternalAssemblyProbe.
+function finishWebcilInstance(instance: WebAssembly.Instance, payloadPtr: number, payloadSize: number, tableSize: number, virtualPath: string): void {
+ const webcilVersion = (instance.exports.webcilVersion as WebAssembly.Global).value;
+ if (webcilVersion > 1 || webcilVersion < 0) {
+ throw new Error(`Unsupported Webcil version: ${webcilVersion}`);
+ }
- const name = virtualPath.startsWith(browserVirtualAppBase)
- ? virtualPath.substring(browserVirtualAppBase.length)
- : virtualPath.substring(virtualPath.lastIndexOf("/") + 1);
- _ems_.dotnetLogger.debug(`Registered Webcil assembly '${virtualPath}' (name: '${name}') at ${payloadPtr.toString(16)} length ${payloadSize}`);
- loadedAssemblies.set(virtualPath, { ptr: payloadPtr, length: payloadSize });
- loadedAssemblies.set(name, { ptr: payloadPtr, length: payloadSize });
- } finally {
- _ems_.stackRestore(sp);
+ const getWebcilPayload = instance.exports.getWebcilPayload as (ptr: number, size: number) => void;
+ getWebcilPayload(payloadPtr, payloadSize);
+ if (tableSize > 0) {
+ const fillWebcilTable = instance.exports.fillWebcilTable as () => void;
+ fillWebcilTable();
}
+
+ const name = virtualPath.startsWith(browserVirtualAppBase)
+ ? virtualPath.substring(browserVirtualAppBase.length)
+ : virtualPath.substring(virtualPath.lastIndexOf("/") + 1);
+ _ems_.dotnetLogger.debug(`Registered Webcil assembly '${virtualPath}' (name: '${name}') at ${payloadPtr.toString(16)} length ${payloadSize}`);
+ loadedAssemblies.set(virtualPath, { ptr: payloadPtr, length: payloadSize });
+ loadedAssemblies.set(name, { ptr: payloadPtr, length: payloadSize });
}
export function BrowserHost_ExternalAssemblyProbe(pathPtr: CharPtr, outDataStartPtr: VoidPtrPtr, outSize: VoidPtr): boolean {
diff --git a/src/native/libs/Common/JavaScript/loader/assets.ts b/src/native/libs/Common/JavaScript/loader/assets.ts
index 5e82191316841f..eaf4508a140905 100644
--- a/src/native/libs/Common/JavaScript/loader/assets.ts
+++ b/src/native/libs/Common/JavaScript/loader/assets.ts
@@ -176,7 +176,7 @@ async function fetchWebcil(assetInternal: AssetEntryInternal): Promise {
const webcilPromise = loadResource(assetInternal);
const memory = await wasmMemoryPromiseController.promise;
- const instancePromise = dotnetBrowserHostExports.instantiateWebcilModule(webcilPromise, memory, assetInternal.virtualPath!);
+ const instancePromise = dotnetBrowserHostExports.instantiateWebcilModule(webcilPromise, memory, assetInternal.virtualPath!, assetInternal.tableSize, assetInternal.payloadSize);
await instancePromise;
} finally {
onDownloadedAsset(assetInternal);
diff --git a/src/native/libs/Common/JavaScript/types/ems-ambient.ts b/src/native/libs/Common/JavaScript/types/ems-ambient.ts
index 93be8641f71608..e4e21ebde9e684 100644
--- a/src/native/libs/Common/JavaScript/types/ems-ambient.ts
+++ b/src/native/libs/Common/JavaScript/types/ems-ambient.ts
@@ -104,4 +104,9 @@ export type EmsAmbientSymbolsType = EmscriptenModuleInternal & {
wasmMemory: WebAssembly.Memory;
wasmTable: WebAssembly.Table;
+ wasmExports: {
+ __stack_pointer: WebAssembly.Global;
+ __coreclr_wasm_rtlrestorecontext_tag: object;
+ [key: string]: unknown;
+ };
}
diff --git a/src/native/libs/Common/JavaScript/types/internal.ts b/src/native/libs/Common/JavaScript/types/internal.ts
index aaac89fc35e2f4..7307d336ee6772 100644
--- a/src/native/libs/Common/JavaScript/types/internal.ts
+++ b/src/native/libs/Common/JavaScript/types/internal.ts
@@ -63,6 +63,8 @@ export interface AssetEntryInternal extends AssetEntry {
priority?: boolean
shortName?: string
inprogress?: boolean
+ tableSize?: number
+ payloadSize?: number
}
export type LoaderConfigInternal = LoaderConfig & {
diff --git a/src/native/libs/Common/JavaScript/types/public-api.ts b/src/native/libs/Common/JavaScript/types/public-api.ts
index f6372e9ddba5b9..ce4f51ef9645ab 100644
--- a/src/native/libs/Common/JavaScript/types/public-api.ts
+++ b/src/native/libs/Common/JavaScript/types/public-api.ts
@@ -252,6 +252,20 @@ export type AssemblyAsset = Asset & {
name: string;
hash?: string | null | "";
};
+export type WebcilAsset = AssemblyAsset & {
+ /**
+ * The size in bytes of the Webcil payload to allocate. Present for every Webcil-in-wasm
+ * assembly; the runtime uses it to instantiate the image without buffering its bytes or parsing
+ * the wasm data section.
+ */
+ payloadSize?: number;
+ /**
+ * For ReadyToRun (R2R) webcil-in-wasm images only: the number of table entries the module needs.
+ * The runtime grows the indirect-call table by this amount before instantiation. Absent for
+ * plain (non-R2R) webcil.
+ */
+ tableSize?: number;
+};
export type PdbAsset = Asset & {
virtualPath: string;
name: string;
diff --git a/src/native/libs/System.Native.Browser/libSystem.Native.Browser.Utils.footer.js b/src/native/libs/System.Native.Browser/libSystem.Native.Browser.Utils.footer.js
index 4b45221eeca9c7..9a03c405feb09d 100644
--- a/src/native/libs/System.Native.Browser/libSystem.Native.Browser.Utils.footer.js
+++ b/src/native/libs/System.Native.Browser/libSystem.Native.Browser.Utils.footer.js
@@ -25,6 +25,8 @@ function libBrowserUtilsFactory() {
"abort",
"__trap",
"__stack_pointer",
+ "__coreclr_wasm_rtlrestorecontext_tag",
+ "__async_continuation",
"$readI53FromU64",
"$readI53FromI64",
"$writeI53ToI64"
diff --git a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonBuilderHelper.cs b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonBuilderHelper.cs
index f47dd997750eb0..6144836299eb1d 100644
--- a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonBuilderHelper.cs
+++ b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonBuilderHelper.cs
@@ -213,7 +213,7 @@ public int GetDebugLevel(bool hasPdb)
return intValue;
}
- public string TransformResourcesToAssets(BootJsonData config, bool bundlerFriendly = false)
+ public string TransformResourcesToAssets(BootJsonData config, bool bundlerFriendly = false, Dictionary? webcilSizes = null)
{
List imports = [];
@@ -297,6 +297,20 @@ public string TransformResourcesToAssets(BootJsonData config, bool bundlerFriend
cache = GetCacheControl(a.Key, resources)
};
+ // Webcil payload/table sizes. For satellites (subFolder == culture) the key is
+ // culture-qualified to match GenerateWasmBootJson's store key and disambiguate
+ // same-named satellites across cultures.
+ if (webcilSizes != null)
+ {
+ string r2rKey = subFolder != null ? subFolder + "/" + a.Key : a.Key;
+ if (webcilSizes.TryGetValue(r2rKey, out var sizes))
+ {
+ asset.payloadSize = sizes.payloadSize;
+ if (sizes.tableSize > 0)
+ asset.tableSize = sizes.tableSize;
+ }
+ }
+
if (bundlerFriendly)
{
string escaped = EscapeName(string.Concat(subFolder, a.Key));
diff --git a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonData.cs b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonData.cs
index f6fe170d54ac1b..9205cf85124a56 100644
--- a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonData.cs
+++ b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonData.cs
@@ -394,6 +394,22 @@ public class GeneralAsset
public string hash { get; set; }
public string resolvedUrl { get; set; }
public string cache { get; set; }
+
+ ///
+ /// 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.
+ ///
+ [DataMember(EmitDefaultValue = false)]
+ public int? tableSize { get; set; }
+
+ ///
+ /// 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 .
+ ///
+ [DataMember(EmitDefaultValue = false)]
+ public int? payloadSize { get; set; }
}
[DataContract]
diff --git a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/ConvertDllsToWebCil.cs b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/ConvertDllsToWebCil.cs
index 582bd7d3098093..ceff9f3ea1b79f 100644
--- a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/ConvertDllsToWebCil.cs
+++ b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/ConvertDllsToWebCil.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.IO;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
@@ -28,6 +29,9 @@ public class ConvertDllsToWebcil : Task
[Output]
public ITaskItem[] WebcilCandidates { get; set; }
+ [Output]
+ public ITaskItem[] WebcilSizes { get; set; }
+
///
/// Files from shared locations (runtime pack, NuGet cache) that need Framework
/// SourceType materialization to get unique per-project Identity.
@@ -43,6 +47,8 @@ public class ConvertDllsToWebcil : Task
protected readonly List _fileWrites = new();
+ private readonly List _webcilSizes = new();
+
[Output]
public string[]? FileWrites => _fileWrites.ToArray();
@@ -118,6 +124,7 @@ public override bool Execute()
WebcilCandidates = webcilCandidates.ToArray();
PassThroughCandidates = passThroughCandidates.ToArray();
+ WebcilSizes = _webcilSizes.ToArray();
return true;
}
@@ -125,13 +132,37 @@ private TaskItem ConvertDll(string tmpDir, ITaskItem candidate)
{
var dllFilePath = candidate.ItemSpec;
var webcilFileName = Path.GetFileNameWithoutExtension(dllFilePath) + Utils.WebcilInWasmExtension;
- string candidatePath = candidate.GetMetadata("AssetTraitName") == "Culture"
- ? Path.Combine(OutputPath, candidate.GetMetadata("AssetTraitValue"))
+ bool isCulture = candidate.GetMetadata("AssetTraitName") == "Culture";
+ string culture = isCulture ? candidate.GetMetadata("AssetTraitValue") : null;
+ string candidatePath = isCulture
+ ? Path.Combine(OutputPath, culture)
: OutputPath;
string finalWebcil = Path.Combine(candidatePath, webcilFileName);
- if (Utils.IsNewerThan(dllFilePath, finalWebcil))
+ // A prebuilt R2R webcil-in-wasm image from the runtime pack replaces conversion of the .dll:
+ // stage (copy) it into the webcil output so it flows through the same downstream metadata as a
+ // converted assembly, but carries native code. The .dll is kept only as the metadata source.
+ string r2rWebcilPath = candidate.GetMetadata("R2RWebcilPath");
+ if (!string.IsNullOrEmpty(r2rWebcilPath))
+ {
+ if (Utils.IsNewerThan(r2rWebcilPath, finalWebcil))
+ {
+ if (!Directory.Exists(candidatePath))
+ Directory.CreateDirectory(candidatePath);
+
+ // Copy (not move): the runtime pack's native/*.wasm is a shared source that must survive staging.
+ if (Utils.CopyIfDifferent(r2rWebcilPath, finalWebcil, useHash: false))
+ Log.LogMessage(MessageImportance.Low, $"Staged prebuilt R2R webcil {finalWebcil} from {r2rWebcilPath} .");
+ else
+ Log.LogMessage(MessageImportance.Low, $"Skipped staging {finalWebcil} as the contents are unchanged.");
+ }
+ else
+ {
+ Log.LogMessage(MessageImportance.Low, $"Skipping {r2rWebcilPath} as it is older than the output file {finalWebcil}");
+ }
+ }
+ else if (Utils.IsNewerThan(dllFilePath, finalWebcil))
{
var tmpWebcil = Path.Combine(tmpDir, webcilFileName);
var logAdapter = new Microsoft.WebAssembly.Build.Tasks.LogAdapter(Log);
@@ -165,6 +196,145 @@ private TaskItem ConvertDll(string tmpDir, ITaskItem candidate)
Log.LogMessage(MessageImportance.Low, $"Changing related asset of {webcilItem} to {relatedAsset}.");
}
+ RecordWebcilSize(finalWebcil, culture);
return webcilItem;
}
+
+ // Parses the produced webcil's data segment 0 and records payloadSize/tableSize keyed by the
+ // produced webcil-in-wasm file name (".wasm", or "{culture}/{name}.wasm" for satellites) so that
+ // GenerateWasmBootJson can emit them into the boot config without re-parsing. The runtime loader
+ // requires payloadSize for every webcil-in-wasm assembly, so failing to read it is a build error
+ // rather than a silent skip.
+ private void RecordWebcilSize(string webcilPath, string culture)
+ {
+ if (!TryReadWebcilSizes(webcilPath, out int payloadSize, out int tableSize, out string failureReason))
+ {
+ Log.LogError($"Could not read the Webcil payload/table sizes from '{webcilPath}' ({failureReason}). The runtime loader requires payloadSize for every webcil-in-wasm assembly.");
+ return;
+ }
+
+ // Key by the produced webcil-in-wasm file name (".wasm"): GenerateWasmBootJson derives its
+ // lookup key from each asset's OriginalItemSpec, which is the produced ".wasm" path, so
+ // keying by ".dll" here would never match and payloadSize/tableSize would never be emitted.
+ // Satellites share a file name across cultures, so qualify by culture to avoid collisions.
+ string fileName = Path.GetFileName(webcilPath);
+ string key = string.IsNullOrEmpty(culture) ? fileName : culture + "/" + fileName;
+ var item = new TaskItem(key);
+ item.SetMetadata("PayloadSize", payloadSize.ToString(CultureInfo.InvariantCulture));
+ item.SetMetadata("TableSize", tableSize.ToString(CultureInfo.InvariantCulture));
+ _webcilSizes.Add(item);
+ }
+
+ // Reads payloadSize and tableSize from data segment 0 of a Webcil-in-wasm image without
+ // instantiating it. tableSize > 0 indicates a ReadyToRun image. The data section is the last
+ // wasm section, so for R2R images (large code sections) it can start well beyond the first few
+ // KB; this streams through the section headers, seeking past each body, instead of reading a
+ // fixed prefix. All multi-byte integers in the wasm binary format are little-endian and are read
+ // as such regardless of host endianness. See docs/design/mono/webcil.md.
+ internal static bool TryReadWebcilSizes(string path, out int payloadSize, out int tableSize, out string failureReason)
+ {
+ payloadSize = 0;
+ tableSize = 0;
+ failureReason = null;
+ try
+ {
+ using var fs = File.OpenRead(path);
+
+ byte[] header = new byte[8];
+ if (!TryFill(fs, header, 8)
+ || ReadUInt32LE(header, 0) != 0x6d736100 /* \0asm */
+ || ReadUInt32LE(header, 4) != 1 /* wasm version */)
+ {
+ failureReason = "not a WebAssembly module (missing '\\0asm' magic or unexpected version)";
+ return false;
+ }
+
+ while (true)
+ {
+ int sectionCode = fs.ReadByte();
+ if (sectionCode < 0)
+ {
+ failureReason = "reached end of file without finding a data section";
+ return false;
+ }
+ if (!TryReadULEB128(fs, out uint sectionSize))
+ {
+ failureReason = "malformed section size (truncated ULEB128)";
+ return false;
+ }
+
+ if (sectionCode == 11 /* data section */)
+ {
+ if (!TryReadULEB128(fs, out uint segmentCount) || segmentCount < 1)
+ {
+ failureReason = "data section has no segments";
+ return false;
+ }
+ if (fs.ReadByte() != 1 /* passive segment */)
+ {
+ failureReason = "data segment 0 is not a passive segment";
+ return false;
+ }
+ if (!TryReadULEB128(fs, out uint dataLength) || dataLength < 4)
+ {
+ failureReason = "data segment 0 is too small to hold a payload size";
+ return false;
+ }
+
+ 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;
+ }
+
+ fs.Seek(sectionSize, SeekOrigin.Current);
+ }
+ }
+ catch (Exception ex)
+ {
+ failureReason = ex.Message;
+ return false;
+ }
+ }
+
+ private static bool TryFill(Stream stream, byte[] buffer, int count)
+ {
+ int read = 0;
+ while (read < count)
+ {
+ int r = stream.Read(buffer, read, count - read);
+ if (r == 0)
+ return false;
+ read += r;
+ }
+ return true;
+ }
+
+ private static uint ReadUInt32LE(byte[] bytes, int offset)
+ => (uint)(bytes[offset] | (bytes[offset + 1] << 8) | (bytes[offset + 2] << 16) | (bytes[offset + 3] << 24));
+
+ private static bool TryReadULEB128(Stream stream, out uint value)
+ {
+ value = 0;
+ int shift = 0;
+ while (true)
+ {
+ if (shift >= 35)
+ return false;
+ int b = stream.ReadByte();
+ if (b < 0)
+ return false;
+ value |= (uint)(b & 0x7f) << shift;
+ if ((b & 0x80) == 0)
+ return true;
+ shift += 7;
+ }
+ }
}
diff --git a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/GenerateWasmBootJson.cs b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/GenerateWasmBootJson.cs
index 784b2a77d71755..0a87f54672dab7 100644
--- a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/GenerateWasmBootJson.cs
+++ b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/GenerateWasmBootJson.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
+using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
@@ -89,6 +90,8 @@ public class GenerateWasmBootJson : Task
public bool FingerprintAssets { get; set; }
+ public ITaskItem[] WebcilSizes { get; set; }
+
public string ApplicationEnvironment { get; set; }
public string MergeWith { get; set; }
@@ -124,6 +127,28 @@ private void WriteBootConfig(string entryAssemblyName)
bool isMonoRuntime = string.IsNullOrEmpty(UseMonoRuntime) || string.Equals(UseMonoRuntime, "true", StringComparison.OrdinalIgnoreCase);
var helper = new BootJsonBuilderHelper(Log, DebugLevel, IsMultiThreaded, IsPublish, ParsedTargetFrameworkVersion, isMonoRuntime);
+ // ReadyToRun webcil-in-wasm images carry payload/table sizes that the loader needs before
+ // instantiation. Record them (keyed by fingerprinted route) so they can be emitted into the
+ // boot config, letting the loader stream-instantiate instead of buffering and parsing.
+ var webcilSizes = new Dictionary();
+
+ // Webcil sizes computed by ConvertDllsToWebcil, keyed by the produced webcil-in-wasm file
+ // name ("{name}.wasm", or "{culture}/{name}.wasm" for satellites so cultures don't collide).
+ var webcilSizeByName = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ if (WebcilSizes != null)
+ {
+ foreach (var s in WebcilSizes)
+ {
+ 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);
+ }
+ }
+
var result = new BootJsonData
{
resources = new ResourcesData(),
@@ -231,6 +256,15 @@ private void WriteBootConfig(string entryAssemblyName)
var resourceEndpoint = endpointByAsset[resource.ItemSpec].ItemSpec;
var resourceRoute = Path.GetFileName(resourceEndpoint);
+ // Keys for looking up the webcil payload/table sizes (see below). Satellites share a
+ // file name across cultures, so qualify by culture to avoid collisions: the lookup key
+ // matches ConvertDllsToWebcil's WebcilSizes ItemSpec (the produced ".wasm" file name),
+ // and the store key matches how BootJsonBuilderHelper resolves webcilSizes per (culture
+ // subfolder, route).
+ string webcilCulture = string.Equals("Culture", assetTraitName, StringComparison.OrdinalIgnoreCase) ? assetTraitValue : null;
+ string webcilSizeLookupKey = webcilCulture != null ? webcilCulture + "/" + resourceName : resourceName;
+ string r2rSizeStoreKey = webcilCulture != null ? webcilCulture + "/" + resourceRoute : resourceRoute;
+
if (TryGetLazyLoadedAssembly(lazyLoadAssembliesWithoutExtension, resourceName, out var lazyLoad))
{
MapFingerprintedAsset(resourceData, resourceRoute, resourceName);
@@ -389,6 +423,42 @@ private void WriteBootConfig(string entryAssemblyName)
if (resourceList != null)
{
AddResourceToList(resource, resourceList, resourceRoute);
+
+ // Webcil-in-wasm assemblies (startup, lazy, satellite) carry payload/table sizes
+ // so the runtime loader can instantiate without parsing the wasm. payloadSize is
+ // emitted for every webcil; tableSize only for R2R. Identify them by the produced
+ // ".wasm" extension, excluding native wasm (dotnet.native.wasm) which is handled
+ // separately and is not a webcil module.
+ bool isWebcilInWasmAssembly = IsTargeting100OrLater()
+ && string.Equals(fileExtension, ".wasm", StringComparison.OrdinalIgnoreCase)
+ && !(string.Equals(assetTraitName, "WasmResource", StringComparison.OrdinalIgnoreCase)
+ && string.Equals(assetTraitValue, "native", StringComparison.OrdinalIgnoreCase));
+
+ if (isWebcilInWasmAssembly)
+ {
+ // Fast path: ConvertDllsToWebcil already computed the sizes. Fall back to
+ // reading them straight from the produced webcil when it didn't: that task is
+ // incremental and can be skipped while this boot config is regenerated (e.g. a
+ // boot-config property changed but no assembly did), which would otherwise drop
+ // payloadSize/tableSize and break the loader. R2R images especially need
+ // tableSize before instantiation.
+ if (!webcilSizeByName.TryGetValue(webcilSizeLookupKey, out var sizes))
+ {
+ string webcilFile = resource.GetMetadata("OriginalItemSpec");
+ if (string.IsNullOrEmpty(webcilFile) || !File.Exists(webcilFile))
+ webcilFile = resource.ItemSpec;
+
+ if (!ConvertDllsToWebcil.TryReadWebcilSizes(webcilFile, out int ps, out int ts, out string failureReason) || ps <= 0)
+ {
+ Log.LogError($"Could not read the Webcil payload/table sizes for '{resourceName}' from '{webcilFile}' ({failureReason}). The runtime loader requires payloadSize for every webcil-in-wasm assembly.");
+ continue;
+ }
+
+ sizes = (ts, ps);
+ }
+
+ webcilSizes[r2rSizeStoreKey] = sizes;
+ }
}
if (!string.IsNullOrEmpty(behavior))
@@ -479,7 +549,7 @@ private void WriteBootConfig(string entryAssemblyName)
string? imports = null;
if (IsTargeting100OrLater())
- imports = helper.TransformResourcesToAssets(result, BundlerFriendly);
+ imports = helper.TransformResourcesToAssets(result, BundlerFriendly, webcilSizes);
helper.WriteConfigToFile(result, OutputPath, mergeWith: MergeWith, imports: imports);