Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,7 @@ Copyright (c) .NET Foundation. All rights reserved.
<ConvertDllsToWebcil Candidates="@(_WasmDllBuildCandidates)" IntermediateOutputPath="$(_WasmBuildTmpWebcilPath)" OutputPath="$(_WasmBuildWebcilPath)" IsEnabled="$(_WasmEnableWebcil)" WebcilVersion="$(_WasmWebcilVersion)">
<Output TaskParameter="FileWrites" ItemName="FileWrites" />
<Output TaskParameter="FileWrites" ItemName="_WasmConvertedWebcilOutputs" />
<Output TaskParameter="WebcilSizes" ItemName="_WasmWebcilSizes" />
</ConvertDllsToWebcil>

<!-- Touch is needed for property-triggered full rebuilds: the task's content comparison
Expand Down Expand Up @@ -742,6 +743,7 @@ Copyright (c) .NET Foundation. All rights reserved.
IsMultiThreaded="$(WasmEnableThreads)"
UseMonoRuntime="$(UseMonoRuntime)"
FingerprintAssets="$(_WasmFingerprintAssets)"
WebcilSizes="@(_WasmWebcilSizes)"
BundlerFriendly="$(_WasmBundlerFriendlyBootConfig)"
ExitOnUnhandledError="$(WasmTestExitOnUnhandledError)"
AppendElementOnExit="$(WasmTestAppendElementOnExit)"
Expand Down Expand Up @@ -999,6 +1001,7 @@ Copyright (c) .NET Foundation. All rights reserved.
<ConvertDllsToWebcil Candidates="@(_NewWasmPublishStaticWebAssets)" IntermediateOutputPath="$(_WasmPublishTmpWebcilPath)" OutputPath="$(_WasmPublishWebcilPath)" IsEnabled="$(_WasmEnableWebcil)" WebcilVersion="$(_WasmWebcilVersion)">
<Output TaskParameter="WebcilCandidates" ItemName="_NewWebcilPublishStaticWebAssetsCandidates" />
<Output TaskParameter="FileWrites" ItemName="FileWrites" />
<Output TaskParameter="WebcilSizes" ItemName="_WasmWebcilSizes" />
</ConvertDllsToWebcil>

<!-- _NewWebcilPublishStaticWebAssetsCandidates contain the `Fingerprint` and the `Integrity` from the old assets.
Expand Down Expand Up @@ -1229,6 +1232,7 @@ Copyright (c) .NET Foundation. All rights reserved.
IsMultiThreaded="$(WasmEnableThreads)"
UseMonoRuntime="$(UseMonoRuntime)"
FingerprintAssets="$(_WasmFingerprintAssets)"
WebcilSizes="@(_WasmWebcilSizes)"
BundlerFriendly="$(_WasmBundlerFriendlyBootConfig)"
ExitOnUnhandledError="$(WasmTestExitOnUnhandledError)"
AppendElementOnExit="$(WasmTestAppendElementOnExit)"
Expand Down
1 change: 1 addition & 0 deletions src/mono/sample/wasm/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
causing WasmApp.Common.props (Mono) to be imported even for CoreCLR builds. -->
<PropertyGroup>
<RuntimeFlavor Condition="'$(Nested_RuntimeFlavor)' != ''">$(Nested_RuntimeFlavor)</RuntimeFlavor>
<PublishReadyToRun Condition="'$(PublishReadyToRun)' == '' and '$(Nested_PublishReadyToRun)' != ''">$(Nested_PublishReadyToRun)</PublishReadyToRun>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to be sure, only properties that needs to be available very very early are required here.
Is this the case?

</PropertyGroup>

<!-- Import late, so properties like $(ArtifactsBinDir) are set -->
Expand Down
1 change: 1 addition & 0 deletions src/mono/sample/wasm/Directory.Build.targets
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
<NestedBuildProperty Include="InvariantGlobalization" />
<NestedBuildProperty Include="InvariantTimezone" />
<NestedBuildProperty Include="PublishTrimmed" />
<NestedBuildProperty Include="PublishReadyToRun" />
<NestedBuildProperty Include="RunAOTCompilation" />
<NestedBuildProperty Include="UsingNativeAOT" />
<NestedBuildProperty Include="WasmBuildNative" />
Expand Down
122 changes: 92 additions & 30 deletions src/native/libs/Common/JavaScript/host/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,50 +46,112 @@ export function registerDllBytes(bytes: Uint8Array, virtualPath: string, shortNa
}
}

export async function instantiateWebcilModule(webcilPromise: Promise<Response>, memory: WebAssembly.Memory, virtualPath: string): Promise<void> {
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.`);
}
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;
Comment on lines +58 to +62
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<Response>, virtualPath: string): Promise<Response> {
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<string, WebAssembly.ImportValue> {
const webcilImports: Record<string, WebAssembly.ImportValue> = { 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 {
Expand Down
2 changes: 1 addition & 1 deletion src/native/libs/Common/JavaScript/loader/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ async function fetchWebcil(assetInternal: AssetEntryInternal): Promise<void> {
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);
Expand Down
5 changes: 5 additions & 0 deletions src/native/libs/Common/JavaScript/types/ems-ambient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
}
2 changes: 2 additions & 0 deletions src/native/libs/Common/JavaScript/types/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ export interface AssetEntryInternal extends AssetEntry {
priority?: boolean
shortName?: string
inprogress?: boolean
tableSize?: number
payloadSize?: number
}

export type LoaderConfigInternal = LoaderConfig & {
Expand Down
14 changes: 14 additions & 0 deletions src/native/libs/Common/JavaScript/types/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ function libBrowserUtilsFactory() {
"abort",
"__trap",
"__stack_pointer",
"__coreclr_wasm_rtlrestorecontext_tag",
"__async_continuation",
"$readI53FromU64",
"$readI53FromI64",
"$writeI53ToI64"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, (int tableSize, int payloadSize)>? webcilSizes = null)
{
List<string> imports = [];

Expand Down Expand Up @@ -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));
Expand Down
16 changes: 16 additions & 0 deletions src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,22 @@ public class GeneralAsset
public string hash { get; set; }
public string resolvedUrl { get; set; }
public string cache { get; set; }

/// <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; }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, create a WebcilAsset and put these specific props there.


/// <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; }
}

[DataContract]
Expand Down
Loading
Loading