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);