diff --git a/docs/design/mono/webcil.md b/docs/design/mono/webcil.md index b8d1a295962b6c..0adf8e7320ff64 100644 --- a/docs/design/mono/webcil.md +++ b/docs/design/mono/webcil.md @@ -217,14 +217,21 @@ table bases, merges the host, shim, and composite with `wasm-merge`, folds the g preserve the `name` section with `-g`. The host exports its reserved buffer address, buffer capacity, and composite table base through -`wasi_r2r_image_base`, `wasi_r2r_image_cap`, and `wasi_r2r_table_base`. Before merging, the composer -checks that the active payload fits the buffer and that the composite's table entries end before +`wasi_r2r_image_base`, `wasi_r2r_image_cap`, and `wasi_r2r_table_base`, and the address and capacity +of a composite name buffer through `wasi_r2r_composite_name_base` and `wasi_r2r_composite_name_cap`. +Before merging, the composer checks that the active payload fits the buffer, that the composite's +file name fits the name buffer, and that the composite's table entries end before the host's own active element segment. These checks must happen before instantiation: a runtime check cannot prevent an active segment from overwriting an undersized reservation. The shim's start function calls `patchWebcilHeader`; the active segments themselves install the -payload and function table. The host's external assembly probe serves `composite-r2r.wasm` from +payload and function table. The shim also carries an active segment that writes the composite's +file name, NUL-terminated, into the host's name buffer. Component stubs record the owner by the file +name Crossgen2 wrote, so that name is the single authority: publishing uses the `.r2r.wasm` +name the ReadyToRun tasks also give browser, and a prebuilt host can be composed with a composite +of any name. The host's external assembly probe serves the recorded name from the embedded buffer and maps raw per-assembly WebCIL forwarding stubs from `comp/.dll`. +An uncomposed host records no name and serves no composite. The composer extracts those payloads from their self-installing Wasm wrappers at build time. Placing a composite on disk without composing it into the host does not satisfy this contract. diff --git a/docs/workflow/building/coreclr/wasi-r2r.md b/docs/workflow/building/coreclr/wasi-r2r.md index f968cf63639149..ae6ae15ac018c3 100644 --- a/docs/workflow/building/coreclr/wasi-r2r.md +++ b/docs/workflow/building/coreclr/wasi-r2r.md @@ -4,7 +4,7 @@ This is an experimental in-tree publishing workflow. The shipping WASI SDK does not yet select the CoreCLR app builder. WASI requires the compiled composite to be composed into the host component before execution. -Copying `composite-r2r.wasm` beside an unmodified host is not sufficient. Composition is +Copying the composite beside an unmodified host is not sufficient. Composition is implemented by the in-tree `ComposeWasiReadyToRun` MSBuild task. The [WebCIL design document](../../../design/mono/webcil.md#wasi-host-composition) describes the image layout and host contract. @@ -31,8 +31,11 @@ Build the runtime, libraries, and packs, then publish an in-tree WASI project: ``` The app builder enables composite R2R, compiles the app/framework closure, sizes the host's image -buffer and table reservation, links the host, and invokes the composer. It deploys the composed -host and the per-assembly stubs under the app bundle's `managed/` directory. +buffer and table reservation, links the host, and invokes the composer. As on browser, the +ReadyToRun tasks name the composite `.r2r.wasm` after the app's main assembly. The composer +records that file name in the host, so the host answers only to the name the component stubs +carry. It deploys the composed host and the per-assembly stubs under the app bundle's `managed/` +directory. Non-composite R2R and `WasmSingleFileBundle` are not supported by this path. ## Composer diagnostics diff --git a/src/coreclr/hosts/corerun/wasi_r2r_probe.hpp b/src/coreclr/hosts/corerun/wasi_r2r_probe.hpp index a72d77b85ce55d..ffe3ca74a37b91 100644 --- a/src/coreclr/hosts/corerun/wasi_r2r_probe.hpp +++ b/src/coreclr/hosts/corerun/wasi_r2r_probe.hpp @@ -48,11 +48,23 @@ extern "C" uint32_t g_wasi_r2r_image_cap; #define WEBCIL_SECTION_HEADER_SIZE (16u) #define WEBCIL_VERSION_MAJOR_OFFSET (4u) -// The composite native image's bundle-relative file name (the ownerCompositeExecutable named by each -// per-assembly stub). The runtime asks for this via NativeImage::Open -> external_assembly_probe. -#ifndef WASI_R2R_COMPOSITE_NAME -#define WASI_R2R_COMPOSITE_NAME "composite-r2r.wasm" +// The composite native image's file name (the ownerCompositeExecutable recorded in each per-assembly +// stub). The runtime asks for this via NativeImage::Open -> external_assembly_probe. crossgen2's output +// name is the single authority: the composer writes that NUL-terminated name here with an active data +// segment, alongside the payload, so a host that was never composed matches no name at all. +#ifndef WASI_R2R_COMPOSITE_NAME_CAP +#define WASI_R2R_COMPOSITE_NAME_CAP (256u) #endif +extern "C" +{ + char g_wasi_r2r_composite_name[WASI_R2R_COMPOSITE_NAME_CAP] = {}; +} + +static bool WasiIsCompositeName(const char* name) +{ + return g_wasi_r2r_composite_name[0] != '\0' + && strncmp(name, g_wasi_r2r_composite_name, WASI_R2R_COMPOSITE_NAME_CAP) == 0; +} static bool WasiIsWebcilV1(const uint8_t* p, size_t len) { @@ -140,7 +152,7 @@ static bool WasiStaticR2RProbe(const char* name, const char* const* dirs, size_t { // The composite native image itself: return the merged composite payload at imageBase. Its size is // read from the self-describing WbIL header (no baked constant), and validated against the buffer cap. - if (strcmp(name, WASI_R2R_COMPOSITE_NAME) == 0) + if (WasiIsCompositeName(name)) { int64_t payloadSize = WasiWebcilPayloadSize(&g_wasi_r2r_image[0], g_wasi_r2r_image_cap); if (payloadSize <= 0 || static_cast(payloadSize) > g_wasi_r2r_image_cap) @@ -157,7 +169,7 @@ static bool WasiStaticR2RProbe(const char* name, const char* const* dirs, size_t // A managed assembly: return its per-assembly stub payload (extracted from .wasm on disk). // The stub carries the assembly metadata + the R2R header naming the composite, which drives the - // runtime to then request WASI_R2R_COMPOSITE_NAME above. + // runtime to then request the composite name above. size_t nlen = strlen(name); if (nlen > 4 && strcmp(name + nlen - 4, ".dll") == 0) { @@ -209,6 +221,19 @@ extern "C" __attribute__((export_name("wasi_r2r_table_base"))) uint32_t wasi_r2r return (uint32_t)WASI_R2R_TABLE_BASE; } +// The composite name buffer's address and capacity, so the composer can write the composite's file name +// into it without a host rebuild. This is what lets one prebuilt host serve whatever name crossgen2 gave +// the composite. +extern "C" __attribute__((export_name("wasi_r2r_composite_name_base"))) uint32_t wasi_r2r_composite_name_base(void) +{ + return (uint32_t)(uintptr_t)&wasi_r2r::g_wasi_r2r_composite_name[0]; +} + +extern "C" __attribute__((export_name("wasi_r2r_composite_name_cap"))) uint32_t wasi_r2r_composite_name_cap(void) +{ + return (uint32_t)WASI_R2R_COMPOSITE_NAME_CAP; +} + #endif // TARGET_WASI #endif // WASI_R2R_PROBE_HPP diff --git a/src/mono/wasi/build/WasiApp.CoreCLR.targets b/src/mono/wasi/build/WasiApp.CoreCLR.targets index 0c61a7cea23567..b3ee1b3ad168de 100644 --- a/src/mono/wasi/build/WasiApp.CoreCLR.targets +++ b/src/mono/wasi/build/WasiApp.CoreCLR.targets @@ -131,9 +131,17 @@ + + + <_WasiR2RCompositeCompilation Include="@(_ReadyToRunCompileList->WithMetadataValue('CreateCompositeImage', 'true'))" /> + + <_WasiR2ROutputDir>$([MSBuild]::NormalizeDirectory('$(_ReadyToRunOutputPath)')) - <_WasiR2RCompositePath>$(_WasiR2ROutputDir)composite-r2r.wasm + <_WasiR2RCompositePath>@(_WasiR2RCompositeCompilation->'%(OutputR2RImage)') <_WasiR2RImageBufferSource>$([MSBuild]::NormalizePath('$(_WasmIntermediateOutputPath)', 'corerun-relink', 'r2r-image-buffer.cpp')) reservedTableStart) throw new LogAsErrorException( @@ -74,13 +78,25 @@ public override bool Execute() throw new LogAsErrorException( $"The composite payload is {PayloadSize} bytes but the host staging buffer is only {imageCapacity} bytes."); + // Component stubs name their owner by the file name crossgen2 wrote, so the composite's own + // file name is the one the host must answer to. + string compositeName = Path.GetFileName(CompositePath); + byte[] compositeNameBytes = Encoding.UTF8.GetBytes(compositeName); + if (compositeNameBytes.Length == 0 || Array.IndexOf(compositeNameBytes, (byte)0) >= 0) + throw new LogAsErrorException($"The composite file name '{compositeName}' cannot be recorded in the host."); + if (compositeNameBytes.Length >= compositeNameCapacity) + throw new LogAsErrorException( + $"The composite file name '{compositeName}' is {compositeNameBytes.Length} UTF-8 bytes, but the host " + + $"records at most {compositeNameCapacity - 1}."); + Log.LogMessage(MessageImportance.High, $"WASI R2R composition: imageBase={imageBase} tableBase={tableBase} " + - $"reservedSlots={reservedTableStart} compositeFuncs={FunctionCount} payload={PayloadSize} cap={imageCapacity}"); + $"reservedSlots={reservedTableStart} compositeFuncs={FunctionCount} payload={PayloadSize} cap={imageCapacity} " + + $"composite='{compositeName}'"); string shimWatPath = Path.Combine(OutputDirectory!, "shim.wat"); string shimPath = Path.Combine(OutputDirectory!, "shim.wasm"); - File.WriteAllText(shimWatPath, CreateShimWat(imageBase, tableBase, PayloadSize)); + File.WriteAllText(shimWatPath, CreateShimWat(imageBase, tableBase, PayloadSize, compositeNameBase, compositeNameBytes)); Run(WasmToolsPath!, $"parse {Quote(shimWatPath)} -o {Quote(shimPath)}"); Run(WasmToolsPath!, $"validate --features all {Quote(shimPath)}"); @@ -168,18 +184,28 @@ private static void DeleteUnbundledModules(string outputDirectory) File.Delete(path); } - private static string CreateShimWat(int memoryBase, int tableBase, int payloadSize) => - FormattableString.Invariant($""" + // The shim imports the host's memory (the merge names the host "webcil", as the composite does) so its + // active segment writes the NUL-terminated composite name into the host's reserved name buffer. + private static string CreateShimWat(int memoryBase, int tableBase, int payloadSize, int compositeNameBase, byte[] compositeName) + { + StringBuilder escapedName = new(); + foreach (byte b in compositeName) + escapedName.Append('\\').Append(b.ToString("x2", CultureInfo.InvariantCulture)); + + return FormattableString.Invariant($""" (module + (import "webcil" "memory" (memory 0)) (import "composite" "patchWebcilHeader" (func $patchWebcilHeader (param i32 i32))) (global (export "__memory_base") i32 (i32.const {memoryBase})) (global (export "__table_base") i32 (i32.const {tableBase})) + (data (i32.const {compositeNameBase}) "{escapedName}\00") (func $start i32.const {memoryBase} i32.const {payloadSize} call $patchWebcilHeader) (start $start)) """); + } private void Run(string tool, string arguments) {