diff --git a/docs/design/features/partitioned-wasm-r2r-modules-mibc-sizes.csv b/docs/design/features/partitioned-wasm-r2r-modules-mibc-sizes.csv
new file mode 100644
index 00000000000000..77e9b7025d8772
--- /dev/null
+++ b/docs/design/features/partitioned-wasm-r2r-modules-mibc-sizes.csv
@@ -0,0 +1,25 @@
+variant,role,raw,gzip,brotli
+heuristic,primary,11473928,1775725,1138160
+heuristic,map,2099936,909297,716273
+heuristic,body,43239255,6455104,3365735
+heuristic,all,56813119,9140126,5220168
+sparse,primary,6765354,729050,387406
+sparse,map,2188618,979681,768394
+sparse,body,48078684,7345638,3914392
+sparse,all,57032656,9054369,5070192
+empty,primary,6764118,728715,388491
+empty,map,2188630,979496,768322
+empty,body,48079617,7345659,3913065
+empty,all,57032365,9053870,5069878
+union,primary,6766483,729415,388545
+union,map,2188602,979495,769139
+union,body,48077643,7345520,3914205
+union,all,57032728,9054430,5071889
+repeated,primary,6765354,729042,388073
+repeated,map,2188618,979681,768394
+repeated,body,48078684,7345640,3913053
+repeated,all,57032656,9054363,5069520
+merged-union,primary,6766483,729392,389344
+merged-union,map,2188602,979495,769139
+merged-union,body,48077643,7345520,3915052
+merged-union,all,57032728,9054407,5073535
diff --git a/docs/design/features/partitioned-wasm-r2r-modules-sizes.csv b/docs/design/features/partitioned-wasm-r2r-modules-sizes.csv
new file mode 100644
index 00000000000000..e9b18164081cdd
--- /dev/null
+++ b/docs/design/features/partitioned-wasm-r2r-modules-sizes.csv
@@ -0,0 +1,55 @@
+side,app,role,files,raw_bytes,gzip_bytes,brotli_bytes
+baseline,empty-browser,primary,1,3626082,1042189,722337
+baseline,empty-browser,entrypoints-map,0,0,0,0
+baseline,empty-browser,body,0,0,0,0
+baseline,empty-blazor,primary,1,14748120,3659495,2190179
+baseline,empty-blazor,entrypoints-map,0,0,0,0
+baseline,empty-blazor,body,0,0,0,0
+baseline,blazing-pizza,primary,1,25231901,5327995,3100237
+baseline,blazing-pizza,entrypoints-map,0,0,0,0
+baseline,blazing-pizza,body,0,0,0,0
+baseline,micro-benchmarks,primary,1,3912482,1127476,775941
+baseline,micro-benchmarks,entrypoints-map,0,0,0,0
+baseline,micro-benchmarks,body,0,0,0,0
+baseline,havit-bootstrap,primary,1,42669126,8164595,4731287
+baseline,havit-bootstrap,entrypoints-map,0,0,0,0
+baseline,havit-bootstrap,body,0,0,0,0
+baseline,mud-blazor,primary,1,136209465,23167417,12019321
+baseline,mud-blazor,entrypoints-map,0,0,0,0
+baseline,mud-blazor,body,0,0,0,0
+baseline,igniteui-light,primary,1,29373727,5891038,3426600
+baseline,igniteui-light,entrypoints-map,0,0,0,0
+baseline,igniteui-light,body,0,0,0,0
+baseline,blazor-perf,primary,1,15420384,3780025,2266750
+baseline,blazor-perf,entrypoints-map,0,0,0,0
+baseline,blazor-perf,body,0,0,0,0
+baseline,avalonia-bench,primary,1,15190773,3722407,2377841
+baseline,avalonia-bench,entrypoints-map,0,0,0,0
+baseline,avalonia-bench,body,0,0,0,0
+feature,empty-browser,primary,1,1591530,379683,271875
+feature,empty-browser,entrypoints-map,1,111661,58585,48451
+feature,empty-browser,body,1,2897189,708921,477380
+feature,empty-blazor,primary,1,4607065,933941,641456
+feature,empty-blazor,entrypoints-map,1,605836,283176,227702
+feature,empty-blazor,body,1,13609113,2839154,1508192
+feature,blazing-pizza,primary,1,7131888,1258291,830275
+feature,blazing-pizza,entrypoints-map,1,1257548,564851,445728
+feature,blazing-pizza,body,1,25590289,4142627,2126338
+feature,micro-benchmarks,primary,1,1669058,397843,283578
+feature,micro-benchmarks,entrypoints-map,1,121194,63397,52604
+feature,micro-benchmarks,body,1,3159760,777089,521213
+feature,havit-bootstrap,primary,1,11473928,1775730,1138165
+feature,havit-bootstrap,entrypoints-map,1,2099936,909297,716273
+feature,havit-bootstrap,body,1,43239255,6455088,3366232
+feature,mud-blazor,primary,1,27514543,3250352,1911039
+feature,mud-blazor,entrypoints-map,1,8101906,3087422,2335534
+feature,mud-blazor,body,1,145879819,19527694,8980534
+feature,igniteui-light,primary,1,8235409,1373042,893843
+feature,igniteui-light,entrypoints-map,1,1479638,666186,518666
+feature,igniteui-light,body,1,29714718,4540681,2375772
+feature,blazor-perf,primary,1,4743053,954992,651958
+feature,blazor-perf,entrypoints-map,1,635863,296544,239116
+feature,blazor-perf,body,1,14276433,2934765,1565229
+feature,avalonia-bench,primary,1,5966799,1227308,847497
+feature,avalonia-bench,entrypoints-map,1,615587,295663,237782
+feature,avalonia-bench,body,1,12931853,2600819,1524572
diff --git a/docs/design/features/partitioned-wasm-r2r-modules.md b/docs/design/features/partitioned-wasm-r2r-modules.md
new file mode 100644
index 00000000000000..1b0fae5e82248d
--- /dev/null
+++ b/docs/design/features/partitioned-wasm-r2r-modules.md
@@ -0,0 +1,555 @@
+# Design for partitioned R2R modules with delayed cold code loading
+
+## Overview
+
+This doc outlines a design for a logical ReadyToRun (R2R) module which is distributed across a primary WebAssembly module and one or more auxiliary WebAssembly modules.
+
+Loading the primary module makes the logical R2R module functional and usable. Methods whose compiled code has not yet arrived remain callable through R2R-to-interpreter thunks. Auxiliary modules can arrive later.
+
+Auxiliary modules will hold the wasm method bodies for their aux methods, as well as their fixup data, GC info, and EH info.
+
+Auxiliary modules export a function, `ActivateModule()`, which copies all its data into the shared memory, and fills in its allotted range in the global function table. Once the module is activated, the runtime can run fixups and enable executing R2R code for the methods in the auxiliary module.
+
+The mapping for which methods have code in the auxiliary modules is in a separate module called the Auxiliary EntryPoints Module. This contains a second set of `MethodDefEntryPoints` and `InstanceMethodEntryPoints` sections. Until that module is downloaded, it's not possible to know which methods may have R2R code and which will always be interpreted.
+
+Metadata and activation info about auxiliary modules is stored in the AuxiliaryModuleMetadata section in the primary module.
+
+### Implemented V1
+
+Phases 1 through 4 are implemented for single-threaded CoreCLR Browser-WASM composite images with retained IL. The feature is opt-in:
+
+```shell
+dotnet publish -p:PublishReadyToRun=true -p:PublishReadyToRunComposite=true -p:WasmEnableAuxiliaryR2RModules=true
+```
+
+Publishing requires matching locally built runtime and WebAssembly SDK packs. Crossgen2 exposes the corresponding `--wasm-auxiliary-modules` option. Non-browser, non-composite, multithreaded, and stripped-IL configurations are rejected.
+
+The compiler emits a primary module, an `.entrypoints.wasm` mapping module, and a `.1.wasm` cold-code module, plus an `.auxiliary.json` publication manifest. Without MIBC input, helpers and public non-generic methods on non-generic types are hot; methods that cannot use interpreter fallback are also kept primary. With explicit `--mibc` inputs, the union of profiled methods replaces the visibility/genericity heuristic, while mandatory primary methods and helpers stay resident. Concrete reference instantiations select their shared canonical body; value-type instantiations stay distinct. An empty or unmatched explicit profile moves every eligible method cold rather than restoring the heuristic. This selects by membership, not CPU weight, and consumes existing profiles without collecting a browser trace. Ordinary managed calls remain indirect. Eligible managed compiler helpers retain direct calls through local copies of their bodies and funclets, sharing the primary's logical slots and metadata. Shared fallback thunks and eagerly accessed data remain primary.
+
+The cold-code module owns its methods and funclets, runtime-function records, GC/unwind and exception-handling data, fixup lists, and exclusively cold import cells and signatures. The mapping module owns auxiliary entrypoint tables and their method-identification signatures. Shared imports, async-resume data rebased during image registration, and the final runtime-function/unwind record needed to reserve the virtual-IP range remain primary. Activation never overwrites those resident data.
+
+Before assigning import offsets, the WebCIL writer classifies import usage from method fixups and relocations. Each import table and its parallel signature-RVA array use the same primary-first, cold-exclusive-last partition key, retaining the existing deterministic order within each group. Where an import section requires GC reference maps, each signature RVA points to a map followed immediately by its method fixup signature. Table identities and entry sizes do not change; fixup lists and relocations are emitted against the new indices and offsets rather than patched afterward.
+
+Eager, shared, unused, and resident-data-referenced cells stay primary. Signature ownership is computed after symbol deduplication: signatures shared with primary cells or directly referenced by resident data remain primary even when some referencing cells move cold. Cold-exclusive signature blobs, including their GC reference-map prefixes, are stably grouped at the end of data emission and move together to the cold-code module. Shared signatures and their maps remain primary. Alignment gaps are not claimed as cold data, so aligned blobs can still require multiple passive segments. Portable import-thunk records are resident data and pin their referenced cells primary; moving those records would require a separate ownership analysis and is deliberately not inferred from a cold method's use of the cell.
+
+Both sidecars import the primary module's memory, table, bases, stack pointer, exception tag, and async global. Their data and element segments are passive. `ActivateModule()` installs those segments and writes its activation cell last; repeated activation is a no-op. Mapping activation also notifies the runtime to advance the global EntryPoints era. The browser host starts sidecar loading without adding it to the startup barrier.
+
+Activation runs on the browser's JavaScript thread. A synchronous managed computation started before activation can block the event loop and remain interpreted until it yields or returns, even if the sidecars have finished downloading. Startup measurements include this fallback interval; post-activation throughput measurements must first verify that both activation cells are active.
+
+The existing five `DelayedR2RActivation` test variants retain the phase-1 simulation. Five `PartitionedR2RActivation` variants exercise real sidecars, eager loading, both manually controlled activation orders, and all-hot/all-cold partition boundaries. Coverage includes GC, exception handlers, generics from another image, delegates, reflection, managed and unmanaged function pointers, and suspended interpreter continuations. Zero-length partitions are valid; their empty sidecars preserve the same activation contract and contiguous runtime-function ranges.
+
+See the [compiler usage and restrictions](../../../src/coreclr/tools/aot/crossgen2/README.md) and [browser build integration](../../../src/mono/browser/build/README.md) for the emitted-file contract and supported diagnostic options.
+
+### Measured V1 results
+
+The final Release comparison used feature commit `f0984cc980d4d6201d1daebc15305c1c2c240d87` against the unchanged phase-1 control at `1d0ddf7b2e680164058928e14b615de37a46504e`. These results use the no-MIBC heuristic, not profile-guided partitioning. This measures the complete feature change, not just toggling the publishing property on the new compiler. All nine applications came from the same macro-benchmark checkout at `84f9ac6e9c293f1ce50f64dcb36d747daf8f5509`, including its preserved pre-existing local changes. Publishing used SDK `11.0.100-rc.2.26504.105`, locally built `12.0.0-dev` packs, and the frozen final compiler.
+
+All 36 normal desktop/mobile result rows were measured again, separately from 12 activation-gated steady-state pages. External cold/warm startup counts were 10/5; the internal microbenchmark exports only its single cold page and standard seven-window sampling. UI walkthrough counts remained unchanged: five for the standalone Blazor applications, three per Avalonia scenario, and one per hosted Blazor scenario. These are descriptive local measurements on a monitored shared Linux desktop, not exclusive-machine or statistical-significance claims. Attempts overlapping observed external runtime builds/tests were excluded and repeated; a failed hosted-server readiness attempt was also repeated without assuming its cause.
+
+#### Physical modules and transferred size
+
+The following sizes are KiB of Brotli-compressed R2R modules. The control has one module; the feature has a primary, an entrypoint map, and one cold-body module. The [size inventory](partitioned-wasm-r2r-modules-sizes.csv) records exact raw, gzip, and Brotli bytes for every role and application.
+
+| Application | Control | Primary | Entrypoint map | Cold body | Combined change |
+|---|---:|---:|---:|---:|---:|
+| empty-browser | 705.4 | 265.5 | 47.3 | 466.2 | +10.4% |
+| empty-blazor | 2138.8 | 626.4 | 222.4 | 1472.8 | +8.5% |
+| blazing-pizza | 3027.6 | 810.8 | 435.3 | 2076.5 | +9.7% |
+| micro-benchmarks | 757.8 | 276.9 | 51.4 | 509.0 | +10.5% |
+| havit-bootstrap | 4620.4 | 1111.5 | 699.5 | 3287.3 | +10.3% |
+| mud-blazor | 11737.6 | 1866.2 | 2280.8 | 8770.1 | +10.0% |
+| igniteui-light | 3346.3 | 872.9 | 506.5 | 2320.1 | +10.6% |
+| blazor-perf | 2213.6 | 636.7 | 233.5 | 1528.5 | +8.4% |
+| avalonia-bench | 2322.1 | 827.6 | 232.2 | 1488.8 | +9.8% |
+
+#### Startup and initial interactions
+
+Times below are control / partitioned milliseconds, with smaller being better. Startup is the unchanged harness's cold managed-readiness metric except for micro-benchmarks, which reports .NET creation, and hosted blazor-perf, which reports its server-rendered landing page rather than WASM startup.
+
+| Application | Desktop cold | Mobile cold | Framework Brotli change | Completed encoded download change |
+|---|---:|---:|---:|---:|
+| empty-browser | 140 / 153 | 1492 / 1317 | +2.4% | +2.9% |
+| empty-blazor | 396 / 446 | 2835 / 2778 | +3.4% | +3.8% |
+| blazing-pizza | 419 / 471 | 3523 / 3113 | +4.4% | +4.4% |
+| micro-benchmarks | 151 / 139 | 1518 / 1354 | +2.5% | +3.0% |
+| havit-bootstrap | 526 / 517 | 4874 / 4052 | +5.5% | +5.0% |
+| mud-blazor | 1276 / 929 | 10438 / 6507 | +6.4% | +6.4% |
+| igniteui-light | 529 / 560 | 4144 / 3531 | +5.0% | +5.2% |
+| blazor-perf | 29 / 30 | 212 / 206 | +3.1% | +3.6% |
+| avalonia-bench | 384 / 608 | 3538 / 3731 | +3.2% | +3.5% |
+
+Framework sizes are on-disk totals, not necessarily fetched bytes. Completed download audits include finished asynchronous sidecar transfers; the harness's earlier startup download snapshot can omit those transfers. Hosted blazor-perf's download row uses its actual `/counter-heavy-wasm` client route, not the small landing page. Its full 22-scenario desktop walkthrough, including nine WASM client scenarios, was retained.
+
+Earlier managed readiness is not the same as faster initial interactions. Desktop walkthrough times regressed from 126 to 334 ms for Pizza (+165%), 2674 to 4718 ms for Havit (+76%), 5059 to 15882 ms for Mud (+214%), and 731 to 1035 ms for Ignite (+42%). Activation state was not recorded on those normal walkthrough pages, so these regressions are not all attributed to one mechanism.
+
+The ungated synchronous JSON benchmark measured 368948 / 5978 operations per second on desktop and 116166 / 1987 on mobile. Separate diagnostics verified that its long initial managed call prevents JavaScript activation until it returns, leaving the measured loop interpreted. These startup/fallback costs are retained, not replaced by the following steady-state numbers.
+
+#### Verified post-activation throughput
+
+Separate fresh pages waited for actual mapping/body status `1/1` before timing. Each repetition used the original seven two-second windows, discarded the first, and applied the existing interquartile-range filtering. Values below are medians of three repetitions per side/profile. All 252 feature window-boundary checks observed active status, and each feature page forwarded exactly one real native mapping-activation notification.
+
+| Profile | Operations per second | Control | Partitioned | Change |
+|---|---|---:|---:|---:|
+| Desktop | JSON parse | 375077 | 341759 | -8.88% |
+| Mobile | JSON parse | 116459 | 107613 | -7.60% |
+| Desktop | JavaScript interop | 782032 | 789858 | +1.00% |
+| Mobile | JavaScript interop | 263550 | 261608 | -0.74% |
+| Desktop | Exceptions | 176 | 165 | -6.25% |
+| Mobile | Exceptions | 59 | 55 | -6.78% |
+
+Avalonia required an artifact-only SDK11 eligibility override and native SkiaSharp relinking using matching local workload targets/tasks. Both sides rendered successfully; this is not out-of-box SDK11/harness support. No tracked benchmark source changes or package/workload installations were needed for the workaround.
+
+The V1 heuristic therefore reduces the primary download and can improve managed readiness, but increases total transfer size and does not eliminate initial-interaction or steady-throughput regressions. It remains an opt-in experimental tradeoff, not a general performance improvement.
+
+#### Validation and retained evidence
+
+Browser Release and Linux x64 Release builds passed. The compiler suite passed 226 tests with 39 skips; 17 loader tests, five simulation variants, five physical variants, and eight engine activation/boundary cases passed. A CoreLib-inclusive native helper fixture also verified 200 exceptions, 400 finally executions, 400 forced collections, and live GC roots.
+
+The local evidence root is `~/.copilot/session-state/0797c2c0-9a42-4488-8454-7761c25a6e19/files/aux-benchmarks/final-f0984cc980d4/`. Its `comparison-summary.json`, `performance-comparison.csv`, `module-role-sizes.csv`, `steady-performance-comparison.csv`, raw results, completed download audits, monitoring/retry logs, and 342 checksummed evidence files retain the full comparison. `reproduce-commands.json` records exact commands, environments, inputs, and exit results; the older parent-directory cohort is provisional and is not pooled with these results. Compiler/native validation commands and logs are separately indexed by `artifacts/aux-helper-validation.json` in the feature worktree.
+
+Final publishing inputs are identified by SHA256:
+
+| Input | SHA256 |
+|---|---|
+| Frozen crossgen2 executable | `6683be0a5645d5c2db67c53081d2a1837f3ceb20f8f5686d0a211e4f697db7ef` |
+| Browser runtime pack | `b3b63fc2e104c9c6b5f33cdf4bb2c612f7ffcf7f6f785ca499527fd177c41e8b` |
+| WebAssembly SDK pack | `e7498abf56efe8675ac566ccbf59f365938332a82e9843e9b2907c678c7623fe` |
+
+### MIBC-selected Havit comparison
+
+The follow-up comparison uses the same updated compiler on both sides, SHA256 `f8b896e39df267bc89b5accb57c3763625b997e295b0c07ea2acb462d26f70b9`. It compares the no-profile heuristic against a four-method MIBC generated with `dotnet-pgo create-mibc-from-method-list` from matching trimmed Havit assemblies. This is a synthetic selection profile, not a captured startup profile or evidence of improved startup performance. Its SHA256 is `2fb912f8174663eed44a57fa6bd5f1960db7e200e82a3e63f4c1675229b1c319`.
+
+With matching updated compiler and SDK build targets, add the existing profile item to the application project and use the auxiliary publish command above:
+
+```xml
+
+
+
+```
+
+The SDK forwards this as `--mibc`/`-m` without implicitly requesting `--partial` in auxiliary mode. The profile therefore selects placement rather than reducing the compiled coverage. All six measured variants retain 113,991 managed function/funclet slots.
+
+The following sizes are KiB of Brotli-compressed R2R module files, not whole-application transfer sizes:
+
+| Module | No MIBC | Four-method MIBC |
+|---|---:|---:|
+| Primary | 1111.5 | 378.3 |
+| Entrypoint map | 699.5 | 750.4 |
+| Cold body | 3286.9 | 3822.6 |
+| Combined | 5097.8 | 4951.4 |
+
+Primary size falls 66.0%; primary plus mapping falls 37.7%. Combined Brotli size falls 2.9%, while combined raw size grows 0.4%. This moves most code to the body module; it does not demonstrate that the four selected methods cover startup. No startup timing or completed network-transfer measurement was repeated for this cohort.
+
+The [MIBC size inventory](partitioned-wasm-r2r-modules-mibc-sizes.csv) records exact raw, gzip and Brotli bytes for the heuristic, four-method profile (`sparse`), empty profile, two-profile union, repeated profile, and premerged union. Repeated and premerged profiles preserve the corresponding slot counts and raw role sizes; whole-image byte identity is not claimed.
+
+All six real publishes and browser smokes passed, with five rendered UI navigations each, both activation cells active, and a real native mapping-activation notification. Publishing used an isolated SDK package copy with only the changed source target overlaid; the archived SDK/runtime packages, old compiler, and prior nine-app modules were preserved. The compiler suite passed 232 tests with 39 skips. Six direct MSBuild profile-flow cases passed after the SDK repair, including the auxiliary/profile case that failed before it; the extended Wasm.Build.Tests xUnit fixture was not run because its test assets were not prepared. A CoreLib-inclusive native run with an explicit unmatched MIBC also passed activation, 200 exceptions, 400 finally executions, and 400 forced collections with live roots.
+
+Local size/publish/browser evidence and exact commands are in `~/src/macro-benchmarks/artifacts/mibc-hotcold-full-f8b896-20261028/report.json`; compiler/native validation is indexed by `artifacts/mibc-selection-validation.json` in the feature worktree.
+
+## Flow
+
+When a new method is discovered to be executed, the runtime will attempt to resolve R2R code for the method, first in the primary module, then in the auxiliary modules, if the Auxiliary EntryPoints Module information is available. If the method is determined to be in an auxiliary module, the runtime will check if the module has been activated, and activate the method if it is. Otherwise, flags on the PEP will track what is holding activation of the method: the Auxiliary EntryPoints Module, or the auxiliary module containing the code. Subsequent calls to the interpreted method will check activation status and attempt activation of the R2R code. Checking for activation of the EntryPoints module and the auxiliary module should be as cheap as possible. In this design, it is a simple flag check in the WASM memory.
+
+A lookup that finds nothing does not prove the method will always be interpreted. Generic code can live in an image other than the method's defining module, and an image that is loaded later can have the code. A method that misses therefore records the *auxiliary EntryPoints era* it observed, and repeats the lookup once the era has advanced. The era is global, and advances each time an Auxiliary EntryPoints Module is activated or a new image with code for unrelated generics is registered. For the same reason, the module that will eventually hold a missed method's code is not known up front, so the method waits on the era rather than on a specific module's EntryPoints status.
+
+Auxiliary modules are downloaded eagerly unless it is determined better to wait and download on demand.
+
+The Auxiliary EntryPoints Module and the auxiliary modules can be activated in either order. An auxiliary module that is activated first is not observable, because no lookup can find its methods until the EntryPoints module arrives. The era then advances, and the retried lookup finds the method and activates it directly. An EntryPoints module activated first leaves the methods it maps in the pending-module state until their module arrives.
+
+To reduce cold download size, the mapping of method to auxiliary module resides in a separate module, the Auxiliary EntryPoints Module, which is downloaded eagerly.
+
+Once an auxiliary module is downloaded (we may also require the module to be "requested"), the host calls the exported `ActivateModule()` function, which copies over all data required for methods to be activated and executed, fills the global function table (table 0) with funcrefs, and marks itself as activated via a well-known location in the primary module's data. Once the module is activated, its methods can be activated.
+
+### Pseudocode
+
+The following code describes the interpreter flow for activation of aux methods. 2 PEP flags track what a method is waiting on: `kAwaitingAuxiliaryEntryPoints` (the lookup missed and may succeed after the era advances) and `kPendingAuxiliaryModule` (the code is in an auxiliary module that is not yet activated). The interpreter method data caches the same state as a discriminated value, so call sites decide with a few loads whether to try activation: nothing to wait for (the default), the address of an auxiliary module activation flag, or the era observed by the missed lookup.
+
+The helper names below are illustrative only. Entry-point lookup only reads the method's entry-point record; it does not run fixups or publish a callable target. Status addresses refer to cells in shared memory. `EnsureInterpreterFallback` ensures interpreter bytecode and data exist and leaves `kPrefersInterpreterEntryPoint` set. `CandidateImages` is the defining module's image, then the alternate generic location, then the images registered for unrelated generics.
+
+```csharp
+InterpOrR2R DiscoverMethod(PEP pep)
+{
+ // Read before any lookup, so an activation or registration that happens during the lookup is not missed.
+ var era = CurrentAuxiliaryEntryPointsEra;
+
+ foreach (var image in CandidateImages(pep.MethodDesc))
+ {
+ EntryPoint entry = LookupEntryPoint(
+ image.MethodDefEntryPoints, image.InstanceMethodEntryPoints, pep.MethodDesc);
+ if (entry is null && image.AuxiliaryEntryPointsModuleActivationStatus != 0)
+ {
+ entry = LookupEntryPoint(
+ image.AuxMethodDefEntryPoints, image.AuxInstanceMethodEntryPoints, pep.MethodDesc);
+ }
+
+ if (entry is null)
+ {
+ continue;
+ }
+
+ AuxiliaryModuleMetadataEntry auxModule = FindAuxiliaryModuleMetadataForRuntimeIndex(entry.RuntimeFunctionIndex);
+ if (auxModule is not null && auxModule.ActivationStatus == 0)
+ {
+ // Found, but the code has not arrived. Stop searching: this is the image that owns the method.
+ EnsureInterpreterFallback(pep);
+ pep.InterpreterData.AuxiliaryMethodInfo = CheckActivationFlag(&auxModule.ActivationStatus);
+ pep.SetFlag(kPendingAuxiliaryModule);
+ return InterpOrR2R.Interp;
+ }
+
+ return ActivateOrFallBackToInterpreter(pep, entry);
+ }
+
+ // Nothing found. An Auxiliary EntryPoints Module activated later, or an image registered later, may have the code.
+ EnsureInterpreterFallback(pep);
+ pep.InterpreterData.AuxiliaryMethodInfo = CheckEra(era);
+ pep.SetFlag(kAwaitingAuxiliaryEntryPoints);
+ return InterpOrR2R.Interp;
+}
+```
+
+On subsequent invocations (but not continuation resumption), the interpreter checks the cached state before executing the method body:
+
+```csharp
+InterpOrR2R ReenterDiscoveredMethod(PEP pep)
+{
+ if (!pep.HasFlag(kPrefersInterpreterEntryPoint))
+ {
+ return InterpOrR2R.R2R;
+ }
+
+ var info = pep.InterpreterData.AuxiliaryMethodInfo;
+ bool ready = info.Kind switch
+ {
+ CheckActivationFlag => IsActivated(info.ActivationFlagAddress),
+ CheckEra => info.Era != CurrentAuxiliaryEntryPointsEra,
+ _ => false,
+ };
+
+ if (!ready)
+ {
+ return InterpOrR2R.Interp;
+ }
+
+ // Clear before the lookup and fixups so a reentrant call does not activate the method twice.
+ pep.ClearFlag(kAwaitingAuxiliaryEntryPoints);
+ pep.ClearFlag(kPendingAuxiliaryModule);
+ return DiscoverMethod(pep);
+}
+```
+
+## Partitioned Data
+
+### RuntimeFunctions
+
+A single logical `RuntimeFunctions` section exists, spread across the primary module and auxiliary modules. The primary module contains contiguous entries for the methods it contains, as well as the last entry for the last function in the last aux module (and the unwind data it points to), and zeroed memory for auxiliary modules to fill in during activation. The last entry in the section allows the runtime to determine the total VirtualIP allocation required for the virtual module. When a module is activated, it fills in its allotted range in the RuntimeFunctions table for activation of its methods.
+
+VirtualIP lookup must use a range-aware binary search that only searches the ranges of activated modules, using the AuxiliaryModuleMetadata activation info to determine active ranges.
+
+```text
+AuxiliaryModuleMetadata RuntimeFunctions table
+ Index Owning module
+ ┌─────┬───────────────┐
+Header: ┌─┤ 0 │ Primary │
+ PrimaryModuleMethodCount = 3 ─────────────►┤ │ 1 │ Primary │
+ AuxiliaryModuleCount = 2 └─┤ 2 │ Primary │
+ ├─────┼───────────────┤
+Entries: ┌─┤ 3 │ Aux A │
+ Aux A │ │ 4 │ Aux A │
+ MethodCount = 4 ────────────────────────►┤ │ 5 │ Aux A │
+ RelativeTableBase = 3 └─┤ 6 │ Aux A │
+ ├─────┼───────────────┤
+ Aux B ┌─┤ 7 │ Aux B │
+ MethodCount = 3 ────────────────────────►┤ │ 8 │ Aux B │
+ RelativeTableBase = 7 └─┤ 9 │ Aux B │
+ └─────┴───────────────┘
+```
+
+The entire table is allocated space in the primary module. Slots 0-2 are populated, slots 3-8 zeroed, and slot 9 (plus its unwind data) is populated for VirtualIP sizing. The method in slot 9 is still "owned" by Auxiliary module B, and is included in its MethodCount.
+
+### MethodDefEntryPoints and InstanceMethodEntryPoints
+
+Each `MethodDefEntryPoints` section (one for each assembly in the R2R image), as well as the image-wide `InstanceMethodEntryPoints` section (shared by all assemblies in the R2R image), will have a partial copy in each of the primary module and the Auxiliary EntryPoints Module. Fixup lists for the entries in the Auxiliary EntryPoints Module exist in the auxiliary modules. The runtime must check both sections when the Auxiliary EntryPoints Module is downloaded. The primary module will allocate space for each section at the RVA designated in the Auxiliary Module Metadata. The entries for each method and their RuntimeFunctions index are present in the Auxiliary EntryPoints Module, but the fixup lists are placed in the auxiliary modules and copied during activation.
+
+### ImportSection
+
+The `ImportSection` (fixups) will be split among primary and auxiliary modules. Each module will have a range of `READYTORUN_IMPORT_SECTION` descriptors for the methods contained within each module. The primary module will allocate space in the WebCil `ImportSection` for auxiliary modules to copy their ImportSection entries, as well as the fixup signatures, non-zero import cells, and auxiliary data to their respective RVAs in the data section. Methods in the auxiliary module may reference fixups in the primary module, but auxiliary modules may not reference fixups owned by other auxiliary modules.
+
+## Primary Module
+
+### Data layout
+
+The primary module defines a data segment large enough to hold all relevant data for the logical module, including data supplied by auxiliary modules.
+
+Regions whose contents belong exclusively to auxiliary modules are initially filled with zeros. Zero bytes in a data segment are stored literally, so they cost file size before compression but compress very well.
+
+### Callable methods and thunks
+
+The primary module contains:
+
+- All string-referenced thunks.
+- Shared R2R-to-interpreter thunks, one per signature shape needed by auxiliary methods. An unactivated aux method's PEP points at the thunk for its signature.
+- An element segment defining the primary module's own callable methods and thunks. The slot range reserved for methods in auxiliary modules is left empty and is filled during activation of the auxiliary module.
+- `MethodDefEntryPoints` and `InstanceMethodEntryPoints` for the methods in the primary module.
+
+### Method activation behavior
+
+If activation is unsuccessful because fixup resolution fails (or throws), the `kPrefersInterpreterEntryPoint` flag is left set. Method activation should not be retried after a failure. After failed activation, the runtime should treat the method as any other interpreter-only method, including resetting the cached interpreter method data so calls no longer re-enter the prestub.
+
+## Auxiliary Modules
+
+An auxiliary module contains:
+
+- The function bodies of the contained methods.
+- The RuntimeFunctions table for the contained methods.
+- The fixup data for the contained methods.
+- An Element segment of funcrefs to its contained methods to fill its allotted range in table 0 during activation.
+- Any other data required by the module but able to be removed from the primary module.
+
+It imports:
+
+- The primary module's memory and function table.
+- The primary module's base memory address and base table index.
+- (future, optional) functions from the primary module, such as R2R helpers or commonly called managed methods. This enables direct calls to these methods in the primary module.
+
+It exports:
+
+- `ActivateModule()`, a method which copies the module's data into its allotted range in the primary module's memory, installs funcrefs to all of the module's compiled methods and funclets into its allotted range of table 0, and marks itself as activated via a well-known flag in the primary module's memory. Note that installing a funcref does not mean the method is activated and ready to be executed.
+
+## Auxiliary EntryPoints Module
+
+The Auxiliary EntryPoints Module holds information about what methods have code in any auxiliary module. There is one AuxiliaryEntryPointsModule per logical R2R module. It contains one or more `MethodDefEntryPoints` sections (one for each assembly in the R2R image), and one `InstanceMethodEntryPoints` (for the entire image). Together, these hold a mapping from method to `RuntimeFunctions` index. Each section covers only the aux methods, excluding the fixup cell info. These sections tell which index in the `RuntimeFunctions` table to find information for the method, as well as which fixups to resolve. A single logical `RuntimeFunctions` is spread across the primary and auxiliary modules. Using the length of the primary module's `RuntimeFunctions` range, and the `MethodCount` field in each Auxiliary module metadata entry (see below), the owning auxiliary module can be determined.
+
+The EntryPoints sections in the Auxiliary EntryPoints Module will leave space for the fixup lists that will be filled in by the auxiliary module during activation. To enable auxiliary modules to activate before the Auxiliary EntryPoints Module, this space should not be overwritten during activation of the Auxiliary EntryPoints Module. Fixups for methods from the auxiliary module cannot be run until the module is activated and these indices are filled in. This enables the EntryPoints Module to be smaller, and allows methods without R2R code to be identified as interpreter-only sooner.
+
+### Auxiliary MethodDefEntryPoints format
+
+The auxiliary `MethodDefEntryPoints` section for each assembly may have code spread across multiple auxiliary modules. The AuxiliaryEntryPointsModule will hold the sparse array metadata (header and block-index array), as well as the `RuntimeFunctions` index and flags for each entry, and backwards-distance-encoded offset to the method's fixup list. The actual fixup list will be owned by the Auxiliary Module that holds the code for that method. Each auxiliary module will be allocated contiguous space for all its encoded fixup lists in a blob before the array. This enables the auxiliary module's `ActivateModule()` function to simply copy a data element to a known location.
+
+```
+Lower addresses
+┌───────────────────────────────────┐
+│ Aux A fixup lists │ ◄── installed by Aux A
+├───────────────────────────────────┤
+│ Aux B fixup lists │ ◄── installed by Aux B
+├───────────────────────────────────┤
+│ Sparse-array header │ ◄── R2R directory points HERE
+│ Block-index array │
+│ Tree nodes and entrypoint records │
+│ [method index + flags][distance]│
+└───────────────────────────┬───────┘
+ │
+ backward reference to the relevant list
+Higher addresses
+```
+
+### Auxiliary InstanceMethodEntryPoints format
+
+The AuxiliaryEntryPointsModule contains the `InstanceMethodEntryPoints` hash table metadata and the method records it points to. Each record contains the method signature, the encoded `RuntimeFunctions` index and flags, and, when fixups are required, a backward distance to its fixup list. Each auxiliary module has a contiguous reserved region before its method records for its encoded fixup lists. The auxiliary module copies only those lists into the reserved region during activation; the signatures, indices, flags, and distances are supplied by the AuxiliaryEntryPointsModule.
+
+```
+Hash index (supplied by AuxiliaryEntryPointsModule)
+
+Bucket 0 ─── [low hash][relative offset] ────┐
+ └── [low hash][relative offset] ────┼───────────────────────────────────────────────┐
+Bucket 1 ─── [low hash][relative offset] ─┐ │ │
+ │ │ Aux A storage │ Aux B storage
+ │ │ Lower addresses │ Lower addresses
+ │ │ ┌───────────────────────────────────┐ │ ┌───────────────────────────────────┐
+ │ │ │ Reserved fixup-list space │◄─┐ │ │ Reserved fixup-list space │◄─┐
+ │ │ │ (filled by Aux A) │ │ │ │ (filled by Aux B) │ │
+ │ │ ├───────────────────────────────────┤ │ │ ├───────────────────────────────────┤ │
+ └──┼─►│ A1 signature │ │ └──►│ B1 signature │ │
+ │ │ RuntimeFunctions index + flags │ │ │ RuntimeFunctions index + flags │ │
+ │ │ backward distance │──┤ │ backward distance │──┤
+ │ ├───────────────────────────────────┤ │ ├───────────────────────────────────┤ │
+ └─►│ A2 signature │ │ │ B2 signature │ │
+ │ RuntimeFunctions index + flags │ │ │ RuntimeFunctions index + flags │ │
+ │ backward distance │──┘ │ backward distance │──┘
+ └───────────────────────────────────┘ └───────────────────────────────────┘
+ Higher addresses Higher addresses
+
+Method records are supplied by AuxiliaryEntryPointsModule.
+Each backward distance identifies that record's list within the reserved space.
+The illustrated records have fixups; their index-and-flags field is encoded as
+(RuntimeFunctions index << 2) | 3. Records without fixups omit the distance.
+```
+
+## Auxiliary Module Metadata
+
+Metadata for each auxiliary module resides at a well-defined location in the primary module, probably in a new section. This metadata enables the runtime to activate the module.
+
+Header:
+
+| Field | Meaning |
+|---|---|
+| `InstanceMethodEntryPointsRVA` | RVA of the auxiliary modules' InstanceMethodEntryPoints section |
+| `MethodDefEntryPointsInfoRVA` | RVA of an array of (IMAGE_DATA_DIRECTORY `CorHeader`, IMAGE_DATA_DIRECTORY `MethodDefEntryPointsRVA`) tuples that allow the runtime to resolve an assembly's auxiliary MethodDefEntryPoints section |
+| `EntryPointsModuleActivationStatus` | Flag indicating whether the Auxiliary EntryPoints Module has completed activation and the auxiliary `InstanceMethodEntryPoints` and `MethodDefEntryPoints` tables are valid for reading. This also could be a flag on the ModuleDesc. |
+| `PrimaryModuleMethodCount` | The count of functions (including funclets) that have code and a `RuntimeFunctions` entry in the primary module. This doesn't include the final `RuntimeFunctions` entry used for VirtualIP calculations. |
+| `AuxiliaryModuleCount` | Count of auxiliary modules |
+
+Following the header is the Auxiliary Module Metadata table. The table is an array of entries with the following fields:
+
+| Field | Meaning |
+|---|---|
+| `FilenameRVA` | RVA of the string naming the auxiliary module. |
+| `RelativeTableBase` | Primary-module-relative table index of the first slot assigned to the auxiliary module. |
+| `MethodCount` | Number of method slots assigned to the auxiliary module, including funclets. Defines the number of entries in its `RuntimeFunctions` table and the length of its contiguous function-table range. |
+| `ActivationStatus` | Flag indicating whether the auxiliary module has completed activation and its methods can be activated and the R2R code executed. This also could be a flag on the ModuleDesc. |
+
+The `MethodDefEntryPointsInfoRVA` points to data identical to the format of the `ComponentAssemblies` section. Once filled in by the Auxiliary EntryPoints Module, the `CorHeader` will point to the same location in the assembly as the entry in the `ComponentAssemblies` section (right now crossgen2 emits these as zeros and relies on correlated order with the `ManifestMetadata` assembly names - this ordering must be preserved). The `MethodDefEntryPointsRVA` will point to the auxiliary `MethodDefEntryPoints` section for that assembly. All this data would reside within the Auxiliary Module Metadata section.
+
+### Implemented alternative data format
+
+V1 uses separate sections for the image-wide `AuxiliaryInstanceMethodEntryPoints` and per-assembly `AuxiliaryMethodDefEntryPoints`, rather than the two entrypoint-directory fields described above. ReadyToRun format version 34 identifies this layout, so older runtimes do not search an incomplete runtime-function table.
+
+| Section | Value | Scope |
+|---|---|---|
+| `AuxiliaryModuleMetadata` | 128 | Image-wide |
+| `AuxiliaryMethodDefEntryPoints` | 129 | Per component assembly |
+| `AuxiliaryInstanceMethodEntryPoints` | 130 | Image-wide |
+
+The metadata header consists of four 32-bit fields, in order: `EntryPointsModuleActivationStatus`, `PrimaryModuleMethodCount`, `AuxiliaryModuleCount`, and `EntryPointsFilenameRVA`. It is followed by `AuxiliaryModuleCount` entries, each containing four 32-bit fields: `FilenameRVA`, `RelativeTableBase`, `MethodCount`, and `ActivationStatus`. Filename RVAs are relative to the primary WebCIL payload and identify sibling basenames. Real activation cells are `0` while pending and `1` when active. The primary export `getAuxiliaryModuleMetadata()` returns the metadata's address in shared memory.
+
+Virtual-IP lookup searches only active contiguous ranges and checks a function's own unwind data for its end address. Funclet enumeration is bounded by the owning module's runtime-function range, rather than reading unloaded entries in the next range.
+
+### Index relationships
+
+Each auxiliary module's allotted range in the function table is contiguous and has a one-to-one correspondence with its local `RuntimeFunctions` entries, including funclets.
+
+A method's logical `RuntimeFunctions` index is equal to the sum of `PrimaryModuleMethodCount` and preceding auxiliary modules' `MethodCount`, plus its local index within that range.
+
+A method's index in the global function table (table 0) is the `__table_base` of the primary module plus the method's logical-module-relative `RuntimeFunctions` index. That is, ` = __table_base + ` where the resolved RuntimeFunctions index is the index resolved in the MethodDefEntryPoints or InstanceMethodEntryPoints table.
+
+The count of entries in the auxiliary module's `RuntimeFunctions` table must be equal to the `AuxiliaryModuleMetadata`'s `MethodCount` field for that module.
+
+This relationship between `RuntimeFunctions` index and function table index (relative to __table_base) must also hold for the primary module.
+
+## Compiler Changes
+
+### Assign methods to physical modules
+
+The compiler must decide which methods belong in the primary module and which belong in auxiliary modules.
+
+The initial implementation will support a **hot/cold split**:
+
+- A PGO- or heuristic-selected hot set goes into the primary module.
+- Remaining methods go into a single auxiliary module.
+
+For demonstration purposes, the initial heuristic can classify **helpers and public non-generic methods as hot**, with everything else classified as cold. This is intended as a simple implementation for the proof of concept, not as a long-term solution.
+
+The design also permits future arrangements such as:
+
+- Hot/warm/cold modules.
+- A hot primary module plus per-assembly auxiliary modules.
+
+Auxiliary modules use indexed filenames, for example:
+
+```text
+app.r2r.1.wasm
+app.r2r.2.wasm
+```
+
+### Partition tables and generate fallback thunks
+
+The compiler must:
+
+- Split the `RuntimeFunctions` entries and GC and EH info across auxiliary modules, keeping data for each auxiliary module contiguous, and emitting the last entry of the table to the primary module.
+- Arrange function-table indices so that the primary module's R2R methods are contiguous and each auxiliary module's R2R methods occupy a contiguous range.
+- Include funclet slots in each AuxiliaryModuleMetadata's `MethodCount` and preserve the one-to-one correspondence between that range and its local `RUNTIME_FUNCTION` records.
+- Generate R2R-to-interpreter thunks for every signature shape needed by auxiliary methods. The primary module holds one thunk per shape, and an unactivated auxiliary method's PEP names the thunk for its signature. The primary module's element segment does not reserve a thunk per auxiliary slot.
+- Emit in each auxiliary module an element segment that fills its slot range in the imported table 0 with that module's methods and funclets, so `ActivateModule` can install every funcref.
+
+### Partition data and emit auxiliary metadata
+
+The compiler must identify memory regions belonging to auxiliary modules and communicate that ownership to the Wasm module writers. Memory owned by the auxiliary modules must not include any target locations of any fixups that may run before the auxiliary module is loaded. The auxiliary module should be able to use `memory.init` instructions to fill data, and must not overwrite any fixup cells that have been resolved.
+
+The compiler must also:
+
+- Generate auxiliary-module metadata at a well-known location discoverable by the runtime.
+- Generate `MethodDefEntryPoints` and `InstanceMethodEntryPoints` sections for the primary module and Auxiliary EntryPoints Module.
+- Adjust code-generation rules so auxiliary code can be loaded safely.
+- Retain IL for any method assigned to an auxiliary module.
+
+## Runtime Changes
+
+### Multiple runtime-function tables
+
+A logical R2R module can have two `MethodDefEntryPoints` per assembly and two `InstanceMethodEntryPoints` per image. The runtime must attempt to resolve R2R code using the Auxiliary EntryPoints Module data once it has been activated.
+
+A module's `RuntimeFunctions` table may be only partially filled. VirtualIP lookup must use a range-aware binary search that only searches the ranges of activated modules, using the AuxiliaryModuleMetadata activation info to determine active ranges.
+
+### Interpreter changes
+
+The interpreter must implement the tracking and activation flow defined above. Call sites check the cached state on the interpreter method data, which is shared by every alias of the same bytecode, such as the declaration of a `MethodImpl` that remaps a vtable slot within its own type. The state is not cleared when R2R code is published, so calls through such an alias still reach the prestub and are redirected to the published native code.
+
+## JavaScript Hosting Changes
+
+The JavaScript host must:
+
+1. Trigger auxiliary-module downloads (including the Auxiliary EntryPoints Module), either eagerly or in response to method activation requests.
+2. Instantiate each auxiliary module with the required imports.
+3. Call that module's `ActivateModule` export.
+
+## Implementation phases
+
+V1 will focus on single-threaded, composite mode support. The designs shouldn't make multithreaded WASM or non-composite mode incompatible, but it will be disabled for those configurations in the initial implementation.
+
+### 1. Support delayed resolution of R2R code within the same module.
+
+Using either call-counting in the interpreter or a timer in the host JavaScript code, force methods to be interpreted (even if R2R code exists for them in the primary module) before installing R2R code. This doesn't require any changes to crossgen2 or the webcil format.
+
+The prototype simulates both phases of the flow above and is compiled with `FEATURE_AUXILIARY_R2R_MODULES`, enabled by default on single-threaded Browser-WASM builds with `FEATURE_INTERPRETER`. It can be excluded with `-cmakeargs "-DFEATURE_AUXILIARY_R2R_MODULES=0"`. This prototype does not split modules, download code, or change image formats.
+
+**Simulated downloads.** Each simulated download consumes a configured number of call-boundary checks. The simulated EntryPoints countdown saturates at zero. The simulated body-module cell encodes the remaining count plus one and saturates at one, keeping its ready value consistent with real activation cells while distinguishing countdowns from a real pending cell (`0`). Preparing a method, resuming an existing interpreter continuation, and executing an exception clause do not consume a count.
+
+- `DOTNET_R2RDelayedActivationCallCount` simulates the auxiliary module download. Each composite image has its own countdown, shared by all its component assemblies and by every method waiting on that image. It defaults to `0`, marking auxiliary code ready immediately and preserving eager R2R resolution. Setting it to `3e8` (hexadecimal for 1,000) marks the image ready after 1,000 checks.
+- `DOTNET_R2RDelayedEntryPointsActivationCallCount` simulates the Auxiliary EntryPoints Module download. A single countdown stands in for every composite image, and counts calls to methods awaiting EntryPoints. It defaults to `DOTNET_R2RDelayedActivationCallCount` when not specified. When it reaches zero, the auxiliary EntryPoints era advances.
+
+**Eligibility.** Delay applies to IL methods in a composite image whose IL bodies are retained, including when generic code is found outside its defining assembly. Wrapper stubs, `UnmanagedCallersOnly` methods, methods whose defining module has stripped IL, and lookups for direct calls from R2R code (which cannot wait) keep their existing preparation paths. Non-composite images and images with stripped IL are unchanged. This is decided once, before the EntryPoints-activation check in the R2R lookup, so an ineligible method is never kept interpreted merely because its image's EntryPoints are inactive.
+
+**Phase 1: awaiting EntryPoints.** While an image's EntryPoints module is inactive, the lookup treats its methods as not found. When any lookup misses, the method records the era read before the lookup began and waits for the era to advance. The era is a static field next to the list of unrelated R2R modules. It advances when the EntryPoints simulation completes and when a new image is registered for unrelated generics, since either can make a previous miss succeed. The era is an opaque type in `auxiliaryentrypointsera.h`, shared with the interpreter compiler because `InterpMethod` stores it; only `ReadyToRunInfo` creates or compares eras. Because a miss always waits, interpreted methods with no R2R code do a cheap era check on each call and repeat their lookup once per era advance.
+
+**Phase 2: pending module.** When the lookup finds the method in an image whose auxiliary module is not yet activated, it records a pointer to that image's activation flag and stops searching other images. Generic code found outside the defining assembly records the code-owning image's flag, not the defining module's.
+
+**Cached state.** The state is cached in an `AuxiliaryMethodInfo` on `InterpMethod`, a discriminated union of the activation flag pointer and the era. `ShouldTryActivateMethod()` advances the corresponding simulated countdown and reports whether the call should try activation. The method's portable entrypoint also carries `kPendingAuxiliaryModule` or `kAwaitingAuxiliaryEntryPoints`, and the method's runtime code data holds the same pointer or era for the prestub.
+
+**Activation.** Once ready, activation runs synchronously in the prestub, repeating the lookup with the existing R2R fixup processing before publishing native code through the method's portable entrypoint. Call sites only check readiness, then call the prestub as they would for an unprepared method. Readiness is checked at call boundaries, before the callee's interpreter frame exists. Interpreter-to-interpreter calls (including `calli` and delegate calls) check in `ResolveInterpreterCode`. Calls from R2R code arrive through the R2R-to-interpreter thunk, which checks in `ExecuteInterpretedMethodWithArgs_PortableEntryPoint` and activates under a `PrestubMethodFrame` so the incoming arguments stay reported to the GC. Debugger function evaluation, the remaining caller of `CallDescrWorkerInternal`, checks there and activates the same way. When activation succeeds, the call that observed readiness runs the native code. Resuming an interpreter continuation or executing an exception clause never checks readiness, so a method with a suspended interpreted continuation activates on its next fresh call. A method prepared for the first time after activation uses ordinary eager R2R resolution. Interpreter dispatch caches are bypassed after publication, while active interpreter frames and suspended interpreter continuations retain their bytecode.
+
+**Failure.** The pending and awaiting flags are cleared before the lookup and fixups to prevent reentrant activation. If the lookup finds no R2R body again, the method records the new era or flag and keeps waiting. If fixups reject the R2R body or throw, the method stays interpreted, its cached `AuxiliaryMethodInfo` is reset so later calls skip the prestub, and activation is not retried.
+
+### 2. Split InstanceMethodEntryPoints and MethodDefEntryPoints sections out
+
+Move entries of the `InstanceMethodEntryPoints` and `MethodDefEntryPoints` sections into the Auxiliary EntryPoints Module during object emission in crossgen2. Implement activation of the Auxiliary EntryPoints Module in the host. Update the runtime to check both once the Auxiliary EntryPoints Module is activated. All other data is left in the primary module and activation of the Auxiliary EntryPoints Module will also mark auxiliary modules as activated so that activation of those methods can complete.
+
+### 3. Split some wasm function bodies into the cold module.
+
+Leave all other R2R metadata (fixups, EH and GC info, etc) in the primary module, but move some cold wasm method bodies to a cold module. `ActivateModule` only has to update the global function table. Requires changes from crossgen2, the host, and the runtime.
+
+### 4. Move metadata for aux methods into the auxiliary module.
+
+Move the `RuntimeFunctions` table entries and fixups to the auxiliary module, and have method activation resolve this info using the Auxiliary Module Metadata entries.
+
+## Appendix
+
+### Direct-call dependency sets
+
+Ordinary managed direct calls within an auxiliary module will not be implemented in V1, and this section is irrelevant until they are implemented. The eligible managed compiler-helper copies described above are a separate validated exception; they do not enable these general dependency sets. This section is left for completeness.
+
+Activating method A must also complete the required fixups for any method B that A calls directly, even if B was not independently requested. All the methods that are direct-called by A or direct called by any of those methods, recursively, are considered the direct-call dependency set for root method A. Activation of all methods in a dependency set must succeed for the root method to be successfully activated. Subsets of the dependency set of A may succeed even if other subsets fail - they are not necessary all-or-none.
+
+An aux method may be **unvisited** (it has no interpreter bytecode or R2R code), **interpreted** (`kPrefersInterpreterEntryPoint` is set), **activation-in-progress** (currently being activated, and present in a visited set during recursive activation of direct-called methods), or **activated** (`kPrefersInterpreterEntryPoint` not set). Encountering an activation-in-progress dependency must record the dependency rather than infinitely recursing and restarting its activation.
+
+`READYTORUN_FIXUP_MethodEntry_ReadyToRun` already requests target-method fixup processing. These are implemented as Eager imports which run at module load. This does not work for aux methods whose data and fixups are not present in the primary module. These fixups for aux modules must run either during auxiliary module activation, or be modified to be run as pre-code fixups for methods that have direct calls (which requires handling cycles of these fixups - e.g. M1 direct calls M2 which direct calls M1).
+
+Ordinary managed direct calls within an auxiliary module are not supported in V1. They can be implemented later, subject to this activation requirement.
+
+Direct calls from the primary module to an auxiliary module are not supported.
+
+Direct calls from one auxiliary module to another are not supported.
+
+Direct calls from an auxiliary module to the primary module are possible through complex collaboration between the host, exported methods in the primary module, and required imports in the auxiliary module. The required complexity of an implementation is not justified for V1, and is not supported.
+
+> [!NOTE]
+> This document was written and edited with assistance from GitHub Copilot.
diff --git a/eng/native.wasm.targets b/eng/native.wasm.targets
index 50b8b62332d981..62480ab8ddb277 100644
--- a/eng/native.wasm.targets
+++ b/eng/native.wasm.targets
@@ -137,6 +137,7 @@
+
diff --git a/eng/testing/tests.browser.targets b/eng/testing/tests.browser.targets
index a2eae35f007daf..d396371b6bfb25 100644
--- a/eng/testing/tests.browser.targets
+++ b/eng/testing/tests.browser.targets
@@ -348,6 +348,8 @@
+
diff --git a/src/coreclr/clrdefinitions.cmake b/src/coreclr/clrdefinitions.cmake
index da196ef4cb99a9..79fe50bf8180a9 100644
--- a/src/coreclr/clrdefinitions.cmake
+++ b/src/coreclr/clrdefinitions.cmake
@@ -129,6 +129,9 @@ if (CLR_CMAKE_TARGET_WIN32)
endif (CLR_CMAKE_TARGET_WIN32)
add_compile_definitions($<${FEATURE_INTERPRETER}:FEATURE_INTERPRETER>)
+if (FEATURE_AUXILIARY_R2R_MODULES)
+ add_compile_definitions(FEATURE_AUXILIARY_R2R_MODULES)
+endif()
if (FEATURE_DYNAMIC_CODE_COMPILED)
add_compile_definitions(FEATURE_DYNAMIC_CODE_COMPILED)
endif()
diff --git a/src/coreclr/clrfeatures.cmake b/src/coreclr/clrfeatures.cmake
index 040fe3d8516291..5c5a1621a17f14 100644
--- a/src/coreclr/clrfeatures.cmake
+++ b/src/coreclr/clrfeatures.cmake
@@ -104,6 +104,19 @@ if(NOT DEFINED FEATURE_INTERPRETER)
endif()
endif()
+# Auxiliary R2R modules currently require single-threaded Browser-WASM and interpreter fallback.
+if(NOT DEFINED FEATURE_AUXILIARY_R2R_MODULES)
+ set(FEATURE_AUXILIARY_R2R_MODULES 0)
+ if(CLR_CMAKE_TARGET_BROWSER AND CLR_CMAKE_TARGET_ARCH_WASM AND FEATURE_INTERPRETER AND NOT FEATURE_MULTITHREADING)
+ set(FEATURE_AUXILIARY_R2R_MODULES 1)
+ endif()
+endif()
+
+if(FEATURE_AUXILIARY_R2R_MODULES AND
+ (NOT CLR_CMAKE_TARGET_BROWSER OR NOT CLR_CMAKE_TARGET_ARCH_WASM OR NOT FEATURE_INTERPRETER OR FEATURE_MULTITHREADING))
+ message(FATAL_ERROR "FEATURE_AUXILIARY_R2R_MODULES requires single-threaded Browser-WASM with FEATURE_INTERPRETER.")
+endif()
+
if(NOT DEFINED FEATURE_STANDALONE_GC)
set(FEATURE_STANDALONE_GC 1)
endif(NOT DEFINED FEATURE_STANDALONE_GC)
diff --git a/src/coreclr/hosts/corerun/wasm/libCorerun.js b/src/coreclr/hosts/corerun/wasm/libCorerun.js
index c2d8598af73c9a..4df60c79ee820d 100644
--- a/src/coreclr/hosts/corerun/wasm/libCorerun.js
+++ b/src/coreclr/hosts/corerun/wasm/libCorerun.js
@@ -20,6 +20,145 @@ function libCoreRunFactory() {
}
const mergeCoreRun = {
$CORERUN: {
+ auxiliaryModuleLoads: [],
+ waitForAuxiliaryModules: async () => {
+ await Promise.resolve();
+ await Promise.all(CORERUN.auxiliaryModuleLoads);
+ },
+ installAuxiliaryModuleGates: () => {
+ if (ENV["DOTNET_WasmAuxiliaryManualActivation"] !== "1") return;
+ const releases = {};
+ CORERUN.auxiliaryModuleGates = {};
+ CORERUN.auxiliaryModuleCompletions = { ENTRYPOINTS: [], MODULES: [] };
+ const requested = new Set();
+ let preceding = Promise.resolve();
+ for (const stage of ["ENTRYPOINTS", "MODULES"]) {
+ CORERUN.auxiliaryModuleGates[stage] = new Promise(resolve => releases[stage] = resolve);
+ }
+ const observeLine = message => {
+ for (const stage of ["ENTRYPOINTS", "MODULES"]) {
+ if (message.trim() === "AUXILIARY_R2R_ACTIVATE_" + stage && !requested.has(stage)) {
+ requested.add(stage);
+ const previous = preceding;
+ preceding = Promise.all(CORERUN.auxiliaryModuleCompletions[stage]);
+ // Console.WriteLine may still have native frames on the stack.
+ setTimeout(() => releases[stage](previous), 0);
+ }
+ }
+ };
+ const originalOut = out;
+ out = message => {
+ originalOut(message);
+ observeLine(message);
+ };
+ const installWriteObserver = () => {
+ const write = FS.write;
+ const decoder = new TextDecoder();
+ let pending = "";
+ FS.write = (stream, buffer, offset, length, ...args) => {
+ const written = write.call(FS, stream, buffer, offset, length, ...args);
+ if (stream.fd === 1) {
+ const lines = (pending + decoder.decode(buffer.subarray(offset, offset + written), { stream: true })).split("\n");
+ pending = lines.pop();
+ lines.forEach(observeLine);
+ }
+ return written;
+ };
+ };
+ // NODERAWFS bypasses out, and replaces FS.write after library postsets run.
+ const preRun = Module["preRun"];
+ Module["preRun"] = typeof preRun === "function"
+ ? [preRun, installWriteObserver]
+ : [...preRun || [], installWriteObserver];
+ },
+ queueAuxiliaryModules: (instance, imports, payloadPtr, payloadSize, tableSize, wasmPath) => {
+ if (instance.exports.getAuxiliaryModuleMetadata === undefined) return;
+ if (typeof instance.exports.getAuxiliaryModuleMetadata !== "function") throw new Error("Invalid getAuxiliaryModuleMetadata export");
+ const address = instance.exports.getAuxiliaryModuleMetadata() >>> 0;
+ if (address === 0) return;
+ const end = payloadPtr + payloadSize;
+ const heap = new Uint8Array(wasmMemory.buffer);
+ if (end > heap.length || address < payloadPtr || address % 4 !== 0 || address + 16 > end) {
+ throw new Error("Auxiliary R2R module metadata is outside the primary payload");
+ }
+ const view = new DataView(wasmMemory.buffer);
+ const count = view.getUint32(address + 8, true);
+ if (count > (end - address - 16) / 16) throw new Error("Truncated auxiliary R2R module metadata");
+ const modules = [];
+ const names = new Set();
+ const addModule = (filenameRva, statusAddress, entrypoints) => {
+ const start = payloadPtr + filenameRva;
+ if (view.getUint32(statusAddress, true) > 1 || start >= end) throw new Error("Invalid auxiliary R2R module metadata");
+ let filenameEnd = start;
+ while (filenameEnd < end && heap[filenameEnd] !== 0) filenameEnd++;
+ if (filenameEnd === end) throw new Error("Unterminated auxiliary R2R module filename");
+ const name = new TextDecoder("utf-8", { fatal: true }).decode(heap.subarray(start, filenameEnd));
+ if (!/^[^./\\][^/\\?#:]*\.wasm$/.test(name) || names.has(name)) throw new Error(`Invalid auxiliary R2R module filename '${name}'`);
+ names.add(name);
+ modules.push({ name, statusAddress, entrypoints });
+ };
+ addModule(view.getUint32(address + 12, true), address, true);
+ let nextTableIndex = view.getUint32(address + 4, true);
+ if (nextTableIndex > tableSize) throw new Error("Invalid primary R2R module table range");
+ for (let i = 0; i < count; i++) {
+ const entry = address + 16 + i * 16;
+ const tableBase = view.getUint32(entry + 4, true);
+ const methodCount = view.getUint32(entry + 8, true);
+ if (tableBase !== nextTableIndex || tableBase + methodCount > tableSize) {
+ throw new Error("Invalid auxiliary R2R module table range");
+ }
+ nextTableIndex += methodCount;
+ addModule(view.getUint32(entry, true), entry + 12, false);
+ }
+ // Native registration finishes after the synchronous probe returns. Never activate in the probe.
+ const directory = wasmPath.substring(0, wasmPath.lastIndexOf("/") + 1);
+ const operations = modules.map(module => Promise.resolve().then(async () => {
+ try {
+ if (new DataView(wasmMemory.buffer).getUint32(module.statusAddress, true) === 1) return;
+ if (CORERUN.auxiliaryModuleGates) {
+ await CORERUN.auxiliaryModuleGates[module.entrypoints ? "ENTRYPOINTS" : "MODULES"];
+ }
+ const delayName = module.entrypoints
+ ? "DOTNET_WasmAuxiliaryEntryPointsDelayMilliseconds"
+ : "DOTNET_WasmAuxiliaryModuleDelayMilliseconds";
+ const delay = Number(ENV[delayName] || 0);
+ if (!Number.isInteger(delay) || delay < 0 || delay > 2147483647) {
+ throw new Error(`Invalid ${delayName}: ${ENV[delayName]}`);
+ }
+ if (delay !== 0) await new Promise(resolve => setTimeout(resolve, delay));
+ const bytes = FS.readFile(directory + module.name);
+ const { instance } = await WebAssembly.instantiate(bytes, imports);
+ if (typeof instance.exports.ActivateModule !== "function") throw new Error("Missing ActivateModule export");
+ if (new DataView(wasmMemory.buffer).getUint32(module.statusAddress, true) === 0) {
+ instance.exports.ActivateModule();
+ if (new DataView(wasmMemory.buffer).getUint32(module.statusAddress, true) !== 1) {
+ throw new Error("ActivateModule did not publish its activation status");
+ }
+ if (module.entrypoints) {
+ if (typeof wasmExports.dotnet_wasm_notify_auxiliary_entrypoints_activated !== "function") {
+ throw new Error("Runtime does not support auxiliary R2R modules");
+ }
+ wasmExports.dotnet_wasm_notify_auxiliary_entrypoints_activated();
+ }
+ }
+ } catch (reason) {
+ const error = new Error(`Failed to load auxiliary R2R module '${module.name}': ${reason instanceof Error ? reason.message : String(reason)}`);
+ err(error.message);
+ return error;
+ }
+ }));
+ if (CORERUN.auxiliaryModuleCompletions) {
+ for (let i = 0; i < modules.length; i++) {
+ CORERUN.auxiliaryModuleCompletions[modules[i].entrypoints ? "ENTRYPOINTS" : "MODULES"].push(operations[i]);
+ }
+ }
+ const completion = Promise.all(operations).then(errors => {
+ const error = errors.find(error => error !== undefined);
+ if (error) throw error;
+ });
+ CORERUN.auxiliaryModuleLoads.push(completion);
+ void completion.catch(() => {});
+ },
selfInitialize: () => {
const browserVirtualAppBase = "/";// keep in sync other places that define browserVirtualAppBase
FS.createPath("/", browserVirtualAppBase, true, true);
@@ -32,6 +171,8 @@ function libCoreRunFactory() {
}
ENV["DOTNET_SYSTEM_GLOBALIZATION_INVARIANT"] = "true";
+ Module["waitForAuxiliaryModules"] = CORERUN.waitForAuxiliaryModules;
+ CORERUN.installAuxiliaryModuleGates();
if (ENVIRONMENT_IS_NODE) {
const original_proc_exit = _proc_exit;
@@ -226,6 +367,7 @@ function libCoreRunFactory() {
var payloadPtr = 0;
var wasmInstance;
+ var imports;
const sp = stackSave();
try {
const ptrPtr = stackAlloc(4);
@@ -242,7 +384,7 @@ function libCoreRunFactory() {
throw new Error("__async_continuation was not preserved by the linker or optimizer");
}
payloadPtr = HEAPU32[ptrPtr >>> 2 >>> 0];
- wasmInstance = new WebAssembly.Instance(wasmModule, {
+ imports = {
webcil: {
memory: wasmMemory,
__stack_pointer: wasmExports.__stack_pointer,
@@ -253,7 +395,8 @@ function libCoreRunFactory() {
// Runtime-async continuation return value, shared with the runtime module.
__async_continuation: wasmExports.__async_continuation
}
- });
+ };
+ wasmInstance = new WebAssembly.Instance(wasmModule, imports);
} catch (e) {
const errorMessage = e instanceof Error ? e.message : String(e);
console.error("Failed to construct WebAssembly instance for Webcil image:", { wasmPath, errorMessage });
@@ -281,6 +424,7 @@ function libCoreRunFactory() {
HEAPU32[outDataStartPtr >>> 2 >>> 0] = payloadPtr;
HEAPU32[outSize >>> 2 >>> 0] = payloadSize;
HEAPU32[(outSize + 4) >>> 2 >>> 0] = 0;
+ CORERUN.queueAuxiliaryModules(wasmInstance, imports, payloadPtr, payloadSize, tableSize, wasmPath);
return true;
}
};
diff --git a/src/coreclr/inc/clrconfigvalues.h b/src/coreclr/inc/clrconfigvalues.h
index 123543729c541f..91f22e55f58de7 100644
--- a/src/coreclr/inc/clrconfigvalues.h
+++ b/src/coreclr/inc/clrconfigvalues.h
@@ -301,6 +301,11 @@ RETAIL_CONFIG_STRING_INFO(EXTERNAL_Interpreter, W("Interpreter"), "Enables Inter
RETAIL_CONFIG_DWORD_INFO(EXTERNAL_InterpMode, W("InterpMode"), 0, "Enables Interpreter for a subset of all methods depending on the specified mode.")
#endif // FEATURE_INTERPRETER
+#ifdef FEATURE_AUXILIARY_R2R_MODULES
+RETAIL_CONFIG_DWORD_INFO(EXTERNAL_R2RDelayedActivationCallCount, W("R2RDelayedActivationCallCount"), 0, "Activate auxiliary R2R code after this many pending interpreter entries across a composite image. Zero preserves eager activation.")
+RETAIL_CONFIG_DWORD_INFO(EXTERNAL_R2RDelayedEntryPointsActivationCallCount, W("R2RDelayedEntryPointsActivationCallCount"), 0, "Activate auxiliary R2R EntryPoints modules after this many calls to methods awaiting them. Defaults to R2RDelayedActivationCallCount when not specified.")
+#endif // FEATURE_AUXILIARY_R2R_MODULES
+
RETAIL_CONFIG_DWORD_INFO(EXTERNAL_JitHostMaxSlabCache, W("JitHostMaxSlabCache"), 0x1000000, "Sets jit host max slab cache size, 16MB default")
RETAIL_CONFIG_DWORD_INFO(EXTERNAL_JitOptimizeType, W("JitOptimizeType"), 0 /* OPT_DEFAULT */, "")
diff --git a/src/coreclr/inc/readytorun.h b/src/coreclr/inc/readytorun.h
index a1e9b3758edfba..0d2850a6f66bf2 100644
--- a/src/coreclr/inc/readytorun.h
+++ b/src/coreclr/inc/readytorun.h
@@ -19,8 +19,8 @@
// src/coreclr/nativeaot/Runtime/inc/ModuleHeaders.h
// If you update this, ensure you run `git grep MINIMUM_READYTORUN_MAJOR_VERSION`
// and handle pending work.
-#define READYTORUN_MAJOR_VERSION 33
-#define READYTORUN_MINOR_VERSION 0x0001
+#define READYTORUN_MAJOR_VERSION 34
+#define READYTORUN_MINOR_VERSION 0x0000
#define MINIMUM_READYTORUN_MAJOR_VERSION 33
@@ -86,6 +86,7 @@
// GCRefMaps, moves VASIG_COOKIE from 5 to 7, and prefixes map-bearing import
// signatures with their GCRefMaps.
// R2R Version 33.1 adds READYTORUN_HELPER_WasmProfSamplepoint (used on WebAssembly)
+// R2R Version 34 adds partitioned WebAssembly images with auxiliary entrypoints and partially populated runtime-function tables.
struct READYTORUN_CORE_HEADER
{
@@ -160,6 +161,9 @@ enum class ReadyToRunSectionType : uint32_t
ProxyTypeMaps = 125, // Added in V18.3
TypeMapAssemblyTargets = 126, // Added in V18.3
WasmAsyncResumeInfo = 127, // Added in V29
+ AuxiliaryModuleMetadata = 128,
+ AuxiliaryMethodDefEntryPoints = 129,
+ AuxiliaryInstanceMethodEntryPoints = 130,
// If you add a new section consider whether it is a breaking or non-breaking change.
// Usually it is non-breaking, but if it is preferable to have older runtimes fail
@@ -174,6 +178,27 @@ struct READYTORUN_SECTION
IMAGE_DATA_DIRECTORY Section;
};
+struct READYTORUN_AUXILIARY_MODULE_METADATA
+{
+ DWORD EntryPointsModuleActivationStatus;
+ DWORD PrimaryModuleMethodCount;
+ DWORD AuxiliaryModuleCount;
+ DWORD EntryPointsFilenameRVA;
+};
+
+struct READYTORUN_AUXILIARY_MODULE
+{
+ DWORD FilenameRVA;
+ DWORD RelativeTableBase;
+ DWORD MethodCount;
+ DWORD ActivationStatus;
+};
+
+static_assert(sizeof(READYTORUN_AUXILIARY_MODULE_METADATA) == 16);
+static_assert(sizeof(READYTORUN_AUXILIARY_MODULE) == 16);
+static_assert(offsetof(READYTORUN_AUXILIARY_MODULE_METADATA, EntryPointsModuleActivationStatus) == 0);
+static_assert(offsetof(READYTORUN_AUXILIARY_MODULE, ActivationStatus) == 12);
+
enum class ReadyToRunImportSectionType : uint8_t
{
Unknown = 0,
diff --git a/src/coreclr/interpreter/inc/auxiliaryentrypointsera.h b/src/coreclr/interpreter/inc/auxiliaryentrypointsera.h
new file mode 100644
index 00000000000000..2517db65dc9716
--- /dev/null
+++ b/src/coreclr/interpreter/inc/auxiliaryentrypointsera.h
@@ -0,0 +1,33 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+// This header is shared by the interpreter and the VM so the era can be stored on InterpMethod.
+#ifndef _AUXILIARYENTRYPOINTSERA_H_
+#define _AUXILIARYENTRYPOINTSERA_H_
+
+#ifdef FEATURE_AUXILIARY_R2R_MODULES
+
+#include
+
+class ReadyToRunInfo;
+
+// The auxiliary EntryPoints era observed by a method whose R2R lookup may have missed because
+// auxiliary EntryPoints modules were not yet activated. Only ReadyToRunInfo creates or compares eras.
+class AuxiliaryEntryPointsEra
+{
+ friend class ReadyToRunInfo;
+
+ // 0 means not awaiting EntryPoints; the global era starts at 1.
+ uint32_t m_value;
+
+ explicit AuxiliaryEntryPointsEra(uint32_t value) : m_value(value) {}
+
+public:
+ AuxiliaryEntryPointsEra() : m_value(0) {}
+
+ bool IsAwaiting() const { return m_value != 0; }
+};
+
+#endif // FEATURE_AUXILIARY_R2R_MODULES
+
+#endif // _AUXILIARYENTRYPOINTSERA_H_
diff --git a/src/coreclr/interpreter/inc/interpretershared.h b/src/coreclr/interpreter/inc/interpretershared.h
index a2676e163c2c07..181294ba572191 100644
--- a/src/coreclr/interpreter/inc/interpretershared.h
+++ b/src/coreclr/interpreter/inc/interpretershared.h
@@ -7,6 +7,7 @@
#define _INTERPRETERSHARED_H_
#include "intopsshared.h"
+#include "auxiliaryentrypointsera.h"
#ifdef _MSC_VER
#define INTERP_API
@@ -32,6 +33,36 @@ class MethodTable;
struct CallStubHeader;
+#ifdef FEATURE_AUXILIARY_R2R_MODULES
+enum class AuxiliaryMethodInfoDiscriminator : uint8_t
+{
+ InterpretOnly,
+ CheckAuxiliaryModuleActivationFlag,
+ CheckAuxiliaryEntryPointsEra
+};
+
+// Cached on InterpMethod so the interpreter can decide with few memory loads whether a call should try to
+// switch the method to R2R code. Set by the VM; the interpreter compiler only default-constructs it.
+struct AuxiliaryMethodInfo
+{
+ union
+ {
+ COMPILER_SHARED_TYPE(uint32_t*, PTR_DWORD, auxiliaryModuleActivationFlag);
+ AuxiliaryEntryPointsEra auxiliaryEntryPointsEra;
+ };
+ AuxiliaryMethodInfoDiscriminator discriminator;
+
+ AuxiliaryMethodInfo() : auxiliaryModuleActivationFlag(nullptr), discriminator(AuxiliaryMethodInfoDiscriminator::InterpretOnly) {}
+
+ bool MayActivateMethod() const { return discriminator != AuxiliaryMethodInfoDiscriminator::InterpretOnly; }
+
+#ifndef INTERPRETER_COMPILER_INTERNAL
+ // Defined in the VM's interpexec.h, since it needs ReadyToRunInfo.
+ inline bool ShouldTryActivateMethod() const;
+#endif // !INTERPRETER_COMPILER_INTERNAL
+};
+#endif // FEATURE_AUXILIARY_R2R_MODULES
+
struct InterpMethod
{
#if DEBUG
@@ -46,6 +77,10 @@ struct InterpMethod
bool initLocals;
bool unmanagedCallersOnly;
bool publishSecretStubParam;
+#ifdef FEATURE_AUXILIARY_R2R_MODULES
+ // Set by the VM when the method is interpreted only until an auxiliary module or EntryPoints module is activated
+ AuxiliaryMethodInfo auxiliaryMethodInfo;
+#endif // FEATURE_AUXILIARY_R2R_MODULES
int32_t codeSize; // size in int32_t slots
// Maps Continuation.State (suspension-point index) to the byte offset of the matching
diff --git a/src/coreclr/nativeaot/Runtime/inc/ModuleHeaders.h b/src/coreclr/nativeaot/Runtime/inc/ModuleHeaders.h
index 88869ab69deb13..c91949a2eb75a0 100644
--- a/src/coreclr/nativeaot/Runtime/inc/ModuleHeaders.h
+++ b/src/coreclr/nativeaot/Runtime/inc/ModuleHeaders.h
@@ -11,8 +11,8 @@ struct ReadyToRunHeaderConstants
{
static const uint32_t Signature = 0x00525452; // 'RTR'
- static const uint32_t CurrentMajorVersion = 33;
- static const uint32_t CurrentMinorVersion = 1;
+ static const uint32_t CurrentMajorVersion = 34;
+ static const uint32_t CurrentMinorVersion = 0;
};
struct ReadyToRunHeader
diff --git a/src/coreclr/tools/Common/Compiler/ObjectWriter/ObjectWriter.cs b/src/coreclr/tools/Common/Compiler/ObjectWriter/ObjectWriter.cs
index b256dfa9769e2d..f6b9689f573e8c 100644
--- a/src/coreclr/tools/Common/Compiler/ObjectWriter/ObjectWriter.cs
+++ b/src/coreclr/tools/Common/Compiler/ObjectWriter/ObjectWriter.cs
@@ -441,10 +441,12 @@ public virtual void EmitObject(Stream outputFileStream, IReadOnlyCollection
_entries.TryGetValue(name, out Entry entry) ? entry : _aliases[name];
public int GetImportCount() => _importCounts.Values.Sum();
+ public int GetImportCount(WasmIndexSpace indexSpace) => _importCounts[indexSpace];
public int GetDefinitionCount(WasmIndexSpace indexSpace) =>
_definitionCounts[indexSpace];
diff --git a/src/coreclr/tools/Common/Compiler/ObjectWriter/WebCilObjectWriter.Auxiliary.cs b/src/coreclr/tools/Common/Compiler/ObjectWriter/WebCilObjectWriter.Auxiliary.cs
new file mode 100644
index 00000000000000..6af38feed3854a
--- /dev/null
+++ b/src/coreclr/tools/Common/Compiler/ObjectWriter/WebCilObjectWriter.Auxiliary.cs
@@ -0,0 +1,592 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#if READYTORUN
+using System;
+using System.Buffers.Binary;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using System.Text.Json;
+using ILCompiler.DependencyAnalysis;
+using ILCompiler.DependencyAnalysis.ReadyToRun;
+using ILCompiler.DependencyAnalysis.Wasm;
+using ILCompiler.DependencyAnalysisFramework;
+using ILCompiler.ObjectWriter.WasmInstructions;
+using Internal.Text;
+
+namespace ILCompiler.ObjectWriter;
+
+internal sealed partial class WebCilObjectWriter
+{
+ private IReadOnlyCollection _partitionNodes;
+ private int[] _physicalFunctionIndices;
+ private int[] _primaryFunctionIndices;
+ private int _activationSignature;
+ private List _auxiliaryDataRanges;
+ private WasmImportLayout _importLayout;
+
+ private readonly struct AuxiliaryDataRange
+ {
+ public readonly int Offset;
+ public readonly int Length;
+ public readonly bool EntryPoints;
+
+ public AuxiliaryDataRange(int offset, int length, bool entryPoints)
+ {
+ Offset = offset;
+ Length = length;
+ EntryPoints = entryPoints;
+ }
+ }
+
+ public override void EmitObject(Stream stream, IReadOnlyCollection nodes, IObjectDumper dumper, Logger logger)
+ {
+ if (_nodeFactory.UsesWasmAuxiliaryModules)
+ {
+ _importLayout = PrepareImportLayout(nodes);
+ nodes = _importLayout.OrderSignatureBlobs(nodes);
+ }
+ _partitionNodes = nodes;
+ base.EmitObject(stream, nodes, dumper, logger);
+ }
+
+ private WasmImportLayout PrepareImportLayout(IReadOnlyCollection nodes)
+ {
+ WasmImportLayout layout = new();
+ HashSet signatureArrays = new();
+ foreach (DependencyNode dependency in nodes)
+ {
+ if (dependency is Import import)
+ {
+ layout.AddImport(import);
+ signatureArrays.Add(import.Table.SignatureArray);
+ }
+ }
+ foreach (DependencyNode dependency in nodes)
+ {
+ if (dependency is not ObjectNode node || node.ShouldSkipEmittingObjectNode(_nodeFactory))
+ continue;
+ bool cold = node is MethodWithGCInfo method && _nodeFactory.IsAuxiliaryMethod(method);
+ if (node is MethodWithGCInfo body)
+ {
+ foreach (ISymbolNode fixup in body.Fixups)
+ layout.AddUse(fixup, cold);
+ }
+ // Signature-array references follow their owning import cells, not the resident container.
+ // Other resident data, including portable import-thunk records, must remain usable immediately.
+ if (node is ISymbolNode symbol && signatureArrays.Contains(symbol))
+ continue;
+ if (node.GetData(_nodeFactory, relocsOnly: true).Relocs is Relocation[] relocations)
+ {
+ foreach (Relocation relocation in relocations)
+ layout.AddUse(relocation.Target, cold);
+ }
+ }
+ layout.Complete(symbol => _nodeFactory.ObjectInterner.GetDeduplicatedSymbol(_nodeFactory, symbol));
+ return layout;
+ }
+
+ private List GetAuxiliaryDataRanges()
+ {
+ List ranges = new();
+ int primaryCount = 0;
+ int totalCount = 0;
+ MethodWithGCInfo lastMethod = null;
+ foreach (MethodWithGCInfo method in _nodeFactory.EnumerateCompiledMethods())
+ {
+ if (!_nodeFactory.IsAuxiliaryMethod(method))
+ primaryCount += method.FrameInfos.Length;
+ totalCount += method.FrameInfos.Length;
+ lastMethod = method;
+ }
+
+ int finalUnwindOffset = lastMethod is null ? -1 :
+ lastMethod.GCInfoNode.CalculateFuncletOffsets(_nodeFactory)[lastMethod.FrameInfos.Length - 1];
+ foreach (DependencyNode dependency in _partitionNodes)
+ {
+ if (dependency is not ObjectNode node || node is not ISymbolNode symbol ||
+ !_definedSymbols.TryGetValue(GetMangledName(symbol), out SymbolDefinition definition) ||
+ _sections[definition.SectionIndex] is not WebcilSection section)
+ continue;
+
+ int offset = checked((int)section.Header.VirtualAddress + (int)definition.Value);
+ int fixupSize = node switch
+ {
+ MethodEntryPointTableNode { IsAuxiliary: true } table => table.Offset,
+ InstanceEntryPointTableNode { IsAuxiliary: true } table => table.Offset,
+ _ => -1,
+ };
+ if (fixupSize >= 0)
+ {
+ Add(offset - fixupSize, fixupSize, entryPoints: false);
+ Add(offset, definition.Size, entryPoints: true);
+ }
+ else if (node is RuntimeFunctionsTableNode)
+ {
+ // The final function and its unwind data remain readable before any sidecar arrives.
+ Add(offset + primaryCount * 8, Math.Max(totalCount - primaryCount - 1, 0) * 8, entryPoints: false);
+ }
+ else if (node is RuntimeFunctionsGCInfoNode gcInfo)
+ {
+ foreach ((int start, int length) in gcInfo.AuxiliaryDataRanges)
+ {
+ if (finalUnwindOffset < start || finalUnwindOffset >= start + length)
+ Add(offset + start, length, entryPoints: false);
+ }
+ }
+ else if (node is EHInfoNode ehInfo)
+ {
+ foreach ((int start, int length) in ehInfo.AuxiliaryDataRanges)
+ Add(offset + start, length, entryPoints: false);
+ }
+ }
+
+ AddExclusiveImports();
+ ranges.Sort(static (left, right) => left.Offset.CompareTo(right.Offset));
+ List merged = new();
+ foreach (AuxiliaryDataRange range in ranges)
+ {
+ if (merged.Count > 0)
+ {
+ AuxiliaryDataRange previous = merged[merged.Count - 1];
+ if (previous.Offset + previous.Length > range.Offset)
+ throw new InvalidDataException("Overlapping auxiliary WebAssembly data ownership.");
+ if (previous.EntryPoints == range.EntryPoints && previous.Offset + previous.Length == range.Offset)
+ {
+ merged[merged.Count - 1] = new AuxiliaryDataRange(previous.Offset,
+ checked(previous.Length + range.Length), range.EntryPoints);
+ continue;
+ }
+ }
+ merged.Add(range);
+ }
+ return merged;
+
+ void Add(int offset, int length, bool entryPoints)
+ {
+ if (length > 0)
+ ranges.Add(new AuxiliaryDataRange(offset, length, entryPoints));
+ }
+
+ void AddExclusiveImports()
+ {
+ foreach (Import import in _importLayout.ColdImports)
+ {
+ AddSymbolRange(import, import.Table.EntrySize);
+ if (_definedSymbols.TryGetValue(GetMangledName(import.Table.SignatureArray), out SymbolDefinition array))
+ {
+ WebcilSection section = (WebcilSection)_sections[array.SectionIndex];
+ Add(checked((int)section.Header.VirtualAddress + (int)array.Value +
+ import.ImportSignature.OffsetFromBeginningOfArray), sizeof(uint), entryPoints: false);
+ }
+ }
+ foreach (ISymbolNode signature in _importLayout.ColdSignatures)
+ AddSymbolRange(signature, length: 0, prefixLength: ((Signature)signature).Offset);
+ }
+
+ void AddSymbolRange(ISymbolNode symbol, int length, int prefixLength = 0)
+ {
+ if (_definedSymbols.TryGetValue(GetMangledName(symbol), out SymbolDefinition definition))
+ {
+ WebcilSection section = (WebcilSection)_sections[definition.SectionIndex];
+ Add(checked((int)section.Header.VirtualAddress + (int)definition.Value - prefixLength),
+ length == 0 ? checked(definition.Size + prefixLength) : length, entryPoints: false);
+ }
+ }
+ }
+
+ private static bool ContainsAuxiliaryData(List ranges, int offset)
+ {
+ int low = 0;
+ int high = ranges.Count - 1;
+ while (low <= high)
+ {
+ int middle = low + (high - low) / 2;
+ AuxiliaryDataRange range = ranges[middle];
+ if (offset < range.Offset)
+ high = middle - 1;
+ else if (offset - range.Offset >= range.Length)
+ low = middle + 1;
+ else
+ return true;
+ }
+ return false;
+ }
+
+ private void EmitPartitionedObjectFile(Stream primaryOutput, WebcilPayloadDataSegment payloadSegment, WebcilSection[] sections)
+ {
+ if (primaryOutput is not FileStream file)
+ throw new InvalidOperationException("Partitioned WebAssembly output requires a file stream.");
+
+ WriteMemoryImport((ulong)payloadSegment.ContentSize);
+ FinalizeSectionEntryCounts();
+
+ using MemoryStream encodedPayload = new();
+ payloadSegment.EmitToStream(encodedPayload);
+ byte[] payload = encodedPayload.ToArray().AsSpan(payloadSegment.HeaderSize).ToArray();
+ List ranges = _auxiliaryDataRanges;
+ byte[] primaryPayload = (byte[])payload.Clone();
+ foreach (AuxiliaryDataRange range in ranges)
+ primaryPayload.AsSpan(range.Offset, range.Length).Clear();
+
+ int metadataOffset = checked((int)ResolveSymbolRVA(sections,
+ _definedSymbols[GetMangledName(_nodeFactory.AuxiliaryModuleMetadata)]));
+ int primaryCount = BinaryPrimitives.ReadInt32LittleEndian(payload.AsSpan(metadataOffset + 4));
+ int coldCount = BinaryPrimitives.ReadInt32LittleEndian(payload.AsSpan(metadataOffset + 24));
+ int managedCount = checked(primaryCount + coldCount);
+ int[] tableFunctions = GetTableSlotFunctionIndices();
+ HashSet hotFunctions = new();
+ HashSet coldFunctions = new();
+ for (int slot = 0; slot < managedCount; slot++)
+ (slot < primaryCount ? hotFunctions : coldFunctions).Add(tableFunctions[slot]);
+ HashSet managedHelperFunctions = new();
+ foreach (MethodWithGCInfo method in _nodeFactory.EnumerateCompiledMethods())
+ {
+ if (!method.IsJitHelper)
+ continue;
+
+ int firstSlot = _nodeFactory.RuntimeFunctionsTable.GetIndex(method);
+ for (int frame = 0; frame < method.FrameInfos.Length; frame++)
+ managedHelperFunctions.Add(tableFunctions[firstSlot + frame]);
+ }
+
+ int functionImports = _wasmSymbolManager.GetImportCount(WasmIndexSpace.Function);
+ List functions = new();
+ foreach (WasmSymbol symbol in _wasmSymbolManager.GetDefinitions(WasmIndexSpace.Function))
+ {
+ if (!symbol.IsImport)
+ functions.Add(symbol);
+ }
+
+ List functionTypes = new();
+ Stream typeIndices = _sections.GetSection(WasmObjectNodeSection.FunctionSection.Name).ContentReadStream;
+ typeIndices.Position = 0;
+ while (typeIndices.Position < typeIndices.Length)
+ functionTypes.Add(checked((int)DwarfHelper.ReadULEB128(typeIndices, out _).Value));
+
+ WriteModule(primaryOutput, physicalModule: 0);
+ string stem = Path.Combine(Path.GetDirectoryName(file.Name), Path.GetFileNameWithoutExtension(file.Name));
+ using (FileStream entryPointsOutput = File.Create(stem + ".entrypoints.wasm"))
+ WriteModule(entryPointsOutput, physicalModule: 1);
+ using (FileStream coldOutput = File.Create(stem + ".1.wasm"))
+ WriteModule(coldOutput, physicalModule: 2);
+ using (FileStream manifestFile = File.Create(stem + ".auxiliary.json"))
+ using (Utf8JsonWriter manifest = new(manifestFile))
+ {
+ manifest.WriteStartObject();
+ manifest.WriteString("entrypoints", Path.GetFileName(stem) + ".entrypoints.wasm");
+ manifest.WriteStartArray("modules");
+ manifest.WriteStringValue(Path.GetFileName(stem) + ".1.wasm");
+ manifest.WriteEndArray();
+ manifest.WriteEndObject();
+ }
+ _physicalFunctionIndices = null;
+
+ void WriteModule(Stream output, int physicalModule)
+ {
+ List kept = new();
+ _physicalFunctionIndices = new int[functionImports + functions.Count];
+ Array.Fill(_physicalFunctionIndices, -1);
+ for (int index = 0; index < functionImports; index++)
+ _physicalFunctionIndices[index] = index;
+ if (physicalModule != 1)
+ {
+ for (int ordinal = 0; ordinal < functions.Count; ordinal++)
+ {
+ int index = functions[ordinal].Index;
+ if (physicalModule != 0 && _wasmStubNames.Contains(functions[ordinal].Name))
+ continue;
+ bool managed = hotFunctions.Contains(index) || coldFunctions.Contains(index);
+ // Helper copies share the primary's logical slots, frame identities and metadata.
+ if (!managed || managedHelperFunctions.Contains(index) ||
+ (physicalModule == 0 ? hotFunctions : coldFunctions).Contains(index))
+ {
+ _physicalFunctionIndices[index] = functionImports + kept.Count;
+ kept.Add(ordinal);
+ }
+ }
+ if (physicalModule == 0)
+ _primaryFunctionIndices = _physicalFunctionIndices;
+ }
+
+ int codeSectionIndex = _sections.GetSectionIndex(ObjectNodeSection.WasmCodeSection.Name);
+ Stream originalCode = _sections[codeSectionIndex].ContentReadStream;
+ originalCode.Position = 0;
+ using MemoryStream resolvedCode = new();
+ if (physicalModule != 1)
+ {
+ ValidateDirectCalls(originalCode, codeSectionIndex);
+ originalCode.Position = 0;
+ if (_resolvableRelocations.TryGetValue(codeSectionIndex, out List relocations))
+ ResolveRelocations(codeSectionIndex, originalCode, resolvedCode, relocations, shrink: true);
+ else
+ originalCode.CopyTo(resolvedCode);
+ }
+ resolvedCode.Position = 0;
+ List codeBlobs = physicalModule == 1 ? new() : ParseCodeBlobs(resolvedCode);
+ List moduleRanges = new();
+ foreach (AuxiliaryDataRange range in ranges)
+ {
+ if (physicalModule != 0 && range.EntryPoints == (physicalModule == 1))
+ moduleRanges.Add(range);
+ }
+
+ EmitWasmHeader(output);
+ _sections.GetSection(ObjectNodeSection.WasmTypeSection.Name).EmitToStream(output);
+ _sections.GetSection(WasmObjectNodeSection.ImportSection.Name).EmitToStream(output);
+ WriteRawSection(output, WasmSectionType.Function, section =>
+ {
+ WriteUnsigned(section, kept.Count + (physicalModule == 0 ? 0 : 1));
+ foreach (int ordinal in kept)
+ WriteUnsigned(section, functionTypes[ordinal]);
+ if (physicalModule != 0)
+ WriteUnsigned(section, _activationSignature);
+ });
+ if (physicalModule == 0)
+ _sections.GetSection(WasmObjectNodeSection.GlobalSection.Name).EmitToStream(output);
+ WriteRawSection(output, WasmSectionType.Export, section =>
+ {
+ WriteUnsigned(section, physicalModule == 0 ? _wasmStubNames.Count + 2 : 1);
+ if (physicalModule == 0)
+ {
+ WriteExport(section, "table", 1, 0);
+ WriteExport(section, "webcilVersion", 3, _definedGlobals["webcilVersion"].Index);
+ foreach (Utf8String name in _wasmStubNames)
+ WriteExport(section, name.ToString(), 0, _physicalFunctionIndices[_wasmSymbolManager.GetSymbol(name).Index]);
+ }
+ else
+ WriteExport(section, "ActivateModule", 0, functionImports + kept.Count);
+ });
+ if (physicalModule != 1)
+ {
+ WriteRawSection(output, WasmSectionType.Element, section =>
+ {
+ if (physicalModule == 0)
+ {
+ WriteUnsigned(section, 2);
+ WriteElements(section, 0, primaryCount, passive: false);
+ WriteElements(section, managedCount, tableFunctions.Length - managedCount, passive: false);
+ }
+ else
+ {
+ WriteUnsigned(section, 1);
+ WriteElements(section, primaryCount, coldCount, passive: true);
+ }
+ });
+ }
+ WriteRawSection(output, WasmSectionType.DataCount, section =>
+ WriteUnsigned(section, physicalModule == 0 ? 2 : moduleRanges.Count));
+ WriteRawSection(output, WasmSectionType.Code, section =>
+ {
+ WriteUnsigned(section, kept.Count + (physicalModule == 0 ? 0 : 1));
+ foreach (int ordinal in kept)
+ {
+ if (functions[ordinal].Name.ToString() == "getAuxiliaryModuleMetadata")
+ {
+ WriteBody(section, new WasmFunctionBody(new WasmFuncType(new([]), new([WasmValueType.I32])),
+ [Global.Get(ImageBaseGlobalIndex), I32.Const(metadataOffset), I32.Add]));
+ }
+ else
+ {
+ CodeBlob blob = codeBlobs[ordinal];
+ WriteUnsigned(section, checked((int)blob.Size));
+ section.Write(resolvedCode.GetBuffer().AsSpan(checked((int)blob.Start), checked((int)blob.Size)));
+ }
+ }
+ if (physicalModule != 0)
+ WriteBody(section, CreateActivationBody(moduleRanges, metadataOffset,
+ physicalModule == 1, primaryCount, coldCount));
+ });
+ WriteRawSection(output, WasmSectionType.Data, section =>
+ {
+ if (physicalModule == 0)
+ {
+ WriteUnsigned(section, 2);
+ section.WriteByte(1);
+ WriteUnsigned(section, 8);
+ Span size = stackalloc byte[8];
+ BinaryPrimitives.WriteInt32LittleEndian(size, primaryPayload.Length);
+ BinaryPrimitives.WriteInt32LittleEndian(size.Slice(4), MethodCount);
+ section.Write(size);
+ section.WriteByte(0);
+ section.WriteByte(0x23);
+ WriteUnsigned(section, ImageBaseGlobalIndex);
+ section.WriteByte(0x0B);
+ WriteUnsigned(section, primaryPayload.Length);
+ section.Write(primaryPayload);
+ }
+ else
+ {
+ WriteUnsigned(section, moduleRanges.Count);
+ foreach (AuxiliaryDataRange range in moduleRanges)
+ {
+ section.WriteByte(1);
+ WriteUnsigned(section, range.Length);
+ section.Write(payload.AsSpan(range.Offset, range.Length));
+ }
+ }
+ }, alignedSuffixLength: physicalModule == 0 ? primaryPayload.Length : 0);
+ if (_emitNameSection)
+ {
+ List names = new();
+ foreach (int ordinal in kept)
+ {
+ WasmSymbol symbol = functions[ordinal];
+ names.Add(symbol with { Index = _physicalFunctionIndices[symbol.Index] });
+ }
+ if (physicalModule != 0)
+ names.Add(new WasmSymbol(new Utf8String("ActivateModule"), WasmIndexSpace.Function,
+ functionImports + kept.Count, IsImport: false, HasExplicitName: false));
+ new WasmNameSection(names).EmitToStream(output);
+ }
+
+ void WriteElements(Stream section, int start, int count, bool passive)
+ {
+ section.WriteByte(passive ? (byte)1 : (byte)0);
+ if (passive)
+ section.WriteByte(0);
+ else
+ {
+ section.WriteByte(0x23);
+ WriteUnsigned(section, TableBaseGlobalIndex);
+ section.WriteByte(0x41);
+ WriteSigned(section, start);
+ section.WriteByte(0x6A);
+ section.WriteByte(0x0B);
+ }
+ WriteUnsigned(section, count);
+ for (int slot = start; slot < start + count; slot++)
+ {
+ int index = _physicalFunctionIndices[tableFunctions[slot]];
+ if (index < 0)
+ throw new InvalidDataException("Auxiliary element references an unavailable WebAssembly function.");
+ WriteUnsigned(section, index);
+ }
+ }
+ }
+ }
+
+ private void ValidateDirectCalls(Stream code, int sectionIndex)
+ {
+ if (!_resolvableRelocations.TryGetValue(sectionIndex, out List relocations))
+ return;
+ code.Position = 0;
+ List blobs = ParseCodeBlobs(code);
+ int ordinal = 0;
+ int imports = _wasmSymbolManager.GetImportCount(WasmIndexSpace.Function);
+ foreach (SymbolicRelocation relocation in relocations)
+ {
+ while (ordinal < blobs.Count && relocation.Offset >= blobs[ordinal].End)
+ ordinal++;
+ if (ordinal >= blobs.Count || _physicalFunctionIndices[imports + ordinal] < 0 ||
+ relocation.Type != RelocType.WASM_FUNCTION_INDEX_LEB)
+ continue;
+ WasmSymbol target = _wasmSymbolManager.GetSymbol(relocation.SymbolName);
+ if (_physicalFunctionIndices[target.Index] < 0)
+ throw new InvalidDataException($"Direct call across auxiliary WebAssembly partitions: {target.Name}.");
+ }
+ }
+
+ private WasmFunctionBody CreateActivationBody(List ranges, int metadataOffset,
+ bool entryPoints, int tableStart, int tableCount)
+ {
+ int status = metadataOffset + (entryPoints
+ ? AuxiliaryModuleMetadataNode.EntryPointsStatusOffset : AuxiliaryModuleMetadataNode.ModuleStatusOffset);
+ List instructions = new()
+ {
+ Global.Get(ImageBaseGlobalIndex), I32.Load((ulong)status), I32.Eqz, Block.If(WasmBlockType.Empty),
+ };
+ for (int index = 0; index < ranges.Count; index++)
+ {
+ AuxiliaryDataRange range = ranges[index];
+ instructions.Add(Global.Get(ImageBaseGlobalIndex));
+ instructions.Add(I32.Const(range.Offset));
+ instructions.Add(I32.Add);
+ instructions.Add(I32.Const(0));
+ instructions.Add(I32.Const(range.Length));
+ instructions.Add(Memory.Init(index));
+ }
+ foreach (PendingBaseReloc relocation in _pendingBaseRelocs)
+ {
+ WebcilSection section = (WebcilSection)_sections[relocation.SectionIndex];
+ int offset = checked((int)section.Header.VirtualAddress + (int)relocation.Offset);
+ if (!ContainsAuxiliaryData(ranges, offset))
+ continue;
+ int baseGlobal = relocation.FileRelocType switch
+ {
+ RelocType.IMAGE_REL_BASED_HIGHLOW => ImageBaseGlobalIndex,
+ RelocType.IMAGE_REL_BASED_WASM32_TABLE => TableBaseGlobalIndex,
+ _ => throw new InvalidDataException($"Unsupported auxiliary base relocation: {relocation.FileRelocType}."),
+ };
+ instructions.Add(Global.Get(ImageBaseGlobalIndex));
+ instructions.Add(Global.Get(ImageBaseGlobalIndex));
+ instructions.Add(I32.Load((ulong)offset));
+ instructions.Add(Global.Get(baseGlobal));
+ instructions.Add(I32.Add);
+ instructions.Add(I32.Store((ulong)offset));
+ }
+ if (!entryPoints)
+ {
+ instructions.Add(Global.Get(TableBaseGlobalIndex));
+ instructions.Add(I32.Const(tableStart));
+ instructions.Add(I32.Add);
+ instructions.Add(I32.Const(0));
+ instructions.Add(I32.Const(tableCount));
+ instructions.Add(Table.Init(0));
+ }
+ instructions.Add(Global.Get(ImageBaseGlobalIndex));
+ instructions.Add(I32.Const(1));
+ instructions.Add(I32.Store((ulong)status));
+ instructions.Add(Block.End);
+ return new WasmFunctionBody(new WasmFuncType(new([]), new([])), instructions.ToArray());
+ }
+
+ private static void WriteBody(Stream stream, WasmFunctionBody body)
+ {
+ byte[] bytes = new byte[body.EncodeSize()];
+ body.Encode(bytes);
+ stream.Write(bytes);
+ }
+
+ private static void WriteExport(Stream stream, string name, byte kind, int index)
+ {
+ byte[] bytes = Encoding.UTF8.GetBytes(name);
+ WriteUnsigned(stream, bytes.Length);
+ stream.Write(bytes);
+ stream.WriteByte(kind);
+ WriteUnsigned(stream, index);
+ }
+
+ private static void WriteRawSection(Stream output, WasmSectionType type, Action emit,
+ int alignedSuffixLength = 0)
+ {
+ using MemoryStream content = new();
+ emit(content);
+ if (alignedSuffixLength != 0)
+ {
+ long alignedOffset = output.Position + 1 + DwarfHelper.SizeOfULEB128((ulong)content.Length) +
+ content.Length - alignedSuffixLength;
+ new PaddingWasmSection(WebcilSectionAlignment, alignedOffset).EmitToStream(output);
+ }
+ output.WriteByte((byte)type);
+ WriteUnsigned(output, checked((int)content.Length));
+ content.Position = 0;
+ content.CopyTo(output);
+ }
+
+ private static void WriteUnsigned(Stream stream, int value)
+ {
+ Span bytes = stackalloc byte[10];
+ int length = DwarfHelper.WriteULEB128(bytes, checked((ulong)value));
+ stream.Write(bytes.Slice(0, length));
+ }
+
+ private static void WriteSigned(Stream stream, int value)
+ {
+ Span bytes = stackalloc byte[10];
+ int length = DwarfHelper.WriteSLEB128(bytes, value);
+ stream.Write(bytes.Slice(0, length));
+ }
+}
+#endif
diff --git a/src/coreclr/tools/Common/Compiler/ObjectWriter/WebCilObjectWriter.cs b/src/coreclr/tools/Common/Compiler/ObjectWriter/WebCilObjectWriter.cs
index eaf100b5c10fce..1b7cde9411e2e7 100644
--- a/src/coreclr/tools/Common/Compiler/ObjectWriter/WebCilObjectWriter.cs
+++ b/src/coreclr/tools/Common/Compiler/ObjectWriter/WebCilObjectWriter.cs
@@ -25,12 +25,17 @@ namespace ILCompiler.ObjectWriter
///
/// WebCIL object file format writer.
///
- internal sealed class WebCilObjectWriter : WasmObjectWriter
+ internal sealed partial class WebCilObjectWriter : WasmObjectWriter
{
public const int WebcilSectionAlignment = 16;
protected override CodeDataLayout LayoutMode => CodeDataLayout.Separate;
- private protected override bool ShouldDeduplicateFunctionBodies => true;
+ private protected override bool ShouldDeduplicateFunctionBodies =>
+#if READYTORUN
+ !_nodeFactory.UsesWasmAuxiliaryModules;
+#else
+ true;
+#endif
// We use 2 Wasm data segments for webcil,
// 1 for the payload size, and the second for the payload itself.
@@ -279,10 +284,16 @@ private protected override void EmitSectionsAndLayout()
// and function table via active segments, so the only stub the host calls beyond
// getWebcilSize is patchWebcilHeader: the header's tableBase field is read from linear memory
// and cannot come from a segment.
- const int stubCount = 2;
+ const int stubCount = 3;
int totalMethodCount = MethodCount + stubCount;
InsertWasmStub(new Utf8String("getWebcilSize"), GetWebcilSize);
InsertWasmStub(new Utf8String("patchWebcilHeader"), PatchWebcilHeader);
+ InsertWasmStub(new Utf8String("getAuxiliaryModuleMetadata"),
+ new WasmFunctionBody(new WasmFuncType(new([]), new([WasmValueType.I32])), [I32.Const(0)]));
+#if READYTORUN
+ if (_nodeFactory.UsesWasmAuxiliaryModules)
+ _activationSignature = RegisterSignature(new WasmFuncType(new([]), new([])));
+#endif
Debug.Assert(MethodCount == totalMethodCount);
@@ -479,6 +490,10 @@ private protected override void EmitObjectFile(Stream outputFileStream)
// which is required for resolving file-level relocations whose RVA depends on the section VAs.
AssignWebcilSectionVirtualAddresses(webcilSections);
+#if READYTORUN
+ if (_nodeFactory.UsesWasmAuxiliaryModules)
+ _auxiliaryDataRanges = GetAuxiliaryDataRanges();
+#endif
// We can now build our base relocs with the correct addresses
BuildBaseRelocMap();
@@ -497,6 +512,13 @@ private protected override void EmitObjectFile(Stream outputFileStream)
webcilSections,
ImageBaseOffsetExpr);
+#if READYTORUN
+ if (_nodeFactory.UsesWasmAuxiliaryModules)
+ {
+ EmitPartitionedObjectFile(outputFileStream, webcilPayloadSegment, webcilSections);
+ return;
+ }
+#endif
// Writing our memory import <- size of the webcil segment (for an accurate minimum size)
WriteMemoryImport((ulong)webcilPayloadSegment.ContentSize);
FinalizeSectionEntryCounts();
@@ -590,7 +612,16 @@ public void EmitSymbolMap(string path)
using var writer = new StreamWriter(path, append: false, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
foreach (WasmSymbol symbol in _wasmSymbolManager.GetDefinitions(WasmIndexSpace.Function))
{
- writer.Write(symbol.Index.ToString(CultureInfo.InvariantCulture));
+ int index = symbol.Index;
+#if READYTORUN
+ if (_primaryFunctionIndices is not null)
+ {
+ index = _primaryFunctionIndices[index];
+ if (index < 0)
+ continue;
+ }
+#endif
+ writer.Write(index.ToString(CultureInfo.InvariantCulture));
writer.Write(':');
writer.WriteLine(symbol.Name);
}
@@ -653,6 +684,10 @@ private void BuildBaseRelocMap()
// (page RVA -> list of (type<<12 | offsetInPage) WORD entries).
// Note that this handling is logically the same as the implementation in the PE Object Writer.
uint targetRva = webcilSection.Header.VirtualAddress + (uint)pending.Offset;
+#if READYTORUN
+ if (_auxiliaryDataRanges is not null && ContainsAuxiliaryData(_auxiliaryDataRanges, checked((int)targetRva)))
+ continue;
+#endif
Debug.Assert(targetRva != 0); // this section should have been assigned a non-zero VirtualAddress at this point.
uint pageRva = targetRva & ~(WebcilRelocPageSize - 1);
ushort offsetInPage = (ushort)(targetRva & (WebcilRelocPageSize - 1));
@@ -906,12 +941,14 @@ private unsafe int ResolveReloc(int sectionIndex, MemoryStream sourceStream, lon
if (shrink && Relocation.IsVariableLength(reloc.Type))
{
- actualLength = Relocation.WriteVariableLengthValue(reloc.Type, pData, symbol.Index + addend);
+ actualLength = Relocation.WriteVariableLengthValue(reloc.Type, pData,
+ GetPhysicalSymbolIndex(symbol) + addend);
}
else
{
- Relocation.WriteValue(reloc.Type, pData, symbol.Index + addend);
+ Relocation.WriteValue(reloc.Type, pData, GetPhysicalSymbolIndex(symbol) + addend);
}
+
break;
}
@@ -1046,6 +1083,15 @@ int WriteRelocFromDataSpan(SymbolicRelocation reloc, byte* pData, int length)
}
}
+ private int GetPhysicalSymbolIndex(WasmSymbol symbol)
+ {
+#if READYTORUN
+ if (_physicalFunctionIndices is not null && symbol.IndexSpace == WasmIndexSpace.Function)
+ return Math.Max(_physicalFunctionIndices[symbol.Index], 0);
+#endif
+ return symbol.Index;
+ }
+
private void ResolveRelocations(int sectionIndex, Stream sectionStream, MemoryStream dstStream, List relocs, long sectionStart = 0, bool shrink = false)
{
Debug.Assert(sectionStream.CanSeek);
diff --git a/src/coreclr/tools/Common/Internal/Runtime/ModuleHeaders.cs b/src/coreclr/tools/Common/Internal/Runtime/ModuleHeaders.cs
index af7028f1b668d2..5788dea6ce7cb2 100644
--- a/src/coreclr/tools/Common/Internal/Runtime/ModuleHeaders.cs
+++ b/src/coreclr/tools/Common/Internal/Runtime/ModuleHeaders.cs
@@ -15,8 +15,8 @@ internal struct ReadyToRunHeaderConstants
{
public const uint Signature = 0x00525452; // 'RTR'
- public const ushort CurrentMajorVersion = 33;
- public const ushort CurrentMinorVersion = 1;
+ public const ushort CurrentMajorVersion = 34;
+ public const ushort CurrentMinorVersion = 0;
}
#if READYTORUN
#pragma warning disable 0169
@@ -85,6 +85,9 @@ enum ReadyToRunSectionType
ProxyTypeMaps = 125, // Added to CoreCLR in V18.3
TypeMapAssemblyTargets = 126, // Added in V18.3
WasmAsyncResumeInfo = 127, // Added in V29
+ AuxiliaryModuleMetadata = 128,
+ AuxiliaryMethodDefEntryPoints = 129,
+ AuxiliaryInstanceMethodEntryPoints = 130,
//
// NativeAOT ReadyToRun sections
diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/ManagedHelpers/HelperCalls.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/ManagedHelpers/HelperCalls.cs
index 8d5c3160c40c2d..0b9207ec93a1e9 100644
--- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/ManagedHelpers/HelperCalls.cs
+++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/ManagedHelpers/HelperCalls.cs
@@ -13,4 +13,12 @@ public static int Sum(int count)
return result;
}
+
+ private static int SumCold(int count, object value)
+ {
+ int result = ((string)value).Length;
+ for (int i = 0; i < count; i++)
+ result = checked(result + i);
+ return result;
+ }
}
diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs
index b2e0beea86e1d9..c820495681aadb 100644
--- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs
+++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs
@@ -1,16 +1,24 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+extern alias crossgen2;
+
using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection.PortableExecutable;
+using System.Reflection.Metadata;
+using System.Reflection.Metadata.Ecma335;
+using crossgen2::ILCompiler;
using ILCompiler.ReadyToRun.Tests.TestCasesRunner;
using ILCompiler.Reflection.ReadyToRun;
using Internal.ReadyToRunConstants;
using Internal.Runtime;
+using Internal.TypeSystem;
+using Internal.TypeSystem.Ecma;
+using TypeSystemMetadataEmitter = crossgen2::Internal.TypeSystem.TypeSystemMetadataEmitter;
using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;
@@ -31,6 +39,125 @@ public R2RTestSuites(ITestOutputHelper output)
_output = output;
}
+ [ConditionalTheory(typeof(TestPaths), nameof(TestPaths.IsWasmTarget))]
+ [InlineData(0, 1)]
+ [InlineData(0, 2)]
+ [InlineData(1, 1)]
+ [InlineData(1, 2)]
+ [InlineData(2, 1)]
+ [InlineData(2, 2)]
+ public void AuxiliaryModulesUseMibcMethodMembership(int profileMode, int parallelism)
+ {
+ CompiledAssembly input = new()
+ {
+ AssemblyName = nameof(AuxiliaryModulesUseMibcMethodMembership),
+ SourceResourceNames = ["Webcil/WasmWebcilModule.cs"],
+ };
+ List args = ["--wasm-auxiliary-modules", "--wasm-debug-info", "all",
+ "--optimize-disabled", "--parallelism", parallelism.ToString()];
+ new R2RTestRunner(_output).Run(new R2RTestCase(
+ nameof(AuxiliaryModulesUseMibcMethodMembership),
+ [
+ new(nameof(AuxiliaryModulesUseMibcMethodMembership), [new CrossgenAssembly(input)])
+ {
+ Options = [Crossgen2Option.Composite],
+ AdditionalArgs = args,
+ OutputFileExtension = ".wasm",
+ BeforeCompile = PrepareProfiles,
+ Validate = reader => WasmR2RAssert.AssertAuxiliaryProfileSelection(reader,
+ hasHotMethods: profileMode != 0, hasHotValueTypes: profileMode == 2),
+ },
+ ]));
+
+ void PrepareProfiles()
+ {
+ string directory = Path.GetDirectoryName(input.FilePath)!;
+ WriteProfile(Path.Combine(directory, "startup.mibc"), includePrivate: profileMode != 0,
+ includeGenerics: profileMode == 1, useCanon: false);
+ if (profileMode == 2)
+ WriteProfile(Path.Combine(directory, "generics.mibc"), includePrivate: false,
+ includeGenerics: true, useCanon: true);
+ }
+
+ void WriteProfile(string path, bool includePrivate, bool includeGenerics, bool useCanon)
+ {
+ TargetDetails target = new(TargetArchitecture.Wasm32, TargetOS.Browser, TargetAbi.NativeAot);
+ CompilerTypeSystemContext context = new(target, SharedGenericsMode.CanonicalReferenceTypes)
+ {
+ InputFilePaths = new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ [input.AssemblyName] = input.FilePath,
+ ["System.Private.CoreLib"] = TestPaths.SystemPrivateCoreLibPath,
+ },
+ ReferenceFilePaths = new Dictionary(StringComparer.OrdinalIgnoreCase),
+ };
+ context.SetSystemModule((EcmaModule)context.GetModuleForSimpleName("System.Private.CoreLib"));
+ EcmaModule module = (EcmaModule)context.GetModuleForSimpleName(input.AssemblyName);
+ MetadataType type = module.GetType("Webcil"u8, "WasmWebcilModule"u8);
+ TypeSystemMetadataEmitter emitter = new(new AssemblyNameInfo(Path.GetFileName(path)), context);
+ emitter.InjectSystemPrivateCanon();
+ emitter.AllowUseOfAddGlobalMethod();
+ InstructionEncoder group = new(new BlobBuilder());
+ if (includePrivate)
+ AddMethod(type.GetMethod("AuxiliaryMethod"u8, null));
+ if (includeGenerics)
+ {
+ List arguments = [useCanon ? context.CanonType : context.GetWellKnownType(WellKnownType.String)];
+ if (useCanon)
+ arguments.Add(context.GetWellKnownType(WellKnownType.Int32));
+ foreach (TypeDesc argument in arguments)
+ {
+ AddMethod(type.GetMethod("ProfiledGeneric"u8, null).MakeInstantiatedMethod(new Instantiation(argument)));
+ MetadataType holder = type.GetNestedType("ProfiledHolder`1"u8);
+ AddMethod(holder.MakeInstantiatedType(argument).GetMethod("Hot"u8, null));
+ }
+ }
+ group.OpCode(ILOpCode.Ret);
+ MethodDefinitionHandle groupHandle = emitter.AddGlobalMethod("Startup", group, maxStack: 1);
+ InstructionEncoder dictionary = new(new BlobBuilder());
+ dictionary.LoadString(emitter.Builder.GetOrAddUserString(input.AssemblyName + ";"));
+ dictionary.OpCode(ILOpCode.Ldtoken);
+ dictionary.Token(groupHandle);
+ dictionary.OpCode(ILOpCode.Pop);
+ dictionary.OpCode(ILOpCode.Ret);
+ emitter.AddGlobalMethod("AssemblyDictionary", dictionary, maxStack: 2);
+ using (FileStream output = File.Create(path))
+ emitter.SerializeToStream(output);
+ args.AddRange(["--mibc", path]);
+
+ void AddMethod(MethodDesc method)
+ {
+ group.OpCode(ILOpCode.Ldtoken);
+ group.Token(emitter.GetMethodRef(method));
+ group.OpCode(ILOpCode.Pop);
+ }
+ }
+ }
+
+ [ConditionalTheory(typeof(TestPaths), nameof(TestPaths.IsWasmTarget))]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void AuxiliaryModulesSeparateEntryPointsAndColdData(bool optimize)
+ {
+ CompiledAssembly input = new()
+ {
+ AssemblyName = nameof(AuxiliaryModulesSeparateEntryPointsAndColdData),
+ SourceResourceNames = ["Webcil/WasmWebcilModule.cs"],
+ };
+ new R2RTestRunner(_output).Run(new R2RTestCase(
+ nameof(AuxiliaryModulesSeparateEntryPointsAndColdData),
+ [
+ new(nameof(AuxiliaryModulesSeparateEntryPointsAndColdData), [new CrossgenAssembly(input)])
+ {
+ Options = [Crossgen2Option.Composite],
+ AdditionalArgs = ["--wasm-auxiliary-modules", "--wasm-debug-info", "all",
+ optimize ? "--optimize" : "--optimize-disabled"],
+ OutputFileExtension = ".wasm",
+ Validate = WasmR2RAssert.AssertAuxiliaryModuleLayout,
+ },
+ ]));
+ }
+
[Theory]
[InlineData(false, false)]
[InlineData(false, true)]
@@ -504,6 +631,35 @@ static void Validate(ReadyToRunReader reader)
}
}
+ [ConditionalTheory(typeof(TestPaths), nameof(TestPaths.IsWasmTarget))]
+ [InlineData(1)]
+ [InlineData(2)]
+ public void WasmManagedHelpersAreLocalToEachBodyModule(int parallelism)
+ {
+ var input = new CompiledAssembly
+ {
+ AssemblyName = nameof(WasmManagedHelpersAreLocalToEachBodyModule),
+ SourceResourceNames = ["ManagedHelpers/HelperCalls.cs"],
+ };
+
+ new R2RTestRunner(_output).Run(new R2RTestCase(
+ nameof(WasmManagedHelpersAreLocalToEachBodyModule),
+ [
+ new(nameof(WasmManagedHelpersAreLocalToEachBodyModule), [new CrossgenAssembly(input)])
+ {
+ Options = [Crossgen2Option.Composite, Crossgen2Option.Optimize],
+ AdditionalArgs =
+ [
+ "--unrooted-input-file-paths", TestPaths.SystemPrivateCoreLibPath,
+ "--wasm-auxiliary-modules",
+ "--parallelism", parallelism.ToString(),
+ ],
+ OutputFileExtension = ".wasm",
+ Validate = WasmR2RAssert.AssertAuxiliaryManagedHelpers,
+ },
+ ]));
+ }
+
[ConditionalFact(typeof(TestPaths), nameof(TestPaths.IsNotWasmTarget))]
public void BasicCrossModuleInlining()
{
diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/Webcil/WasmWebcilModule.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/Webcil/WasmWebcilModule.cs
index a8ee3216c63cb8..d3a2440e10c6e9 100644
--- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/Webcil/WasmWebcilModule.cs
+++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/Webcil/WasmWebcilModule.cs
@@ -4,6 +4,7 @@
using System;
using System.Reflection;
using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
namespace Webcil;
@@ -172,6 +173,41 @@ public static int AddIntegers(int left, int right)
return left + right;
}
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ public static int CallAuxiliaryMethod(int value) => AuxiliaryMethod(value);
+
+ [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
+ private static int AuxiliaryMethod(int value)
+ {
+ try
+ {
+ if (value < 0)
+ throw new InvalidOperationException("auxiliary method");
+ return s_primes[value % s_primes.Length];
+ }
+ finally
+ {
+ s_counter++;
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private static T ProfiledGeneric(T value) => value;
+
+ public sealed class ProfiledHolder
+ {
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ public static T Hot(T value) => value;
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ public static int CallProfiledGenerics(int value) =>
+ ProfiledGeneric(value) + ProfiledGeneric("method").Length +
+ ProfiledHolder.Hot(value) + ProfiledHolder.Hot("type").Length;
+
+ [UnmanagedCallersOnly]
+ public static int ProfiledUnmanaged(int value) => value + 1;
+
[MethodImpl(MethodImplOptions.NoInlining)]
public static void FoldableBodyOne()
{
diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestRunner.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestRunner.cs
index 66d5d6332489bd..c2cff6b1008be7 100644
--- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestRunner.cs
+++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestRunner.cs
@@ -89,6 +89,8 @@ internal sealed class CrossgenCompilation(string name, List as
///
public List AdditionalArgs { get; init; } = new();
+ public Action? BeforeCompile { get; init; }
+
///
/// Optional validator for this compilation's R2R output image.
///
@@ -212,6 +214,7 @@ public void Run(R2RTestCase testCase)
foreach(var compilation in testCase.Compilations)
{
+ compilation.BeforeCompile?.Invoke();
string outputPath = RunCrossgenCompilation(
testCase.Name, compilation, driver, compilation.FilePath, assemblyPaths);
diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/WasmR2RAssert.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/WasmR2RAssert.cs
index 8fb276fca9755e..3e8cfa6e657cb7 100644
--- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/WasmR2RAssert.cs
+++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/WasmR2RAssert.cs
@@ -22,6 +22,130 @@ namespace ILCompiler.ReadyToRun.Tests.TestCasesRunner;
internal static class WasmR2RAssert
{
+ public static void AssertAuxiliaryManagedHelpers(ReadyToRunReader reader)
+ {
+ ReadOnlySpan primary = reader.CompositeReader.GetEntireImage().AsSpan();
+ string bodyPath = Path.ChangeExtension(reader.Filename, ".1.wasm");
+ ReadOnlySpan body = File.ReadAllBytes(bodyPath);
+ Dictionary primaryNames = ReadFunctionNames(primary);
+ Dictionary bodyNames = ReadFunctionNames(body);
+ Assert.DoesNotContain(primaryNames.Values, name => name.EndsWith("HelperCalls__SumCold", StringComparison.Ordinal));
+ Assert.Contains(bodyNames.Values, name => name.EndsWith("HelperCalls__SumCold", StringComparison.Ordinal));
+ Assert.Contains(primaryNames.Values, name => name.EndsWith("HelperCalls__Sum", StringComparison.Ordinal));
+ Assert.DoesNotContain(bodyNames.Values, name => name.EndsWith("HelperCalls__Sum", StringComparison.Ordinal));
+
+ ReadyToRunSection metadata = reader.ReadyToRunHeader.Sections[ReadyToRunSectionType.AuxiliaryModuleMetadata];
+ int metadataOffset = reader.CompositeReader.GetOffset(metadata.RelativeVirtualAddress);
+ int hotCount = BinaryPrimitives.ReadInt32LittleEndian(primary.Slice(metadataOffset + 4));
+ int coldCount = BinaryPrimitives.ReadInt32LittleEndian(primary.Slice(metadataOffset + 24));
+ Assert.Equal(hotCount, BinaryPrimitives.ReadInt32LittleEndian(primary.Slice(metadataOffset + 20)));
+ Assert.Equal((hotCount + coldCount) * 8,
+ reader.ReadyToRunHeader.Sections[ReadyToRunSectionType.RuntimeFunctions].Size);
+
+ Assert.True(TryGetWasmSectionBounds(body, WasmSectionKind.Element, out int offset, out int end));
+ Assert.Equal(1u, ReadWasmUleb32(body, ref offset, end));
+ Assert.Equal(1u, ReadWasmUleb32(body, ref offset, end));
+ Assert.Equal(0, ReadWasmByte(body, ref offset, end));
+ Assert.Equal((uint)coldCount, ReadWasmUleb32(body, ref offset, end));
+ HashSet installedFunctions = new();
+ for (int index = 0; index < coldCount; index++)
+ installedFunctions.Add(ReadWasmUleb32(body, ref offset, end));
+ Assert.Equal(end, offset);
+
+ foreach (string helper in new[] { "ThrowHelpers__ThrowOverflowException", "CastHelpers__ChkCastInterface" })
+ {
+ string helperName = Assert.Single(primaryNames.Values.Where(name => name.EndsWith(helper, StringComparison.Ordinal)));
+ foreach (string frameName in primaryNames.Values.Where(name =>
+ name == helperName || name.StartsWith(helperName + "_funclet_", StringComparison.Ordinal)))
+ {
+ uint localIndex = Assert.Single(bodyNames.Where(entry => entry.Value == frameName)).Key;
+ Assert.DoesNotContain(localIndex, installedFunctions);
+ }
+ }
+ }
+
+ public static void AssertAuxiliaryProfileSelection(ReadyToRunReader reader, bool hasHotMethods, bool hasHotValueTypes)
+ {
+ Dictionary primary = ReadFunctionNames(reader.CompositeReader.GetEntireImage().AsSpan());
+ Dictionary body = ReadFunctionNames(File.ReadAllBytes(Path.ChangeExtension(reader.Filename, ".1.wasm")));
+ Check("WasmWebcilModule__AddIntegers", hot: false);
+ Check("WasmWebcilModule__ProfiledUnmanaged", hot: true);
+ Check("WasmWebcilModule__AuxiliaryMethod", hot: hasHotMethods);
+ Check("__ProfiledGeneric", hot: hasHotMethods);
+ Check("ProfiledHolder_1__Hot", hot: hasHotMethods);
+ Check("__ProfiledGeneric", hot: hasHotValueTypes);
+ Check("ProfiledHolder_1__Hot", hot: hasHotValueTypes);
+
+ void Check(string name, bool hot)
+ {
+ Dictionary owner = hot ? primary : body;
+ Dictionary other = hot ? body : primary;
+ Assert.Contains(owner.Values, value => value.Contains(name, StringComparison.Ordinal));
+ Assert.DoesNotContain(other.Values, value => value.Contains(name, StringComparison.Ordinal));
+ }
+ }
+
+ private static Dictionary ReadFunctionNames(ReadOnlySpan image)
+ {
+ Assert.True(TryGetWasmFunctionNameSubsection(image, out int offset, out int end));
+ uint count = ReadWasmUleb32(image, ref offset, end);
+ Dictionary names = new();
+ for (uint index = 0; index < count; index++)
+ {
+ uint functionIndex = ReadWasmUleb32(image, ref offset, end);
+ names.Add(functionIndex, ReadWasmName(image, ref offset, end));
+ }
+ Assert.Equal(end, offset);
+ return names;
+ }
+
+ public static void AssertAuxiliaryModuleLayout(ReadyToRunReader reader)
+ {
+ Assert.True(WasmSymbolMapHasExpectedFunctionNames(reader, out string diagnostic), diagnostic);
+ ReadOnlySpan image = reader.CompositeReader.GetEntireImage().AsSpan();
+ ReadyToRunSection metadata = reader.ReadyToRunHeader.Sections[ReadyToRunSectionType.AuxiliaryModuleMetadata];
+ int metadataOffset = reader.CompositeReader.GetOffset(metadata.RelativeVirtualAddress);
+ Assert.Equal(0u, BinaryPrimitives.ReadUInt32LittleEndian(image.Slice(metadataOffset)));
+ int hotCount = BinaryPrimitives.ReadInt32LittleEndian(image.Slice(metadataOffset + 4));
+ Assert.True(hotCount > 0);
+ Assert.Equal(1, BinaryPrimitives.ReadInt32LittleEndian(image.Slice(metadataOffset + 8)));
+ Assert.Equal(hotCount, BinaryPrimitives.ReadInt32LittleEndian(image.Slice(metadataOffset + 20)));
+ int coldCount = BinaryPrimitives.ReadInt32LittleEndian(image.Slice(metadataOffset + 24));
+ Assert.True(coldCount > 1, "The cold method and its finally funclet must both have slots.");
+ Assert.Equal(0, BinaryPrimitives.ReadInt32LittleEndian(image.Slice(metadataOffset + 28)));
+ ReadyToRunSection runtimeFunctions = reader.ReadyToRunHeader.Sections[ReadyToRunSectionType.RuntimeFunctions];
+ Assert.Equal((hotCount + coldCount) * 8, runtimeFunctions.Size);
+ int runtimeOffset = reader.CompositeReader.GetOffset(runtimeFunctions.RelativeVirtualAddress);
+ Assert.All(image.Slice(runtimeOffset + hotCount * 8, (coldCount - 1) * 8).ToArray(),
+ value => Assert.Equal(0, value));
+ Assert.NotEqual(0u, BinaryPrimitives.ReadUInt32LittleEndian(
+ image.Slice(runtimeOffset + (hotCount + coldCount - 1) * 8 + 4)));
+ ReadyToRunSection auxiliaryInstances = reader.ReadyToRunHeader.Sections[ReadyToRunSectionType.AuxiliaryInstanceMethodEntryPoints];
+ Assert.All(image.Slice(reader.CompositeReader.GetOffset(auxiliaryInstances.RelativeVirtualAddress),
+ auxiliaryInstances.Size).ToArray(), value => Assert.Equal(0, value));
+
+ string stem = Path.Combine(Path.GetDirectoryName(reader.Filename)!, Path.GetFileNameWithoutExtension(reader.Filename));
+ Assert.True(File.Exists(stem + ".auxiliary.json"));
+ foreach (string suffix in new[] { ".entrypoints.wasm", ".1.wasm" })
+ {
+ ReadOnlySpan sidecar = File.ReadAllBytes(stem + suffix);
+ Assert.True(TryGetWasmSectionBounds(sidecar, WasmSectionKind.Data, out int offset, out int end));
+ uint count = ReadWasmUleb32(sidecar, ref offset, end);
+ Assert.True(count > 0);
+ int nonzeroBytes = 0;
+ for (uint index = 0; index < count; index++)
+ {
+ Assert.Equal(1u, ReadWasmUleb32(sidecar, ref offset, end));
+ int size = checked((int)ReadWasmUleb32(sidecar, ref offset, end));
+ foreach (byte value in sidecar.Slice(offset, size))
+ nonzeroBytes += value != 0 ? 1 : 0;
+ offset += size;
+ }
+ Assert.True(nonzeroBytes > 0);
+ Assert.Equal(end, offset);
+ }
+ }
+
public static void AssertFrameIdentityPrologs(WebcilImageReader reader)
{
ReadOnlySpan image = reader.GetEntireImage().AsSpan();
@@ -85,8 +209,7 @@ public static void AssertWebcilSegmentLayout(WebcilImageReader reader, bool isCo
Assert.True(definedFunctionCount > 0, "Expected functions in the Webcil wrapper");
if (isComponentStub)
{
- // Component forwarding stubs contain no compiled methods, only the two host-called stubs.
- Assert.Equal(2u, definedFunctionCount);
+ Assert.Equal(3u, definedFunctionCount);
}
string[] actualFunctionExports = ReadWasmExports(reader)
@@ -94,7 +217,7 @@ public static void AssertWebcilSegmentLayout(WebcilImageReader reader, bool isCo
.Select(export => export.Key)
.Order(StringComparer.Ordinal)
.ToArray();
- Assert.Equal(["getWebcilSize", "patchWebcilHeader"], actualFunctionExports);
+ Assert.Equal(["getAuxiliaryModuleMetadata", "getWebcilSize", "patchWebcilHeader"], actualFunctionExports);
Assert.True(
TryGetWasmSectionBounds(image, WasmSectionKind.Global, out int offset, out int end),
@@ -574,14 +697,14 @@ private static void CheckFunctionExports(
// Only the host-called stubs are exported. Exporting every compiled function counts towards
// the engine's effective-type-size limit and makes a framework-sized composite unloadable;
// function names live in the name section instead, which CheckFunctionNames verifies.
- // Every image needs the same two stubs: getWebcilSize, and patchWebcilHeader to record the table base.
+ // The metadata getter returns zero for ordinary and component images.
List exportedFunctions = exports
.Where(export => export.Value.Kind == WasmImportKind.Function)
.Select(export => export.Key)
.Order(StringComparer.Ordinal)
.ToList();
- string[] expectedExports = ["getWebcilSize", "patchWebcilHeader"];
+ string[] expectedExports = ["getAuxiliaryModuleMetadata", "getWebcilSize", "patchWebcilHeader"];
if (!exportedFunctions.SequenceEqual(expectedExports, StringComparer.Ordinal))
{
failures.Add(
diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/WasmImportLayoutTests.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/WasmImportLayoutTests.cs
new file mode 100644
index 00000000000000..cc6a0c12444d58
--- /dev/null
+++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/WasmImportLayoutTests.cs
@@ -0,0 +1,357 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+extern alias crossgen2;
+
+using System;
+using System.Buffers.Binary;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using crossgen2::ILCompiler;
+using crossgen2::ILCompiler.DependencyAnalysis;
+using crossgen2::ILCompiler.DependencyAnalysis.ReadyToRun;
+using crossgen2::ILCompiler.Win32Resources;
+using crossgen2::Internal.JitInterface;
+using crossgen2::Internal.ReadyToRunConstants;
+using crossgen2::Internal.Text;
+using ILCompiler.DependencyAnalysisFramework;
+using Internal.TypeSystem;
+using Internal.TypeSystem.Ecma;
+using Xunit;
+
+namespace ILCompiler.ReadyToRun.Tests;
+
+public class WasmImportLayoutTests
+{
+ private static readonly SortableDependencyNode.EmbeddedObjectNodeComparer s_comparer =
+ new(CompilerComparer.Instance);
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void MixedImportsHaveOneColdRunInBothArrays(bool reverse)
+ {
+ ImportSectionNode table = Table();
+ Import[] imports = Enumerable.Range(1, 8).Select(id =>
+ new Import(table, new ReadyToRunHelperSignature((ReadyToRunHelper)id))).ToArray();
+ Import[] original = imports.OrderBy(import => import, s_comparer).ToArray();
+ HashSet cold = new(original.Where((_, index) => index % 2 == 0));
+ Assert.Equal(4, Runs(original, cold));
+
+ WasmImportLayout layout = new();
+ foreach (Import import in reverse ? imports.Reverse() : imports)
+ {
+ layout.AddImport(import);
+ layout.AddUse(import, cold.Contains(import));
+ }
+ layout.Complete(symbol => symbol);
+
+ Import[] ordered = imports.OrderBy(import => import, s_comparer).ToArray();
+ Assert.Equal(original.Where(import => !cold.Contains(import)).Concat(original.Where(cold.Contains)), ordered);
+ Assert.Equal(1, Runs(ordered, layout.ColdImports.ToHashSet()));
+ Assert.Equal(ordered.Select(import => import.ImportSignature),
+ imports.Select(import => import.ImportSignature).OrderBy(signature => signature, s_comparer));
+ Assert.True(cold.Select(import => (ISymbolNode)import.Signature).ToHashSet().SetEquals(layout.ColdSignatures));
+ }
+
+ [Fact]
+ public void SharedEagerUnusedAndResidentDataImportsStayPrimary()
+ {
+ ImportSectionNode table = Table();
+ Signature sharedSignature = new ReadyToRunHelperSignature((ReadyToRunHelper)1);
+ Import hot = new(table, sharedSignature);
+ Import shared = new(table, new ReadyToRunHelperSignature((ReadyToRunHelper)2));
+ Import coldWithSharedSignature = new(table, sharedSignature);
+ Import residentData = new(table, new ReadyToRunHelperSignature((ReadyToRunHelper)3));
+ Import eager = new(Table(eager: true), new ReadyToRunHelperSignature((ReadyToRunHelper)4));
+ Import unused = new(table, new ReadyToRunHelperSignature((ReadyToRunHelper)5));
+ Import exclusive = new(table, new ReadyToRunHelperSignature((ReadyToRunHelper)6));
+ WasmImportLayout layout = new();
+ foreach (Import import in new[] { hot, shared, coldWithSharedSignature, residentData, eager, unused, exclusive })
+ layout.AddImport(import);
+ layout.AddUse(hot, cold: false);
+ layout.AddUse(shared, cold: false);
+ layout.AddUse(shared, cold: true);
+ layout.AddUse(coldWithSharedSignature, cold: true);
+ layout.AddUse(residentData, cold: true);
+ layout.AddUse(residentData, cold: false);
+ layout.AddUse(eager, cold: true);
+ layout.AddUse(exclusive, cold: true);
+ layout.Complete(symbol => symbol);
+
+ Assert.True(new HashSet { coldWithSharedSignature, exclusive }.SetEquals(layout.ColdImports));
+ Assert.Same(exclusive.Signature, Assert.Single(layout.ColdSignatures));
+ Assert.DoesNotContain(sharedSignature, layout.ColdSignatures);
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void SharedDeduplicatedAndDirectlyReferencedSignaturesStayPrimary(bool directReference)
+ {
+ Signature first = new ReadyToRunHelperSignature((ReadyToRunHelper)1);
+ Signature alias = new ReadyToRunHelperSignature((ReadyToRunHelper)2);
+ Import cold = new(Table(), first);
+ WasmImportLayout layout = new();
+ layout.AddImport(cold);
+ layout.AddUse(cold, cold: true);
+ if (directReference)
+ layout.AddUse(alias, cold: false);
+ else
+ layout.AddImport(new Import(Table(), alias));
+ layout.Complete(symbol => symbol == alias ? first : symbol);
+ Assert.Same(cold, Assert.Single(layout.ColdImports));
+ Assert.Empty(layout.ColdSignatures);
+ }
+
+ [Fact]
+ public void EagerValidationFixupsStillPrecedeReadyToRunCommitFixups()
+ {
+ ImportSectionNode table = Table(eager: true);
+ Import commit = new(table, new TestSignature(1), sortLast: true);
+ Import validation = new(table, new TestSignature(2));
+ WasmImportLayout layout = new();
+ layout.AddImport(commit);
+ layout.AddImport(validation);
+ layout.AddUse(commit, cold: true);
+ layout.AddUse(validation, cold: true);
+ layout.Complete(symbol => symbol);
+ Assert.Empty(layout.ColdImports);
+ Assert.True(s_comparer.Compare(validation, commit) < 0);
+ Assert.True(s_comparer.Compare(validation.ImportSignature, commit.ImportSignature) < 0);
+ }
+
+ [Fact]
+ public void SignatureBlobsAreStablyGroupedWithoutMovingOrderedTables()
+ {
+ ImportSectionNode table = Table();
+ Signature[] signatures = Enumerable.Range(1, 4)
+ .Select(id => (Signature)new ReadyToRunHelperSignature((ReadyToRunHelper)id)).ToArray();
+ WasmImportLayout layout = new();
+ for (int index = 0; index < signatures.Length; index++)
+ {
+ Import import = new(table, signatures[index]);
+ layout.AddImport(import);
+ layout.AddUse(import, cold: index % 2 == 0);
+ }
+ layout.Complete(symbol => symbol);
+ DependencyNode[] original = [table, signatures[0], signatures[1], signatures[2], signatures[3]];
+ Assert.Equal([table, signatures[1], signatures[3], signatures[0], signatures[2]],
+ layout.OrderSignatureBlobs(original));
+ }
+
+ [Theory]
+ [InlineData(0, false)]
+ [InlineData(0, true)]
+ [InlineData(4, false)]
+ [InlineData(4, true)]
+ public void EmptyAndSingleOwnerPartitions(int count, bool cold)
+ {
+ WasmImportLayout layout = new();
+ Import[] imports = Enumerable.Range(1, count).Select(id =>
+ new Import(Table(), new ReadyToRunHelperSignature((ReadyToRunHelper)id))).ToArray();
+ foreach (Import import in imports)
+ {
+ layout.AddImport(import);
+ layout.AddUse(import, cold);
+ }
+ layout.Complete(symbol => symbol);
+ Assert.Equal(cold ? count : 0, layout.ColdImports.Count());
+ Assert.Equal(cold ? count : 0, layout.ColdSignatures.Count());
+ Assert.Equal(imports.Cast(), layout.OrderSignatureBlobs(imports));
+ }
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(8)]
+ [InlineData(1025)]
+ public void EmittedTablesUsePartitionedOffsetsAndPreserveAlignment(int count)
+ {
+ NodeFactory factory = CreateFactory();
+ ImportSectionNode table = new("EmittedImports", ReadyToRunImportSectionType.Unknown,
+ 0, 4, emitPrecode: true, emitGCRefMap: false);
+ Import[] imports = Enumerable.Range(1, count).Select(id =>
+ new Import(table, new TestSignature(id))).ToArray();
+ HashSet coldImports = imports.Where((_, index) => index % 2 == 0).ToHashSet();
+ int originalRuns = Runs(imports.OrderBy(import => import, s_comparer), coldImports);
+ WasmImportLayout layout = new();
+ for (int index = 0; index < count; index++)
+ {
+ table.AddImport(factory, imports[index]);
+ layout.AddImport(imports[index]);
+ layout.AddUse(imports[index], cold: index % 2 == 0);
+ }
+ layout.Complete(symbol => symbol);
+ SortableDependencyNode.DependencySink sink = new();
+ table.AddStaticDependencies(sink, factory);
+ using var dependencies = sink.Drain();
+ List tables = new();
+ while (dependencies.MoveNext())
+ tables.Add(Assert.IsAssignableFrom(dependencies.Dependency));
+ ObjectNode cells = Assert.Single(tables, node => node != table.SignatureArray);
+ ObjectNode.ObjectData cellData = cells.GetData(factory);
+ ObjectNode.ObjectData signatureData = ((ObjectNode)table.SignatureArray).GetData(factory);
+
+ Assert.Equal(4, cellData.Alignment);
+ Assert.Equal(count * 4, cellData.Data.Length);
+ Assert.Equal(count * 4, signatureData.Data.Length);
+ Import[] ordered = imports.OrderBy(import => import, s_comparer).ToArray();
+ for (int index = 0; index < count; index++)
+ {
+ Import import = ordered[index];
+ Assert.Equal(index, import.IndexFromBeginningOfArray);
+ Assert.Equal(index * 4, import.OffsetFromBeginningOfArray);
+ Assert.Equal(index, import.ImportSignature.IndexFromBeginningOfArray);
+ Assert.Equal(index * 4, import.ImportSignature.OffsetFromBeginningOfArray);
+ Assert.Same(import.Signature, signatureData.Relocs[index].Target);
+ Assert.Equal(index * 4, signatureData.Relocs[index].Offset);
+ }
+ Import[] physicalCells = cellData.DefinedSymbols.OfType()
+ .OrderBy(import => ((ISymbolDefinitionNode)import).Offset).ToArray();
+ int emittedRuns = Runs(physicalCells, layout.ColdImports.ToHashSet());
+ Assert.Equal(ordered, physicalCells);
+ Assert.Equal(count == 0 ? 0 : 1, emittedRuns);
+ if (count > 2)
+ Assert.True(emittedRuns < originalRuns,
+ "The emitted table must have fewer cold-owned ranges than the unpartitioned order.");
+ table.InitializeIndexFromBeginningOfArray(3);
+ EcmaModule module = factory.TypeSystemContext.GetModuleFromPath(typeof(WasmImportLayoutTests).Assembly.Location);
+ MethodWithGCInfo method = new(module.GetType("ILCompiler.ReadyToRun.Tests"u8, "WasmImportLayoutTests"u8)
+ .GetMethod("MixedImportsHaveOneColdRunInBothArrays"u8, null));
+ method.SetCode(new ObjectNode.ObjectData([], [], 1, []));
+ foreach (Import import in ordered.Where((_, index) => index % 2 == 0).Reverse())
+ method.Fixups.Add(import);
+ if (count > 0)
+ {
+ byte[] blob = method.GetFixupBlob(factory);
+ NibbleWriter expected = new();
+ expected.WriteUInt(3);
+ expected.WriteUInt(0);
+ for (int index = 2; index < count; index += 2)
+ expected.WriteUInt(2);
+ expected.WriteUInt(0);
+ expected.WriteUInt(0);
+ Assert.Equal(expected.ToArray(), blob);
+ }
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void GCMapsFollowSignaturesAcrossPartitionsAndSharing(bool instantiatingStub)
+ {
+ NodeFactory factory = CreateFactory(hostTarget: true);
+ CompilerTypeSystemContext context = factory.TypeSystemContext;
+ EcmaModule module = context.GetModuleFromPath(typeof(WasmImportLayoutTests).Assembly.Location);
+ MetadataType type = module.GetType("ILCompiler.ReadyToRun.Tests"u8, "WasmImportLayoutTests"u8);
+ MethodDesc generic = type.GetMethod("GenericArgument"u8, null).MakeInstantiatedMethod(
+ new Instantiation(context.GetWellKnownType(WellKnownType.Object)));
+ ImportSectionNode table = new("GCImports", ReadyToRunImportSectionType.StubDispatch, 0, 8,
+ emitPrecode: false, emitGCRefMap: true);
+ MethodDesc[] methods = [type.GetMethod("ObjectArgument"u8, null),
+ type.GetMethod("IntegerArgument"u8, null), generic];
+ MethodFixupSignature[] signatures = methods.Select(method =>
+ new MethodFixupSignature(ReadyToRunFixupKind.MethodEntry,
+ new MethodWithToken(method,
+ new ModuleToken(module, ((EcmaMethod)method.GetTypicalMethodDefinition()).Handle),
+ constrainedType: null, unboxing: false, genericContextObject: null),
+ instantiatingStub)).ToArray();
+ Import[] imports = [new(table, signatures[0]), new(table, signatures[1]),
+ new(table, signatures[2]), new(table, signatures[0])];
+ WasmImportLayout layout = new();
+ for (int index = 0; index < imports.Length; index++)
+ {
+ table.AddImport(factory, imports[index]);
+ layout.AddImport(imports[index]);
+ layout.AddUse(imports[index], cold: index >= 2);
+ }
+ layout.Complete(symbol => symbol);
+ Assert.Same(signatures[2], Assert.Single(layout.ColdSignatures));
+ ObjectNode.ObjectData signatureData = ((ObjectNode)table.SignatureArray).GetData(factory);
+ int indexInArray = 0;
+ foreach (Import import in imports.OrderBy(import => import, s_comparer))
+ {
+ MethodFixupSignature signature = Assert.IsType(import.Signature);
+ Assert.True(signature.EmitGCRefMap);
+ GCRefMapBuilder expected = new(factory.Target, relocsOnly: false);
+ expected.GetCallRefMap(signature.Method, instantiatingStub);
+ byte[] map = signature.GetGCRefMap(factory);
+ Assert.Equal(expected.Builder.ToObjectData().Data, map);
+ Assert.Equal(map.Length, signature.Offset);
+ Assert.Same(signature, signatureData.Relocs[indexInArray].Target);
+ Assert.Equal(-map.Length,
+ BinaryPrimitives.ReadInt32LittleEndian(signatureData.Data.AsSpan(indexInArray * sizeof(int))));
+ indexInArray++;
+ }
+ }
+
+ private static void ObjectArgument(object value) { }
+ private static void IntegerArgument(int value) { }
+ private static void GenericArgument(T value) { }
+
+ private static NodeFactory CreateFactory(bool hostTarget = false)
+ {
+ TargetArchitecture architecture = hostTarget ? TargetArchitecture.X64 : TargetArchitecture.Wasm32;
+ TargetDetails target = new(architecture,
+ hostTarget ? TargetOS.Linux : TargetOS.Browser,
+ TargetAbi.NativeAot);
+ ReadyToRunCompilerContext context = new(target, SharedGenericsMode.CanonicalReferenceTypes,
+ bubbleIncludesCoreModule: true, targetAllowsRuntimeCodeGeneration: hostTarget,
+ targetIsSingleThreaded: !hostTarget, new InstructionSetSupport(default, default, architecture),
+ oldTypeSystemContext: null);
+ if (hostTarget)
+ {
+ context.InputFilePaths = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ context.ReferenceFilePaths = Directory.GetFiles(Path.GetDirectoryName(typeof(object).Assembly.Location)!, "*.dll")
+ .ToDictionary(path => Path.GetFileNameWithoutExtension(path), StringComparer.OrdinalIgnoreCase);
+ context.SetSystemModule(context.GetModuleFromPath(typeof(object).Assembly.Location));
+ }
+ EcmaModule module = context.GetModuleFromPath(typeof(WasmImportLayoutTests).Assembly.Location);
+ NoMethodsCompilationModuleGroup group = new(new ReadyToRunCompilationModuleGroupConfig
+ {
+ Context = context,
+ IsCompositeBuildMode = true,
+ CompilationModuleSet = [module],
+ VersionBubbleModuleSet = [],
+ CrossModuleInlineable = [],
+ });
+ context.SetCompilationGroup(group);
+ return new NodeFactory(context, group, null!, null!, null!, null!, new ResourceData(module),
+ 0, default, ReadyToRunContainerFormat.Wasm, 0, module, -1, -1);
+ }
+
+ private sealed class TestSignature(int id) : Signature
+ {
+ public override int ClassCode => 173201194;
+
+ public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false) =>
+ new(BitConverter.GetBytes(id), [], 1, [this]);
+
+ public override void AppendMangledName(NameMangler nameMangler, Utf8StringBuilder sb) =>
+ sb.Append("TestSignature" + id);
+
+ public override int CompareToImpl(ISortableNode other, CompilerComparer comparer) =>
+ id.CompareTo(((TestSignature)other).Id);
+
+ private int Id => id;
+ }
+
+ private static ImportSectionNode Table(bool eager = false) =>
+ new("TestImports", ReadyToRunImportSectionType.Unknown,
+ eager ? ReadyToRunImportSectionFlags.Eager : 0, 4, emitPrecode: false, emitGCRefMap: false);
+
+ private static int Runs(IEnumerable imports, HashSet cold)
+ {
+ int runs = 0;
+ bool previous = false;
+ foreach (Import import in imports)
+ {
+ bool current = cold.Contains(import);
+ if (current && !previous)
+ runs++;
+ previous = current;
+ }
+ return runs;
+ }
+}
diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/CodeGen/ReadyToRunObjectWriter.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/CodeGen/ReadyToRunObjectWriter.cs
index dd10042941b16e..245b75f9a5e1ab 100644
--- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/CodeGen/ReadyToRunObjectWriter.cs
+++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/CodeGen/ReadyToRunObjectWriter.cs
@@ -281,6 +281,21 @@ public void EmitReadyToRunObjects(ReadyToRunContainerFormat format, Logger logge
{
if (!succeeded)
{
+ if (_nodeFactory.UsesWasmAuxiliaryModules)
+ {
+ string stem = Path.Combine(Path.GetDirectoryName(_objectFilePath),
+ Path.GetFileNameWithoutExtension(_objectFilePath));
+ foreach (string suffix in new[] { ".entrypoints.wasm", ".1.wasm", ".auxiliary.json" })
+ {
+ try
+ {
+ File.Delete(stem + suffix);
+ }
+ catch
+ {
+ }
+ }
+ }
// If there was an exception while generating the OBJ file, make sure we don't leave the unfinished
// object file around.
try
diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/AuxiliaryModuleMetadataNode.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/AuxiliaryModuleMetadataNode.cs
new file mode 100644
index 00000000000000..a5b713c7f7c4e5
--- /dev/null
+++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/AuxiliaryModuleMetadataNode.cs
@@ -0,0 +1,55 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Text;
+using Internal.Text;
+
+namespace ILCompiler.DependencyAnalysis.ReadyToRun;
+
+public sealed class AuxiliaryModuleMetadataNode : HeaderTableNode
+{
+ public const int HeaderSize = 16;
+ public const int EntrySize = 16;
+ public const int EntryPointsStatusOffset = 0;
+ public const int ModuleStatusOffset = HeaderSize + 12;
+
+ public string OutputFileName { get; set; }
+
+ public override void AppendMangledName(NameMangler nameMangler, Utf8StringBuilder builder)
+ => builder.Append(nameMangler.CompilationUnitPrefix).Append("__AuxiliaryModuleMetadata"u8);
+
+ public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false)
+ {
+ ObjectDataBuilder builder = new(factory, relocsOnly);
+ builder.RequireInitialAlignment(4);
+ builder.AddSymbol(this);
+ int primaryCount = 0;
+ int auxiliaryCount = 0;
+ if (!relocsOnly)
+ {
+ foreach (MethodWithGCInfo method in factory.EnumerateCompiledMethods())
+ {
+ if (factory.IsAuxiliaryMethod(method))
+ auxiliaryCount += method.FrameInfos.Length;
+ else
+ primaryCount += method.FrameInfos.Length;
+ }
+ }
+
+ byte[] entryPointsName = Encoding.UTF8.GetBytes(OutputFileName + ".entrypoints.wasm\0");
+ byte[] moduleName = Encoding.UTF8.GetBytes(OutputFileName + ".1.wasm\0");
+ builder.EmitInt(0);
+ builder.EmitInt(primaryCount);
+ builder.EmitInt(1);
+ builder.EmitReloc(this, RelocType.IMAGE_REL_BASED_ADDR32NB, HeaderSize + EntrySize);
+ builder.EmitReloc(this, RelocType.IMAGE_REL_BASED_ADDR32NB, HeaderSize + EntrySize + entryPointsName.Length);
+ builder.EmitInt(primaryCount);
+ builder.EmitInt(auxiliaryCount);
+ builder.EmitInt(0);
+ builder.EmitBytes(entryPointsName);
+ builder.EmitBytes(moduleName);
+ return builder.ToObjectData();
+ }
+
+ public override int ClassCode => 193275841;
+}
diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/ExceptionInfoLookupTableNode.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/ExceptionInfoLookupTableNode.cs
index 8b11f1410e8844..ba36f2970122bb 100644
--- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/ExceptionInfoLookupTableNode.cs
+++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/ExceptionInfoLookupTableNode.cs
@@ -25,6 +25,7 @@ public class EHInfoNode : ObjectNode, ISymbolDefinitionNode
public int Offset => 0;
private ArrayBuilder _ehInfoBuilder;
+ public readonly List<(int Offset, int Length)> AuxiliaryDataRanges = new();
public EHInfoNode()
{
@@ -98,7 +99,10 @@ internal void LayoutMethodsWithEHInfo()
if (ehInfo != null && ehInfo.Data.Length != 0)
{
_methodNodes.Add(method);
- _ehInfoOffsets.Add(_ehInfoNode.AddEHInfo(ehInfo.Data));
+ int offset = _ehInfoNode.AddEHInfo(ehInfo.Data);
+ _ehInfoOffsets.Add(offset);
+ if (_nodeFactory.IsAuxiliaryMethod(method))
+ _ehInfoNode.AuxiliaryDataRanges.Add((offset, ehInfo.Data.Length));
}
}
}
diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/Import.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/Import.cs
index cadc8013774a08..8dff054a7c5163 100644
--- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/Import.cs
+++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/Import.cs
@@ -24,6 +24,12 @@ public class Import : EmbeddedObjectNode, ISymbolDefinitionNode, ISortableSymbol
public Signature Signature => ImportSignature.Target;
+ internal void SetPartition(bool coldExclusive)
+ {
+ // Both arrays must use the same partition key before assigning any indices or offsets.
+ CustomSort = ImportSignature.CustomSort = coldExclusive ? 1 : 0;
+ }
+
public Import(ImportSectionNode tableNode, Signature importSignature, MethodDesc callingMethod = null, bool sortLast = false)
{
Table = tableNode;
diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/ImportSectionNode.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/ImportSectionNode.cs
index dfe4a58d469f22..3c498f10a6f9b3 100644
--- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/ImportSectionNode.cs
+++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/ImportSectionNode.cs
@@ -93,6 +93,7 @@ public void AddImport(NodeFactory factory, Import import)
}
public string Name => _name;
+ internal ISymbolNode SignatureArray => _signatures;
public bool EmitPrecode => _emitPrecode;
diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/InstanceEntryPointTableNode.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/InstanceEntryPointTableNode.cs
index f1ccd58e0b62d2..c058f4a2f51a8b 100644
--- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/InstanceEntryPointTableNode.cs
+++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/InstanceEntryPointTableNode.cs
@@ -22,10 +22,14 @@ public class InstanceEntryPointTableNode : HeaderTableNode, ISignatureEmitter
{
private readonly NodeFactory _factory;
private bool _materializedSignature;
+ private int _entryPointsOffset;
+ public bool IsAuxiliary { get; }
+ public override int Offset => _entryPointsOffset;
- public InstanceEntryPointTableNode(NodeFactory factory)
+ public InstanceEntryPointTableNode(NodeFactory factory, bool auxiliary = false)
{
_factory = factory;
+ IsAuxiliary = auxiliary;
_factory.ManifestMetadataTable.RegisterEmitter(this);
}
@@ -51,7 +55,7 @@ public void MaterializeSignature()
public override void AppendMangledName(NameMangler nameMangler, Utf8StringBuilder sb)
{
sb.Append(nameMangler.CompilationUnitPrefix);
- sb.Append("__ReadyToRunInstanceEntryPointTable"u8);
+ sb.Append(IsAuxiliary ? "__ReadyToRunAuxiliaryInstanceEntryPointTable"u8 : "__ReadyToRunInstanceEntryPointTable"u8);
}
public static byte[] BuildSignatureForMethodDefinedInModule(MethodDesc method, NodeFactory factory)
@@ -100,6 +104,7 @@ public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false)
NativeWriter hashtableWriter = new NativeWriter();
+ Section fixupsSection = IsAuxiliary ? hashtableWriter.NewSection() : null;
Section hashtableSection = hashtableWriter.NewSection();
VertexHashtable vertexHashtable = new VertexHashtable();
hashtableSection.Place(vertexHashtable);
@@ -109,6 +114,9 @@ public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false)
foreach (MethodWithGCInfo method in factory.EnumerateCompiledMethods(null, CompiledMethodCategory.Instantiated))
{
+ if (factory.IsAuxiliaryMethod(method) != IsAuxiliary)
+ continue;
+
// Resumption stubs are discovered via READYTORUN_FIXUP_ResumptionStubEntryPoint fixups
// on their parent async variant methods, so they do not need entries in the InstanceEntryPointTable.
if (method.Method is AsyncResumptionStub)
@@ -136,6 +144,7 @@ public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false)
{
fixupBlob = new BlobVertex(fixup);
uniqueFixups.Add(fixup, fixupBlob);
+ fixupsSection?.Place(fixupBlob);
}
EntryPointVertex entryPointVertex = new EntryPointWithBlobVertex((uint)methodIndex, fixupBlob, signatureBlob);
@@ -150,6 +159,7 @@ public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false)
MemoryStream hashtableContent = new MemoryStream();
hashtableWriter.Save(hashtableContent);
+ _entryPointsOffset = vertexHashtable.VertexOffset;
return new ObjectData(
data: hashtableContent.ToArray(),
relocs: null,
@@ -157,6 +167,6 @@ public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false)
definedSymbols: new ISymbolDefinitionNode[] { this });
}
- public override int ClassCode => -348722540;
+ public override int ClassCode => IsAuxiliary ? -348722539 : -348722540;
}
}
diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/MethodEntryPointTableNode.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/MethodEntryPointTableNode.cs
index ff1a46918b186e..b30e31c84ae3a7 100644
--- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/MethodEntryPointTableNode.cs
+++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/MethodEntryPointTableNode.cs
@@ -17,6 +17,10 @@ namespace ILCompiler.DependencyAnalysis.ReadyToRun
{
public class MethodEntryPointTableNode : ModuleSpecificHeaderTableNode
{
+ public bool IsAuxiliary { get; }
+ public override int Offset => _entryPointsOffset;
+ private int _entryPointsOffset;
+
private struct EntryPoint
{
public static EntryPoint Null = new EntryPoint(-1, null);
@@ -33,11 +37,14 @@ public EntryPoint(int methodIndex, MethodWithGCInfo method)
}
}
- public MethodEntryPointTableNode(EcmaModule module) : base(module)
+ public MethodEntryPointTableNode(EcmaModule module, bool auxiliary = false) : base(module)
{
+ IsAuxiliary = auxiliary;
}
- protected override string ModuleSpecificName => "__ReadyToRunMethodEntryPointTable__";
+ protected override string ModuleSpecificName => IsAuxiliary
+ ? "__ReadyToRunAuxiliaryMethodEntryPointTable__"
+ : "__ReadyToRunMethodEntryPointTable__";
public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false)
{
@@ -50,6 +57,9 @@ public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false)
foreach (MethodWithGCInfo method in factory.EnumerateCompiledMethods(_module, CompiledMethodCategory.NonInstantiated))
{
+ if (factory.IsAuxiliaryMethod(method) != IsAuxiliary)
+ continue;
+
Debug.Assert(method.Method is EcmaMethod);
EcmaMethod ecmaMethod = (EcmaMethod)method.Method;
Debug.Assert(ecmaMethod.Module == _module);
@@ -70,6 +80,7 @@ public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false)
NativeWriter writer = new NativeWriter();
+ Section fixupsSection = IsAuxiliary ? writer.NewSection() : null;
Section arraySection = writer.NewSection();
VertexArray vertexArray = new VertexArray(arraySection);
arraySection.Place(vertexArray);
@@ -88,6 +99,7 @@ public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false)
{
fixupBlobVertex = new BlobVertex(fixups);
uniqueFixups.Add(fixups, fixupBlobVertex);
+ fixupsSection?.Place(fixupBlobVertex);
}
EntryPointVertex entryPointVertex = new EntryPointVertex((uint)entryPoint.MethodIndex, fixupBlobVertex);
vertexArray.Set(rid, entryPointVertex);
@@ -98,6 +110,7 @@ public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false)
MemoryStream arrayContent = new MemoryStream();
writer.Save(arrayContent);
+ _entryPointsOffset = vertexArray.VertexOffset;
return new ObjectData(
data: arrayContent.ToArray(),
relocs: null,
@@ -106,6 +119,6 @@ public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false)
}
protected internal override int Phase => (int)ObjectNodePhase.Ordered;
- public override int ClassCode => (int)ObjectNodeOrder.MethodEntrypointTableNode;
+ public override int ClassCode => (int)ObjectNodeOrder.MethodEntrypointTableNode + (IsAuxiliary ? 1000 : 0);
}
}
diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/MethodGCInfoNode.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/MethodGCInfoNode.cs
index 850ec40f1a7c14..a587d7ddf78391 100644
--- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/MethodGCInfoNode.cs
+++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/MethodGCInfoNode.cs
@@ -256,11 +256,13 @@ public MethodGCInfoNodeDeduplicatingComparer(NodeFactory factory)
NodeFactory _factory;
public bool Equals(MethodGCInfoNode a, MethodGCInfoNode b)
{
- return a.EncodeDataCore(_factory).SequenceEqual(b.EncodeDataCore(_factory));
+ return _factory.IsAuxiliaryMethod(a._methodNode) == _factory.IsAuxiliaryMethod(b._methodNode) &&
+ a.EncodeDataCore(_factory).SequenceEqual(b.EncodeDataCore(_factory));
}
public int GetHashCode(MethodGCInfoNode node)
{
HashCode hashcode = new HashCode();
+ hashcode.Add(_factory.IsAuxiliaryMethod(node._methodNode));
foreach (var item in node.EncodeDataCore(_factory))
{
hashcode.Add(item);
@@ -285,6 +287,7 @@ public override void EncodeData(ref ObjectDataBuilder dataBuilder, NodeFactory f
factory.RuntimeFunctionsGCInfo.Deduplicator.Add(this);
bool targetAlignsGCInfoComponents = TargetAlignsGCInfoComponents(factory.Target.Architecture);
+ int startOffset = dataBuilder.CountBytes;
foreach (var item in EncodeDataCore(factory))
{
@@ -303,6 +306,8 @@ public override void EncodeData(ref ObjectDataBuilder dataBuilder, NodeFactory f
dataBuilder.EmitReloc(item.Symbol, RelocType.IMAGE_REL_BASED_ADDR32NB, item.SymbolDelta);
}
}
+ if (factory.IsAuxiliaryMethod(_methodNode))
+ factory.RuntimeFunctionsGCInfo.AuxiliaryDataRanges.Add((startOffset, dataBuilder.CountBytes - startOffset));
}
protected override string GetName(NodeFactory context)
diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/MethodWithGCInfo.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/MethodWithGCInfo.cs
index febeb3ed56391b..f4d5498bfeaf0a 100644
--- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/MethodWithGCInfo.cs
+++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/MethodWithGCInfo.cs
@@ -268,6 +268,11 @@ public byte[] GetFixupBlob(NodeFactory factory)
protected override void ComputeNonRelocationBasedDependencies(DependencySink sink, NodeFactory factory)
{
sink.Add(GCInfoNode, "Unwind & GC info");
+ if (factory.UsesWasmAuxiliaryModules && factory.IsAuxiliaryMethod(this))
+ {
+ sink.Add(factory.WasmR2RToInterpreterThunk(WasmLowering.GetSignature(_method)),
+ "Interpreter fallback for auxiliary method");
+ }
if (this.ColdCodeNode != null)
{
diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/ReadyToRunHeaderNode.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/ReadyToRunHeaderNode.cs
index b62c14241370e0..2435dc7643eaf6 100644
--- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/ReadyToRunHeaderNode.cs
+++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/ReadyToRunHeaderNode.cs
@@ -18,7 +18,10 @@ public abstract class HeaderTableNode : ObjectNode, ISymbolDefinitionNode
{
public abstract void AppendMangledName(NameMangler nameMangler, Utf8StringBuilder sb);
- public int Offset => 0;
+ public virtual int Offset => 0;
+
+ // The definition already accounts for any leading fixup blobs.
+ int ISymbolNode.Offset => 0;
public override bool IsShareable => false;
diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/RuntimeFunctionsGCInfoNode.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/RuntimeFunctionsGCInfoNode.cs
index aad3718edb6938..312175b06eafee 100644
--- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/RuntimeFunctionsGCInfoNode.cs
+++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/RuntimeFunctionsGCInfoNode.cs
@@ -13,6 +13,7 @@ public RuntimeFunctionsGCInfoNode()
}
public HashSet Deduplicator;
+ public readonly List<(int Offset, int Length)> AuxiliaryDataRanges = new();
protected internal override int Phase => (int)ObjectNodePhase.Ordered;
diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmImportLayout.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmImportLayout.cs
new file mode 100644
index 00000000000000..0d0fb69a3cd533
--- /dev/null
+++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmImportLayout.cs
@@ -0,0 +1,74 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using ILCompiler.DependencyAnalysisFramework;
+
+namespace ILCompiler.DependencyAnalysis.ReadyToRun;
+
+// Ownership is collected before layout and reused when extracting the sidecar's data ranges.
+internal sealed class WasmImportLayout
+{
+ private readonly List _imports = new();
+ private readonly HashSet _primaryImports = new();
+ private readonly HashSet _coldImports = new();
+ private readonly HashSet _primarySignatures = new();
+ private readonly HashSet _coldSignatures = new();
+
+ internal IEnumerable ColdImports => _coldImports;
+ internal IEnumerable ColdSignatures => _coldSignatures;
+
+ internal void AddImport(Import import)
+ {
+ _imports.Add(import);
+ if (import.Table.IsEager)
+ _primaryImports.Add(import);
+ }
+
+ internal void AddUse(ISymbolNode symbol, bool cold)
+ {
+ if (symbol is Import import)
+ (cold ? _coldImports : _primaryImports).Add(import);
+ else if (!cold && symbol is Signature)
+ _primarySignatures.Add(symbol);
+ }
+
+ internal void Complete(Func deduplicate)
+ {
+ _coldImports.ExceptWith(_primaryImports);
+ HashSet primarySignatures = new();
+ foreach (ISymbolNode signature in _primarySignatures)
+ primarySignatures.Add(deduplicate(signature));
+ foreach (Import import in _imports)
+ {
+ bool cold = _coldImports.Contains(import);
+ import.SetPartition(cold);
+ if (!cold)
+ primarySignatures.Add(deduplicate(import.Signature));
+ }
+ foreach (Import import in _coldImports)
+ {
+ ISymbolNode signature = deduplicate(import.Signature);
+ if (!primarySignatures.Contains(signature))
+ _coldSignatures.Add(signature);
+ }
+ }
+
+ internal IReadOnlyCollection OrderSignatureBlobs(IReadOnlyCollection nodes)
+ {
+ List ordered = new(nodes.Count);
+ List cold = new();
+ foreach (DependencyNode node in nodes)
+ {
+ if (node is Signature signature && _coldSignatures.Contains(signature))
+ cold.Add(node);
+ else
+ ordered.Add(node);
+ }
+ // Keep the existing deterministic order in each group. Only independent signature blobs move;
+ // ordered tables (which assign import indices before fixup-list emission) stay in place.
+ ordered.AddRange(cold);
+ return ordered;
+ }
+}
diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRunCodegenNodeFactory.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRunCodegenNodeFactory.cs
index 2f36734970cb45..08934e6c916542 100644
--- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRunCodegenNodeFactory.cs
+++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRunCodegenNodeFactory.cs
@@ -6,6 +6,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
+using System.Reflection;
using ILCompiler.DependencyAnalysis.Wasm;
using ILCompiler.DependencyAnalysis.ReadyToRun;
@@ -72,6 +73,7 @@ public struct NodeFactoryOptimizationFlags
public bool StripInliningInfo;
public bool StripDebugInfo;
public bool StripILBodies;
+ public bool WasmAuxiliaryModules;
public HashSet CompiledMethodDefs;
}
@@ -80,6 +82,7 @@ public struct NodeFactoryOptimizationFlags
public sealed partial class NodeFactory
{
private bool _markingComplete;
+ private readonly HashSet _mibcHotMethods;
public CompilerTypeSystemContext TypeSystemContext { get; }
@@ -106,6 +109,25 @@ public sealed partial class NodeFactory
List _markedILBodyFixupSignatures = new List();
public bool MarkingComplete => _markingComplete;
+ public bool UsesWasmAuxiliaryModules => OptimizationFlags.WasmAuxiliaryModules && !OptimizationFlags.IsComponentModule;
+ public bool IsAuxiliaryMethod(MethodWithGCInfo method)
+ {
+ if (!UsesWasmAuxiliaryModules || method.IsJitHelper)
+ return false;
+
+ MethodDesc target = method.Method;
+ MethodDesc definition = target.GetTypicalMethodDefinition();
+ if (definition is not EcmaMethod ecmaMethod || target.IsAsyncVariant() ||
+ target.IsAsyncCall() || target.IsUnmanagedCallersOnly ||
+ target.IsPInvoke || ecmaMethod.MetadataReader.GetMethodDefinition(ecmaMethod.Handle).RelativeVirtualAddress == 0)
+ return false;
+
+ if (_mibcHotMethods is not null)
+ return !_mibcHotMethods.Contains(target);
+
+ return (ecmaMethod.Attributes & MethodAttributes.MemberAccessMask) != MethodAttributes.Public ||
+ target.HasInstantiation || target.OwningType.HasInstantiation;
+ }
public void GenerateHotColdMap(DependencyAnalyzerBase dependencyGraph)
{
@@ -351,6 +373,15 @@ public NodeFactory(
TypeSystemContext = context;
CompilationModuleGroup = compilationModuleGroup;
ProfileDataManager = profileDataManager;
+ if (UsesWasmAuxiliaryModules && profileDataManager.HasMibcProfile)
+ {
+ _mibcHotMethods = new HashSet();
+ foreach (MethodDesc method in profileDataManager.GetMibcProfileMethods())
+ {
+ // Concrete reference instantiations share the emitted canonical body.
+ _mibcHotMethods.Add(method.GetCanonMethodTarget(CanonicalFormKind.Specific));
+ }
+ }
Target = context.Target;
NameMangler = nameMangler;
MetadataManager = new ReadyToRunTableManager(context);
@@ -598,6 +629,7 @@ private void CreateNodeCaches()
public SymbolNodeRange DelayLoadMethodCallThunks;
public InstanceEntryPointTableNode InstanceEntryPointTable;
+ public AuxiliaryModuleMetadataNode AuxiliaryModuleMetadata;
public ManifestMetadataTableNode ManifestMetadataTable;
@@ -1144,6 +1176,11 @@ public void AttachToDependencyGraph(DependencyAnalyzerBase graph, I
MethodEntryPointTableNode methodEntryPointTable = new MethodEntryPointTableNode(inputModule);
tableHeader.Add(Internal.Runtime.ReadyToRunSectionType.MethodDefEntryPoints, methodEntryPointTable);
+ if (UsesWasmAuxiliaryModules)
+ {
+ tableHeader.Add(Internal.Runtime.ReadyToRunSectionType.AuxiliaryMethodDefEntryPoints,
+ new MethodEntryPointTableNode(inputModule, auxiliary: true));
+ }
TypesTableNode typesTable = new TypesTableNode(inputModule);
tableHeader.Add(Internal.Runtime.ReadyToRunSectionType.AvailableTypes, typesTable);
@@ -1200,6 +1237,13 @@ public void AttachToDependencyGraph(DependencyAnalyzerBase graph, I
InstanceEntryPointTable = new InstanceEntryPointTableNode(this);
Header.Add(Internal.Runtime.ReadyToRunSectionType.InstanceMethodEntryPoints, InstanceEntryPointTable);
+ if (UsesWasmAuxiliaryModules)
+ {
+ Header.Add(Internal.Runtime.ReadyToRunSectionType.AuxiliaryInstanceMethodEntryPoints,
+ new InstanceEntryPointTableNode(this, auxiliary: true));
+ AuxiliaryModuleMetadata = new AuxiliaryModuleMetadataNode();
+ Header.Add(Internal.Runtime.ReadyToRunSectionType.AuxiliaryModuleMetadata, AuxiliaryModuleMetadata);
+ }
ImportSectionsTable = new ImportSectionsTableNode(this);
Header.Add(Internal.Runtime.ReadyToRunSectionType.ImportSections, ImportSectionsTable);
diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ProfileDataManager.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ProfileDataManager.cs
index cf1d8b49582057..26122e5468d0a1 100644
--- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ProfileDataManager.cs
+++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ProfileDataManager.cs
@@ -22,6 +22,7 @@ public class ProfileDataManager
// cannot be modified after loading and is accessed concurrently
// without locks.
private readonly ProfileDataMap _inputProfileData;
+ private readonly int _mibcFileCount;
// When synthesis is enabled, this represents the map of methods with
// synthesized PGO data. This is accessed and modified concurrently.
private readonly ProfileDataMap _synthesizedProfileData;
@@ -50,6 +51,7 @@ public ProfileDataManager(Logger logger,
_compilationGroup = compilationGroup;
_callChainProfile = callChainProfile;
HashSet versionBubble = new HashSet(versionBubbleModules);
+ _mibcFileCount = mibcFiles.Count;
{
// Parse MIbc Data
@@ -109,6 +111,18 @@ public bool IsMethodInInputProfileData(MethodDesc method)
return _inputProfileData.Contains(method);
}
+ public bool HasMibcProfile => _mibcFileCount > 0;
+
+ public IEnumerable GetMibcProfileMethods()
+ {
+ // MIBC inputs precede embedded IBC data; only explicit MIBC files select the partition.
+ for (int index = 0; index < _mibcFileCount; index++)
+ {
+ foreach (MethodProfileData method in _inputData[index].GetAllMethodProfileData())
+ yield return method.Method;
+ }
+ }
+
public MethodProfileData this[MethodDesc method]
{
get
diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCodegenCompilation.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCodegenCompilation.cs
index 2b3317ee0a1520..b8874ae9f0300d 100644
--- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCodegenCompilation.cs
+++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCodegenCompilation.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
+using System.Collections.Immutable;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
@@ -435,6 +436,18 @@ public override void Compile(string outputFile)
var nodes = _dependencyGraph.MarkedNodeList;
nodes = _fileLayoutOptimizer.ApplyProfilerGuidedMethodSort(nodes);
+ if (NodeFactory.UsesWasmAuxiliaryModules)
+ {
+ int hotIndex = 0;
+ int coldIndex = nodes.Length;
+ foreach (DependencyNodeCore node in nodes)
+ {
+ if (node is MethodWithGCInfo method)
+ method.CustomSort = NodeFactory.IsAuxiliaryMethod(method) ? coldIndex++ : hotIndex++;
+ }
+ nodes = nodes.Sort(new SortableDependencyNode.ObjectNodeComparer(CompilerComparer.Instance));
+ NodeFactory.AuxiliaryModuleMetadata.OutputFileName = Path.GetFileNameWithoutExtension(outputFile);
+ }
using (PerfEventSource.StartStopEvents.EmittingEvents())
{
diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/ILCompiler.ReadyToRun.csproj b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/ILCompiler.ReadyToRun.csproj
index 42d82ec3c14ee2..25cd62ef8bf873 100644
--- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/ILCompiler.ReadyToRun.csproj
+++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/ILCompiler.ReadyToRun.csproj
@@ -211,6 +211,7 @@
+
@@ -302,6 +303,7 @@
+
@@ -342,6 +344,7 @@
+
diff --git a/src/coreclr/tools/aot/crossgen2/Crossgen2RootCommand.cs b/src/coreclr/tools/aot/crossgen2/Crossgen2RootCommand.cs
index 22bac7896d6f0f..31efb56637ec7f 100644
--- a/src/coreclr/tools/aot/crossgen2/Crossgen2RootCommand.cs
+++ b/src/coreclr/tools/aot/crossgen2/Crossgen2RootCommand.cs
@@ -134,6 +134,8 @@ internal class Crossgen2RootCommand : RootCommand
new("--perfmap-format-version") { DefaultValueFactory = _ => 0, Description = SR.PerfMapFormatVersionOption };
public Option WasmDebugInfoOption { get; } =
new("--wasm-debug-info") { CustomParser = MakeWasmDebugInfo, DefaultValueFactory = MakeWasmDebugInfo, Description = SR.WasmDebugInfoOption, HelpName = "formats" };
+ public Option WasmAuxiliaryModules { get; } =
+ new("--wasm-auxiliary-modules") { Description = SR.WasmAuxiliaryModulesOption };
public Option CrossModuleInlining { get; } =
new("--opt-cross-module") { Description = SR.CrossModuleInlining };
public Option AsyncMethodOptimization { get; } =
@@ -233,6 +235,7 @@ public Crossgen2RootCommand(string[] args) : base(SR.Crossgen2BannerText)
Options.Add(PerfMapPath);
Options.Add(PerfMapFormatVersion);
Options.Add(WasmDebugInfoOption);
+ Options.Add(WasmAuxiliaryModules);
Options.Add(CrossModuleInlining);
Options.Add(AsyncMethodOptimization);
Options.Add(NonLocalGenericsModule);
diff --git a/src/coreclr/tools/aot/crossgen2/Program.cs b/src/coreclr/tools/aot/crossgen2/Program.cs
index 155e8b4de1a11e..f3ecc66957d513 100644
--- a/src/coreclr/tools/aot/crossgen2/Program.cs
+++ b/src/coreclr/tools/aot/crossgen2/Program.cs
@@ -91,6 +91,19 @@ public int Run()
?? GetTargetAllowsRuntimeCodeGeneration(targetOS, targetArchitecture);
bool targetIsSingleThreaded = Get(_command.TargetSingleThreaded)
?? GetTargetIsSingleThreaded(targetArchitecture);
+ if (Get(_command.WasmAuxiliaryModules) &&
+ (targetArchitecture != TargetArchitecture.Wasm32 || targetOS != TargetOS.Browser ||
+ !Get(_command.Composite) || !targetIsSingleThreaded || Get(_command.StripILBodies) ||
+ Get(_command.OutputFormat) == ReadyToRunContainerFormat.MachO))
+ {
+ throw new CommandLineException(SR.WasmAuxiliaryModulesUnsupportedConfiguration);
+ }
+ if (Get(_command.WasmAuxiliaryModules) &&
+ (Get(_command.Map) || Get(_command.MapCsv) || Get(_command.Pdb) || Get(_command.PerfMap) ||
+ !string.IsNullOrEmpty(Get(_command.CallChainProfileFile))))
+ {
+ throw new CommandLineException(SR.WasmAuxiliaryModulesUnsupportedDiagnostics);
+ }
// Crossgen2 is partial AOT and its pre-compiled methods can be thrown away at runtime if
// they mismatch in required ISAs or computed layouts of structs. On targets that allow
@@ -696,6 +709,7 @@ private void RunSingleCompilation(Dictionary inFilePaths, Instru
nodeFactoryFlags.StripInliningInfo = Get(_command.StripInliningInfo);
nodeFactoryFlags.StripDebugInfo = Get(_command.StripDebugInfo);
nodeFactoryFlags.StripILBodies = Get(_command.StripILBodies);
+ nodeFactoryFlags.WasmAuxiliaryModules = Get(_command.WasmAuxiliaryModules);
builder
.UseMapFile(Get(_command.Map))
diff --git a/src/coreclr/tools/aot/crossgen2/Properties/Resources.resx b/src/coreclr/tools/aot/crossgen2/Properties/Resources.resx
index 30710a5fa4b4f5..4f59011aecb785 100644
--- a/src/coreclr/tools/aot/crossgen2/Properties/Resources.resx
+++ b/src/coreclr/tools/aot/crossgen2/Properties/Resources.resx
@@ -480,4 +480,13 @@
Replace IL method bodies of compiled methods with a throwing stub in composite R2R output assemblies.
+
+ Partition a single-threaded composite Browser WebAssembly image into a primary image and delayed auxiliary modules.
+
+
+ --wasm-auxiliary-modules requires --targetarch wasm --targetos browser --composite, the WebAssembly output format, a single-threaded target, and retained IL bodies (without --strip-il-bodies).
+
+
+ --wasm-auxiliary-modules supports WebAssembly name sections and symbol maps, but not single-image map, PDB, perfmap, or call-chain profile files.
+
\ No newline at end of file
diff --git a/src/coreclr/tools/aot/crossgen2/README.md b/src/coreclr/tools/aot/crossgen2/README.md
new file mode 100644
index 00000000000000..9df145bccc5d74
--- /dev/null
+++ b/src/coreclr/tools/aot/crossgen2/README.md
@@ -0,0 +1,106 @@
+# Crossgen2 partitioned Browser-WASM output
+
+`--wasm-auxiliary-modules` partitions a composite Browser-WASM ReadyToRun image
+into independently activated WebAssembly modules. It requires a single-threaded
+target and retained IL; it cannot be combined with `--strip-il-bodies`.
+
+For example:
+
+```shell
+crossgen2 app.dll --composite --targetarch wasm --targetos browser \
+ --wasm-auxiliary-modules -r:System.Private.CoreLib.dll -o:app.r2r.wasm
+```
+
+In addition to the composite image and its component forwarding images, this emits:
+
+| File | Contents |
+|---|---|
+| `app.r2r.wasm` | Hot methods, interpreter fallback thunks, shared imports and reserved logical memory/table layout |
+| `app.r2r.entrypoints.wasm` | Auxiliary sparse and instantiated-method entrypoint records |
+| `app.r2r.1.wasm` | Cold methods and funclets, their fixup lists, exclusive import cells/signatures, GC/unwind and exception data |
+| `app.r2r.auxiliary.json` | Sibling filenames: `{"entrypoints":"app.r2r.entrypoints.wasm","modules":["app.r2r.1.wasm"]}` |
+
+Without `--mibc`, public nongeneric methods on nongeneric types initially stay hot. Compiler-generated wrappers,
+unmanaged entrypoints, runtime helpers and async/resumption methods also stay hot.
+Other eligible IL methods go into the cold module. Ordinary managed calls use
+indirect imports. Eligible managed JIT helpers remain directly callable: their
+bodies and funclets are copied into body modules while their logical table slots,
+virtual-IP identities and metadata remain canonical in the primary.
+Non-managed helper function bodies are also duplicated where needed.
+
+With one or more `--mibc startup.mibc` inputs, methods present in the union of
+the profiles stay primary, regardless of visibility or genericity. Remaining
+eligible methods go cold. Concrete reference-type instantiations select their
+shared canonical body; value-type instantiations remain distinct. Mandatory
+primary methods and helpers keep the rules above. An explicitly supplied empty
+profile, or one with no matching methods, moves every eligible method cold;
+it does not restore the no-profile heuristic. Embedded IBC and synthesized
+optimization profiles do not select auxiliary-module ownership.
+
+For example:
+
+```shell
+crossgen2 app.dll --composite --targetarch wasm --targetos browser \
+ --wasm-auxiliary-modules --mibc startup.mibc \
+ -r:System.Private.CoreLib.dll -o:app.r2r.wasm
+```
+
+Partitioning uses method membership, not a CPU-weight threshold. Use a profile
+captured over the intended startup/interaction interval and matching the
+published assemblies. This option consumes existing MIBC data; it does not
+collect a browser profile.
+
+All modules use the primary image's `webcil` imports and the same memory/table
+bases. Sidecar instantiation does not install code or data. Its idempotent
+`ActivateModule()` export installs passive data, rebases its owned import cells,
+installs cold table entries, then publishes activation status. The entrypoint and
+body sidecars can activate in either order. The host must notify the runtime when
+entrypoints activate.
+
+The primary exports `getAuxiliaryModuleMetadata(): i32`. It returns an absolute
+pointer to section 128, or zero for an ordinary image. The header has four
+little-endian DWORDs: entrypoint status, primary managed function count, auxiliary
+module count, and entrypoint filename RVA. Each auxiliary descriptor has four
+DWORDs: filename RVA, table-relative start, managed function count, and status.
+Counts include funclets but exclude helper slots. Status zero means pending;
+status one means active. Sections 129 and 130 hold auxiliary per-component
+MethodDef and image-wide instantiated-method entrypoints.
+
+Shared/eager imports, method read-only/writable data and async resume-info remain
+resident. The final runtime-function entry and its unwind data also remain
+resident so the runtime can reserve the complete virtual-IP range before download.
+Cold metadata never overwrites a fixup cell that primary code can resolve.
+
+Per-module WebAssembly name sections and primary symbol maps are supported.
+Single-image map, PDB, perfmap and call-chain profile output is rejected because
+its file-relative addresses cannot describe multiple physical modules.
+
+## Experimental worker preparation
+
+Browser hosts can opt into background auxiliary download and compilation:
+
+```javascript
+dotnet.withEnvironmentVariable("DOTNET_WasmAuxiliaryWorkerCompilation", "1");
+```
+
+The worker receives the resolved resource URL and the existing fetch options,
+including integrity, cache and credentials. Download retries and throttling still
+use the resource loader. Custom resource providers remain on the main thread;
+their response bodies are transferred to the worker for compilation. The worker
+returns a compiled `WebAssembly.Module`, not an instance or runtime memory.
+The main thread instantiates the compiled module after native image registration,
+then runs `ActivateModule` and entrypoint notification immediately in the
+instantiation continuation. It uses `WebAssembly.instantiate(module, imports)`,
+because Chromium rejects the synchronous `Instance` constructor for modules
+larger than 8 MB even when they are already compiled. This avoids starting
+compilation on the page but does not guarantee same-turn instantiation on every
+browser.
+
+This option is off by default and uses the separately published
+`dotnet.auxiliary.worker.js` module worker. It requires compiled-module cloning
+and transferable streams for custom responses. The worker script must be served
+from the application's origin and allowed by Content Security Policy; Blob URLs
+are not used. Failures are reported through the existing auxiliary-module error
+path rather than silently switching modes. Workers terminate when their pending
+operations settle. This does not allow activation during a long managed call:
+the main thread must still reach a scheduling opportunity.
diff --git a/src/coreclr/vm/codeman.cpp b/src/coreclr/vm/codeman.cpp
index 7ae59f754df90e..7a9d4e346a15f9 100644
--- a/src/coreclr/vm/codeman.cpp
+++ b/src/coreclr/vm/codeman.cpp
@@ -5980,6 +5980,8 @@ TADDR ExecutionManager::GetWasmVirtualIPFromFunctionTableIndex(DWORD functionInd
ReadyToRunInfo* pR2RInfo = pModule->GetReadyToRunInfo();
DWORD localIndex = functionIndex - pSection->minFunctionTableIndex;
+ if (!pR2RInfo->IsRuntimeFunctionActive(localIndex))
+ return 0;
do
{
PTR_RUNTIME_FUNCTION pRuntimeFunction = pR2RInfo->GetRuntimeFunctions() + localIndex;
@@ -6051,32 +6053,11 @@ TADDR ExecutionManager::GetWasmFunctionTableIndexFromVirtualIP(TADDR virtualIP)
return 0;
}
- DWORD localVirtualIP32 = (DWORD)localVirtualIP;
- PTR_RUNTIME_FUNCTION pRuntimeFunctions = pR2RInfo->GetRuntimeFunctions();
-
- // Find the function entry with the greatest BeginAddress <= localVirtualIP.
- DWORD low = 0;
- DWORD high = runtimeFunctionCount;
- while (low + 1 < high)
- {
- DWORD mid = low + ((high - low) / 2);
- DWORD beginAddress = RUNTIME_FUNCTION__BeginAddress(pRuntimeFunctions + mid);
- if (beginAddress <= localVirtualIP32)
- {
- low = mid;
- }
- else
- {
- high = mid;
- }
- }
-
- if (RUNTIME_FUNCTION__BeginAddress(pRuntimeFunctions + low) > localVirtualIP32)
- {
+ int functionIndex = pR2RInfo->FindRuntimeFunction(static_cast(localVirtualIP));
+ if (functionIndex < 0)
return 0;
- }
- return pR2RInfo->GetMinFunctionTableIndex() + low;
+ return pR2RInfo->GetMinFunctionTableIndex() + static_cast(functionIndex);
}
#endif // TARGET_WASM
@@ -6508,14 +6489,14 @@ PCODE ExecutionManager::getNextJumpStub(MethodDesc* pMD, PCODE target,
#endif // !DACCESS_COMPILE
static void GetFuncletStartOffsetsHelper(PCODE pCodeStart, SIZE_T size, SIZE_T ofsAdj,
- PTR_RUNTIME_FUNCTION pFunctionEntry, TADDR moduleBase,
+ PTR_RUNTIME_FUNCTION pFunctionEntry, PTR_RUNTIME_FUNCTION pFunctionTableEnd, TADDR moduleBase,
DWORD * pnFunclets, DWORD* pStartFuncletOffsets, DWORD dwLength)
{
_ASSERTE(FitsInU4((pCodeStart + size) - moduleBase));
DWORD endAddress = (DWORD)((pCodeStart + size) - moduleBase);
- // Entries are sorted and terminated by sentinel value (DWORD)-1
- for (; RUNTIME_FUNCTION__BeginAddress(pFunctionEntry) < endAddress; pFunctionEntry++)
+ // An auxiliary module's next range may not be installed yet.
+ for (; pFunctionEntry < pFunctionTableEnd && RUNTIME_FUNCTION__BeginAddress(pFunctionEntry) < endAddress; pFunctionEntry++)
{
#ifdef TARGET_AMD64
_ASSERTE((pFunctionEntry->UnwindData & RUNTIME_FUNCTION_INDIRECT) == 0);
@@ -7220,13 +7201,17 @@ BOOL ReadyToRunJitManager::JitCodeToMethodInfo(RangeSection * pRangeSection,
Module * pModule = pRangeSection->_pR2RModule;
ReadyToRunInfo * pInfo = pModule->GetReadyToRunInfo();
- COUNT_T nRuntimeFunctions = pInfo->m_nRuntimeFunctions;
PTR_RUNTIME_FUNCTION pRuntimeFunctions = pInfo->m_pRuntimeFunctions;
+#ifdef TARGET_WASM
+ int MethodIndex = pInfo->FindRuntimeFunction(RelativePc);
+#else
+ COUNT_T nRuntimeFunctions = pInfo->m_nRuntimeFunctions;
int MethodIndex = NativeUnwindInfoLookupTable::LookupUnwindInfoForMethod(RelativePc,
pRuntimeFunctions,
0,
nRuntimeFunctions - 1);
+#endif // TARGET_WASM
if (MethodIndex < 0)
return FALSE;
@@ -7315,6 +7300,12 @@ TADDR ReadyToRunJitManager::GetFuncletStartAddress(EECodeInfo * pCodeInfo)
DWORD ReadyToRunJitManager::GetFuncletStartOffsets(const METHODTOKEN& MethodToken, DWORD* pStartFuncletOffsets, DWORD dwLength)
{
PTR_RUNTIME_FUNCTION pFirstFuncletFunctionEntry = dac_cast(MethodToken.m_pCodeHeader) + 1;
+ ReadyToRunInfo* pInfo = JitTokenToReadyToRunInfo(MethodToken);
+ DWORD rangeEnd = pInfo->GetRuntimeFunctionCount();
+#ifdef TARGET_WASM
+ rangeEnd = pInfo->GetRuntimeFunctionRangeEnd(
+ static_cast(pFirstFuncletFunctionEntry - pInfo->GetRuntimeFunctions() - 1));
+#endif
TADDR moduleBase = JitTokenToModuleFunctionsBase(MethodToken);
DWORD nFunclets = 0;
@@ -7327,13 +7318,12 @@ DWORD ReadyToRunJitManager::GetFuncletStartOffsets(const METHODTOKEN& MethodToke
// fragments until the first funclet, if any, is found.
GetFuncletStartOffsetsHelper(regionInfo.hotStartAddress, regionInfo.hotSize, 0,
- pFirstFuncletFunctionEntry, moduleBase,
+ pFirstFuncletFunctionEntry, pInfo->GetRuntimeFunctions() + rangeEnd, moduleBase,
&nFunclets, pStartFuncletOffsets, dwLength);
// Technically, the cold code is not a funclet, but it looks like the debugger wants it
if (regionInfo.coldSize > 0)
{
- ReadyToRunInfo * pInfo = JitTokenToReadyToRunInfo(MethodToken);
PTR_RUNTIME_FUNCTION pRuntimeFunctions = pInfo->m_pRuntimeFunctions;
int i = 0;
while (true)
@@ -7347,7 +7337,7 @@ DWORD ReadyToRunJitManager::GetFuncletStartOffsets(const METHODTOKEN& MethodToke
}
GetFuncletStartOffsetsHelper(regionInfo.coldStartAddress, regionInfo.coldSize, 0,
- pFirstFuncletFunctionEntry, moduleBase,
+ pFirstFuncletFunctionEntry, pRuntimeFunctions + pInfo->GetRuntimeFunctionCount(), moduleBase,
&nFunclets, pStartFuncletOffsets, dwLength);
}
diff --git a/src/coreclr/vm/interpexec.cpp b/src/coreclr/vm/interpexec.cpp
index 281eb5176de9a4..fa476d5f234ff7 100644
--- a/src/coreclr/vm/interpexec.cpp
+++ b/src/coreclr/vm/interpexec.cpp
@@ -1477,6 +1477,45 @@ static InterpByteCodeStart* PrepareInterpreterCode(MethodDesc* targetMethod, Int
return targetIp;
}
+static InterpByteCodeStart* ResolveInterpreterCode(MethodDesc*& targetMethod, InterpMethodContextFrame* pFrame, InterpreterFrame* pInterpreterFrame, const int32_t* ip)
+{
+ InterpByteCodeStart* targetIp;
+ if (!targetMethod->IsInterpreterCodeInitialized(targetIp))
+ {
+ targetIp = PrepareInterpreterCode(targetMethod, pFrame, pInterpreterFrame, ip);
+ }
+
+#ifdef FEATURE_AUXILIARY_R2R_MODULES
+ // This info is not cleared when R2R code is published, so calls through a stale override alias still
+ // reach the prestub and are redirected to the published native code.
+ if (targetIp != nullptr
+ && targetIp->Method->auxiliaryMethodInfo.ShouldTryActivateMethod())
+ {
+ pFrame->ip = ip;
+ pInterpreterFrame->SetTopInterpMethodContextFrame(pFrame);
+ MethodDesc* pActualMethod = targetIp->Method->methodHnd;
+ // A method that already switched to R2R code has no interpreter code left, so a call through an alias
+ // only needs the redirect below.
+ if (pActualMethod->GetInterpreterCode() != nullptr)
+ {
+ GCX_PREEMP_REGION_BEGIN();
+ CallWithSEHWrapper(
+ [pActualMethod]() {
+ return pActualMethod->DoPrestub(nullptr, CallerGCMode::Coop);
+ });
+ GCX_PREEMP_REGION_END();
+ }
+ if (pActualMethod->GetInterpreterCode() == nullptr)
+ {
+ targetMethod = pActualMethod;
+ return nullptr;
+ }
+ }
+#endif // FEATURE_AUXILIARY_R2R_MODULES
+
+ return targetIp;
+}
+
void InterpExecMethod(InterpreterFrame *pInterpreterFrame, InterpMethodContextFrame *pFrame, InterpThreadContext *pThreadContext, ExceptionClauseArgs *pExceptionClauseArgs)
{
CONTRACTL
@@ -3582,11 +3621,7 @@ void InterpExecMethod(InterpreterFrame *pInterpreterFrame, InterpMethodContextFr
targetMethod = NonVirtualEntry2MethodDesc(actualTarget);
}
- PTR_InterpByteCodeStart targetIp;
- if (!targetMethod->IsInterpreterCodeInitialized(targetIp))
- {
- targetIp = PrepareInterpreterCode(targetMethod, pFrame, pInterpreterFrame, ip);
- }
+ PTR_InterpByteCodeStart targetIp = ResolveInterpreterCode(targetMethod, pFrame, pInterpreterFrame, ip);
if (targetIp != NULL)
{
pFrame->ip = ip;
@@ -3680,11 +3715,7 @@ void InterpExecMethod(InterpreterFrame *pInterpreterFrame, InterpMethodContextFr
// Save current execution state for when we return from called method
pFrame->ip = ip;
- InterpByteCodeStart* targetIp;
- if (!targetMethod->IsInterpreterCodeInitialized(targetIp))
- {
- targetIp = PrepareInterpreterCode(targetMethod, pFrame, pInterpreterFrame, ip);
- }
+ InterpByteCodeStart* targetIp = ResolveInterpreterCode(targetMethod, pFrame, pInterpreterFrame, ip);
if (targetIp == NULL)
{
// If we didn't get the interpreter code pointer setup, then this is a method we need to invoke as a compiled method.
diff --git a/src/coreclr/vm/interpexec.h b/src/coreclr/vm/interpexec.h
index 7300cda7a14b6d..42df461545f6db 100644
--- a/src/coreclr/vm/interpexec.h
+++ b/src/coreclr/vm/interpexec.h
@@ -123,4 +123,22 @@ void InvokeManagedMethod(MethodDesc *pMD, int8_t *pArgs, int8_t *pRet, PCODE tar
void InterpDispatchCache_ReclaimAll();
void InterpDispatchCache_ClearForLoaderAllocator(LoaderAllocator* pLoaderAllocator);
+#ifdef FEATURE_AUXILIARY_R2R_MODULES
+inline bool AuxiliaryMethodInfo::ShouldTryActivateMethod() const
+{
+ LIMITED_METHOD_CONTRACT;
+ switch (discriminator)
+ {
+ case AuxiliaryMethodInfoDiscriminator::CheckAuxiliaryModuleActivationFlag:
+ ReadyToRunInfo::AdvanceSimulatedAuxiliaryModuleDownload(auxiliaryModuleActivationFlag);
+ return ReadyToRunInfo::IsAuxiliaryModuleActivated(auxiliaryModuleActivationFlag);
+ case AuxiliaryMethodInfoDiscriminator::CheckAuxiliaryEntryPointsEra:
+ ReadyToRunInfo::AdvanceSimulatedAuxiliaryEntryPointsDownload();
+ return ReadyToRunInfo::IsAuxiliaryEntryPointsEraStale(auxiliaryEntryPointsEra);
+ default:
+ return false;
+ }
+}
+#endif // FEATURE_AUXILIARY_R2R_MODULES
+
#endif // _INTERPEXEC_H_
diff --git a/src/coreclr/vm/jitinterface.cpp b/src/coreclr/vm/jitinterface.cpp
index da08628a6e4306..c09cddfb5ce295 100644
--- a/src/coreclr/vm/jitinterface.cpp
+++ b/src/coreclr/vm/jitinterface.cpp
@@ -14119,6 +14119,13 @@ BOOL LoadDynamicInfoEntry(Module *currentModule,
}
PrepareCodeConfig config(NativeCodeVersion(pMD), FALSE, TRUE);
+#ifdef FEATURE_AUXILIARY_R2R_MODULES
+ // R2R callers call this method directly, so it must have its R2R entry point now.
+ // Do not simulate a delayed auxiliary module activation for this.
+ // Once we no longer need simulations, we can remove this call.
+ // The compile should ensure we never have a direct call across different auxiliary modules.
+ config.DisableAuxiliaryModuleDelay();
+#endif // FEATURE_AUXILIARY_R2R_MODULES
PCODE pEntryPoint = pMD->GetModule()->GetReadyToRunInfo()->GetEntryPoint(pMD, &config, TRUE /* fFixups */);
if (pEntryPoint == (PCODE)NULL)
{
diff --git a/src/coreclr/vm/method.hpp b/src/coreclr/vm/method.hpp
index 7a930af947c329..6f233fc2a8b7e0 100644
--- a/src/coreclr/vm/method.hpp
+++ b/src/coreclr/vm/method.hpp
@@ -279,6 +279,12 @@ struct MethodDescCodeData final
#ifdef FEATURE_INTERPRETER
InterpreterCalliCookie CalliCookie;
#endif // FEATURE_INTERPRETER
+#ifdef FEATURE_AUXILIARY_R2R_MODULES
+ // Activation status of the auxiliary module holding this method's R2R code. Zero means activated.
+ // TODO: This points at a shared countdown standing in for the auxiliary module's activation flag.
+ PTR_DWORD AuxiliaryModuleActivationFlag;
+ AuxiliaryEntryPointsEra AuxiliaryEntryPointsEra;
+#endif // FEATURE_AUXILIARY_R2R_MODULES
#if defined(_DEBUG) && defined(ALLOW_SXS_JIT)
PatchpointInfo *AltJitPatchpointInfo;
#endif // _DEBUG && ALLOW_SXS_JIT
@@ -2374,6 +2380,11 @@ class MethodDesc
bool TryPublishR2RCodeForUnmanagedCallersOnly();
#endif // FEATURE_PORTABLE_ENTRYPOINTS
+#ifdef FEATURE_AUXILIARY_R2R_MODULES
+ void TryActivateDelayedR2RCode(CallerGCMode callerGCMode);
+ void RecordDelayedR2RCode(PrepareCodeConfig* pConfig, PCODE pCode);
+#endif // FEATURE_AUXILIARY_R2R_MODULES
+
private:
PCODE GetPrecompiledCode(PrepareCodeConfig* pConfig, bool shouldTier);
PCODE GetPrecompiledR2RCode(PrepareCodeConfig* pConfig);
@@ -2458,6 +2469,74 @@ class PrepareCodeConfig
CallerGCMode GetCallerGCMode();
void SetCallerGCMode(CallerGCMode mode);
+#ifdef FEATURE_AUXILIARY_R2R_MODULES
+ // Activation status of the auxiliary module holding the method's R2R code, set when lookup delays it.
+ PTR_DWORD GetAuxiliaryModuleActivationFlag() const
+ {
+ LIMITED_METHOD_CONTRACT;
+ return m_pAuxiliaryModuleActivationFlag;
+ }
+
+ bool HasKnownAuxiliaryModuleDependency() const
+ {
+ LIMITED_METHOD_CONTRACT;
+ return m_pAuxiliaryModuleActivationFlag != nullptr;
+ }
+
+ bool IsAuxiliaryModuleActivated() const
+ {
+ CONTRACTL
+ {
+ NOTHROW;
+ GC_NOTRIGGER;
+ MODE_ANY;
+ PRECONDITION(m_pAuxiliaryModuleActivationFlag != nullptr);
+ }
+ CONTRACTL_END;
+
+ return ReadyToRunInfo::IsAuxiliaryModuleActivated(m_pAuxiliaryModuleActivationFlag);
+ }
+
+ void SetAuxiliaryModuleActivationFlag(PTR_DWORD pAuxiliaryModuleActivationFlag)
+ {
+ LIMITED_METHOD_CONTRACT;
+ _ASSERTE(pAuxiliaryModuleActivationFlag != nullptr && m_pAuxiliaryModuleActivationFlag == nullptr);
+ m_pAuxiliaryModuleActivationFlag = pAuxiliaryModuleActivationFlag;
+ }
+
+ // Lookups that commit a method to R2R code for direct calls from R2R callers cannot wait for activation.
+ bool MayDelayAuxiliaryModule() const
+ {
+ LIMITED_METHOD_CONTRACT;
+ return m_mayDelayAuxiliaryModule;
+ }
+
+ void DisableAuxiliaryModuleDelay()
+ {
+ LIMITED_METHOD_CONTRACT;
+ m_mayDelayAuxiliaryModule = false;
+ }
+
+ bool GetIsAwaitingAuxiliaryModuleEntryPoints() const
+ {
+ LIMITED_METHOD_CONTRACT;
+ return m_auxiliaryEntryPointsEra.IsAwaiting();
+ }
+
+ AuxiliaryEntryPointsEra GetAuxiliaryEntryPointsEra() const
+ {
+ LIMITED_METHOD_CONTRACT;
+ return m_auxiliaryEntryPointsEra;
+ }
+
+ void SetIsAwaitingAuxiliaryModuleEntryPoints(AuxiliaryEntryPointsEra era)
+ {
+ LIMITED_METHOD_CONTRACT;
+ _ASSERTE(era.IsAwaiting());
+ m_auxiliaryEntryPointsEra = era;
+ }
+#endif // FEATURE_AUXILIARY_R2R_MODULES
+
public:
bool IsForMulticoreJit() const
{
@@ -2590,6 +2669,12 @@ class PrepareCodeConfig
BOOL m_ReadyToRunRejectedPrecompiledCode;
CallerGCMode m_callerGCMode;
+#ifdef FEATURE_AUXILIARY_R2R_MODULES
+ PTR_DWORD m_pAuxiliaryModuleActivationFlag = nullptr;
+ bool m_mayDelayAuxiliaryModule = true;
+ AuxiliaryEntryPointsEra m_auxiliaryEntryPointsEra;
+#endif // FEATURE_AUXILIARY_R2R_MODULES
+
#ifdef FEATURE_MULTICOREJIT
private:
bool m_isForMulticoreJit;
diff --git a/src/coreclr/vm/precode_portable.hpp b/src/coreclr/vm/precode_portable.hpp
index fc2199778470e0..6d46d8f691cb93 100644
--- a/src/coreclr/vm/precode_portable.hpp
+++ b/src/coreclr/vm/precode_portable.hpp
@@ -41,6 +41,10 @@ class PortableEntryPoint final
// closed-static-retbuf resolution list, so a redundant registration attempt
// can be rejected without a linear scan of that list.
kPendingClosedStaticRetBufResolution = 0x8,
+#ifdef FEATURE_AUXILIARY_R2R_MODULES
+ kPendingAuxiliaryModule = 0x10,
+ kAwaitingAuxiliaryEntryPoints = 0x20,
+#endif // FEATURE_AUXILIARY_R2R_MODULES
};
Volatile _flags;
@@ -150,6 +154,52 @@ class PortableEntryPoint final
ClearFlagsInterlocked(kPendingClosedStaticRetBufResolution);
}
+#ifdef FEATURE_AUXILIARY_R2R_MODULES
+ bool IsPendingAuxiliaryModule() const
+ {
+ LIMITED_METHOD_CONTRACT;
+ _ASSERTE(IsValid());
+ return (_flags & kPendingAuxiliaryModule) != 0;
+ }
+
+ void SetPendingAuxiliaryModule()
+ {
+ LIMITED_METHOD_CONTRACT;
+ _ASSERTE(IsValid());
+ _ASSERTE(PrefersInterpreterEntryPoint());
+ SetFlagsInterlocked(kPendingAuxiliaryModule);
+ }
+
+ void ClearPendingAuxiliaryModule()
+ {
+ LIMITED_METHOD_CONTRACT;
+ _ASSERTE(IsValid());
+ ClearFlagsInterlocked(kPendingAuxiliaryModule);
+ }
+
+ bool IsAwaitingAuxiliaryEntryPoints() const
+ {
+ LIMITED_METHOD_CONTRACT;
+ _ASSERTE(IsValid());
+ return (_flags & kAwaitingAuxiliaryEntryPoints) != 0;
+ }
+
+ void SetAwaitingAuxiliaryEntryPoints()
+ {
+ LIMITED_METHOD_CONTRACT;
+ _ASSERTE(IsValid());
+ _ASSERTE(PrefersInterpreterEntryPoint());
+ SetFlagsInterlocked(kAwaitingAuxiliaryEntryPoints);
+ }
+
+ void ClearAwaitingAuxiliaryEntryPoints()
+ {
+ LIMITED_METHOD_CONTRACT;
+ _ASSERTE(IsValid());
+ ClearFlagsInterlocked(kAwaitingAuxiliaryEntryPoints);
+ }
+#endif // FEATURE_AUXILIARY_R2R_MODULES
+
// Atomically install an interpreter thunk if _pActualCode is still NULL.
// Returns true if the thunk was installed, false if _pActualCode was already set.
bool TrySetInterpreterThunk(void* thunk)
diff --git a/src/coreclr/vm/prestub.cpp b/src/coreclr/vm/prestub.cpp
index 5a828874757a65..4354ef09e6cb4c 100644
--- a/src/coreclr/vm/prestub.cpp
+++ b/src/coreclr/vm/prestub.cpp
@@ -313,6 +313,10 @@ PCODE MethodDesc::PrepareInitialCode(CallerGCMode callerGCMode)
STANDARD_VM_CONTRACT;
PrepareCodeConfig config(NativeCodeVersion(this), TRUE, TRUE);
config.SetCallerGCMode(callerGCMode);
+#ifdef FEATURE_AUXILIARY_R2R_MODULES
+ if (m_codeData->AuxiliaryModuleActivationFlag != nullptr)
+ config.SetAuxiliaryModuleActivationFlag(m_codeData->AuxiliaryModuleActivationFlag);
+#endif
return PrepareCode(&config);
}
@@ -397,6 +401,9 @@ PCODE MethodDesc::PrepareCode(PrepareCodeConfig* pConfig)
LOG((LF_CLASSLOADER, LL_INFO1000000,
" In PrepareILBasedCode, calling JitCompileCode\n"));
pCode = JitCompileCode(pConfig);
+#ifdef FEATURE_AUXILIARY_R2R_MODULES
+ RecordDelayedR2RCode(pConfig, pCode);
+#endif // FEATURE_AUXILIARY_R2R_MODULES
}
else
{
@@ -491,6 +498,10 @@ PCODE MethodDesc::GetPrecompiledR2RCode(PrepareCodeConfig* pConfig)
PCODE pCode = (PCODE)NULL;
#ifdef FEATURE_READYTORUN
+#ifdef FEATURE_AUXILIARY_R2R_MODULES
+ AuxiliaryEntryPointsEra searchedEra = ReadyToRunInfo::GetCurrentAuxiliaryEntryPointsEra();
+#endif
+
ReadyToRunInfo* pAlreadyExaminedInfos[2] = {NULL, NULL};
Module * pModule = GetModule();
if (pModule->IsReadyToRun())
@@ -499,6 +510,12 @@ PCODE MethodDesc::GetPrecompiledR2RCode(PrepareCodeConfig* pConfig)
pCode = pAlreadyExaminedInfos[0]->GetEntryPoint(this, pConfig, TRUE /* fFixups */);
}
+#ifdef FEATURE_AUXILIARY_R2R_MODULES
+ // The method's code was found in an image whose auxiliary module is not yet activated, so the search is over.
+ if (pConfig->HasKnownAuxiliaryModuleDependency())
+ return (PCODE)NULL;
+#endif // FEATURE_AUXILIARY_R2R_MODULES
+
// Generics may be located in several places
if (pCode == (PCODE)NULL && HasClassOrMethodInstantiation())
{
@@ -511,6 +528,11 @@ PCODE MethodDesc::GetPrecompiledR2RCode(PrepareCodeConfig* pConfig)
pCode = pAlreadyExaminedInfos[1]->GetEntryPoint(this, pConfig, TRUE /* fFixups */);
}
+#ifdef FEATURE_AUXILIARY_R2R_MODULES
+ if (pConfig->HasKnownAuxiliaryModuleDependency())
+ return (PCODE)NULL;
+#endif // FEATURE_AUXILIARY_R2R_MODULES
+
if (pCode == (PCODE)NULL)
{
// R2R also supports a concept of R2R code that has code for "Unrelated" generics embedded within it
@@ -523,11 +545,25 @@ PCODE MethodDesc::GetPrecompiledR2RCode(PrepareCodeConfig* pConfig)
if (pUnrelatedInfo == pAlreadyExaminedInfos[1]) continue;
pCode = pUnrelatedInfo->GetEntryPoint(this, pConfig, TRUE /* fFixups */);
+#ifdef FEATURE_AUXILIARY_R2R_MODULES
+ if (pConfig->HasKnownAuxiliaryModuleDependency())
+ return (PCODE)NULL;
+#endif // FEATURE_AUXILIARY_R2R_MODULES
}
}
}
+
+#ifdef FEATURE_AUXILIARY_R2R_MODULES
+ if (pCode == (PCODE)NULL && !pConfig->ReadyToRunRejectedPrecompiledCode())
+ {
+ // Even when every searched image has its EntryPoints module activated, an image registered later may have
+ // the code, which also advances the era.
+ pConfig->SetIsAwaitingAuxiliaryModuleEntryPoints(searchedEra);
+ }
#endif
+#endif // FEATURE_READYTORUN
+
return pCode;
}
@@ -553,6 +589,119 @@ bool MethodDesc::TryPublishR2RCodeForUnmanagedCallersOnly()
}
#endif // FEATURE_PORTABLE_ENTRYPOINTS
+#ifdef FEATURE_AUXILIARY_R2R_MODULES
+// Records why an interpreted method may get R2R code later, so callers check before interpreting it.
+// A method found in a pending auxiliary module waits for that module; a method not found while
+// auxiliary EntryPoints modules were pending waits for the EntryPoints era to advance.
+void MethodDesc::RecordDelayedR2RCode(PrepareCodeConfig* pConfig, PCODE pCode)
+{
+ STANDARD_VM_CONTRACT;
+
+ if (!PortableEntryPoint::PrefersInterpreterEntryPoint(pCode))
+ return;
+
+ _ASSERTE(pCode == GetPortableEntryPoint());
+ _ASSERTE(m_codeData != nullptr);
+ PortableEntryPoint* entryPoint = PortableEntryPoint::ToPortableEntryPoint(pCode);
+ InterpMethod* pInterpMethod = PortableEntryPoint::HasInterpreterData(pCode)
+ ? static_cast(PortableEntryPoint::GetInterpreterData(pCode))->Method
+ : nullptr;
+
+ // R2R code that was rejected is never retried, so the method stays interpreted.
+ if (pConfig->ReadyToRunRejectedPrecompiledCode())
+ {
+ if (pInterpMethod != nullptr)
+ pInterpMethod->auxiliaryMethodInfo = AuxiliaryMethodInfo();
+ return;
+ }
+
+ // The InterpMethod info is not cleared when R2R code is published, so calls through a stale override alias
+ // still reach the prestub and are redirected to the published native code.
+ if (pConfig->HasKnownAuxiliaryModuleDependency())
+ {
+ m_codeData->AuxiliaryModuleActivationFlag = pConfig->GetAuxiliaryModuleActivationFlag();
+ m_codeData->AuxiliaryEntryPointsEra = AuxiliaryEntryPointsEra();
+ entryPoint->ClearAwaitingAuxiliaryEntryPoints();
+ entryPoint->SetPendingAuxiliaryModule();
+ if (pInterpMethod != nullptr)
+ {
+ pInterpMethod->auxiliaryMethodInfo.auxiliaryModuleActivationFlag = pConfig->GetAuxiliaryModuleActivationFlag();
+ pInterpMethod->auxiliaryMethodInfo.discriminator = AuxiliaryMethodInfoDiscriminator::CheckAuxiliaryModuleActivationFlag;
+ }
+ }
+ else if (pConfig->GetIsAwaitingAuxiliaryModuleEntryPoints())
+ {
+ m_codeData->AuxiliaryEntryPointsEra = pConfig->GetAuxiliaryEntryPointsEra();
+ entryPoint->SetAwaitingAuxiliaryEntryPoints();
+ if (pInterpMethod != nullptr)
+ {
+ pInterpMethod->auxiliaryMethodInfo.auxiliaryEntryPointsEra = pConfig->GetAuxiliaryEntryPointsEra();
+ pInterpMethod->auxiliaryMethodInfo.discriminator = AuxiliaryMethodInfoDiscriminator::CheckAuxiliaryEntryPointsEra;
+ }
+ }
+ // Otherwise no R2R lookup was done, so there is nothing to wait for and the default InterpretOnly info stays.
+}
+
+// Called by the prestub. A method waiting on its auxiliary module already has interpreter code, so callers
+// only send it back to the prestub once the module is activated. Switch it to its R2R code if it has any.
+void MethodDesc::TryActivateDelayedR2RCode(CallerGCMode callerGCMode)
+{
+ CONTRACTL
+ {
+ THROWS;
+ GC_TRIGGERS;
+ MODE_PREEMPTIVE;
+ }
+ CONTRACTL_END;
+
+ PCODE temporaryEntryPoint = GetPortableEntryPointIfExists();
+ if (temporaryEntryPoint == (PCODE)NULL)
+ return;
+
+ PortableEntryPoint* entryPoint = PortableEntryPoint::ToPortableEntryPoint(temporaryEntryPoint);
+ if (!entryPoint->PrefersInterpreterEntryPoint())
+ return;
+
+ bool moduleActivated = entryPoint->IsPendingAuxiliaryModule()
+ && ReadyToRunInfo::IsAuxiliaryModuleActivated(m_codeData->AuxiliaryModuleActivationFlag);
+ bool entryPointsEraStale = entryPoint->IsAwaitingAuxiliaryEntryPoints()
+ && ReadyToRunInfo::IsAuxiliaryEntryPointsEraStale(m_codeData->AuxiliaryEntryPointsEra);
+ if (!moduleActivated && !entryPointsEraStale)
+ return;
+
+ entryPoint->ClearPendingAuxiliaryModule();
+ entryPoint->ClearAwaitingAuxiliaryEntryPoints();
+ EX_TRY
+ {
+ PrepareCodeConfig config(NativeCodeVersion(this), FALSE, TRUE);
+ config.SetCallerGCMode(callerGCMode);
+ PCODE code = GetPrecompiledCode(&config, false);
+ if (code != (PCODE)NULL)
+ {
+ PoisonInterpreterCode();
+ PerfMap::LogPreCompiledMethod(this, code);
+ DACNotifyCompilationFinished(this, code);
+ }
+ else
+ {
+ RecordDelayedR2RCode(&config, temporaryEntryPoint);
+ }
+ }
+ EX_CATCH
+ {
+ // The method already runs in the interpreter, so failing to load its R2R code (for example,
+ // a fixup that throws) must not fail the call. It stays interpreted and is not retried.
+ RethrowTerminalExceptions();
+ if (PortableEntryPoint::HasInterpreterData(temporaryEntryPoint))
+ {
+ static_cast(PortableEntryPoint::GetInterpreterData(temporaryEntryPoint))->Method->auxiliaryMethodInfo =
+ AuxiliaryMethodInfo();
+ }
+ }
+ EX_END_CATCH
+}
+#endif // FEATURE_AUXILIARY_R2R_MODULES
+
PCODE MethodDesc::GetMulticoreJitCode(PrepareCodeConfig* pConfig, bool* pWasTier0)
{
STANDARD_VM_CONTRACT;
@@ -2223,12 +2372,25 @@ void ExecuteInterpretedMethodWithArgs_PortableEntryPoint_Complex(PCODE portableE
// The method to run; differs from pMethod when a MethodImpl in the same type redirects pMethod's slot.
MethodDesc* pTargetMethod = pMethod;
InterpByteCodeStart* targetIp = pMethod->GetInterpreterCode();
+ const bool firstCall = (targetIp == NULL);
+ bool enterPrestubPortion = firstCall;
+ bool runPrestub = firstCall;
if (targetIp != NULL)
{
// PrepareInterpreterCode may have stored the MethodImpl target's code on the declaration; run as that method.
pTargetMethod = targetIp->Method->methodHnd;
+#ifdef FEATURE_AUXILIARY_R2R_MODULES
+ // This is not only reached on the first call: a method waiting for its R2R code comes back here once the wait
+ // is over. A call through a stale alias of a method that already switched to R2R code only needs the dispatch.
+ if (targetIp->Method->auxiliaryMethodInfo.ShouldTryActivateMethod())
+ {
+ enterPrestubPortion = true;
+ runPrestub = pTargetMethod->GetInterpreterCode() != NULL;
+ }
+#endif // FEATURE_AUXILIARY_R2R_MODULES
}
- else
+
+ if (enterPrestubPortion)
{
MAKE_CURRENT_THREAD_AVAILABLE_EX(GetThread());
@@ -2247,11 +2409,14 @@ void ExecuteInterpretedMethodWithArgs_PortableEntryPoint_Complex(PCODE portableE
{
GCX_PREEMP_REGION_BEGIN();
// Match the interpreter's PrepareInterpreterCode: run the method that pMethod's slot resolves to.
- if (pMethod->IsVtableSlot())
+ if (firstCall && pMethod->IsVtableSlot())
{
pTargetMethod = MethodTable::MapMethodDeclToMethodImpl(pMethod);
}
- (void)pTargetMethod->DoPrestub(NULL /* MethodTable */, CallerGCMode::Coop);
+ if (runPrestub)
+ {
+ (void)pTargetMethod->DoPrestub(NULL /* MethodTable */, CallerGCMode::Coop);
+ }
targetIp = pTargetMethod->GetInterpreterCode();
GCX_PREEMP_REGION_END();
}
@@ -2323,16 +2488,26 @@ void ExecuteInterpretedMethodWithArgs_PortableEntryPoint_Complex(PCODE portableE
void ExecuteInterpretedMethodWithArgs_PortableEntryPoint(PCODE portableEntrypoint, TransitionBlock* block, size_t argsSize, int8_t* retBuff)
{
- PCODE targetIp;
+ InterpByteCodeStart* targetIp = PortableEntryPoint::HasInterpreterData(portableEntrypoint)
+ ? static_cast(PortableEntryPoint::GetInterpreterData(portableEntrypoint))
+ : nullptr;
- if (!PortableEntryPoint::HasInterpreterData(portableEntrypoint))
+#ifdef FEATURE_AUXILIARY_R2R_MODULES
+ if (targetIp != nullptr
+ && targetIp->Method->auxiliaryMethodInfo.MayActivateMethod())
+ {
+ ExecuteInterpretedMethodWithArgs_PortableEntryPoint_Complex(portableEntrypoint, block, argsSize, retBuff);
+ return;
+ }
+#endif // FEATURE_AUXILIARY_R2R_MODULES
+
+ if (targetIp == nullptr)
{
// In this case, we're using this entrypoint like the prestub.
ExecuteInterpretedMethodWithArgs_PortableEntryPoint_Complex(portableEntrypoint, block, argsSize, retBuff);
}
else
{
- targetIp = (PCODE)PortableEntryPoint::GetInterpreterData(portableEntrypoint);
int8_t* args = (int8_t*)(block + 1);
// Copy arguments to the stack
@@ -2541,6 +2716,11 @@ PCODE MethodDesc::DoPrestub(MethodTable *pDispatchingMT, CallerGCMode callerGCMo
}
#endif // FEATURE_CODE_VERSIONING
+#ifdef FEATURE_AUXILIARY_R2R_MODULES
+ // A method waiting on its auxiliary module has interpreter code, so ShouldCallPrestub() is false either way.
+ TryActivateDelayedR2RCode(callerGCMode);
+#endif // FEATURE_AUXILIARY_R2R_MODULES
+
if (!ShouldCallPrestub())
{
LOG((LF_CLASSLOADER, LL_INFO10000,
diff --git a/src/coreclr/vm/readytoruninfo.cpp b/src/coreclr/vm/readytoruninfo.cpp
index b0d0d865506f7a..29ae725ce2e0ac 100644
--- a/src/coreclr/vm/readytoruninfo.cpp
+++ b/src/coreclr/vm/readytoruninfo.cpp
@@ -91,6 +91,93 @@ IMAGE_DATA_DIRECTORY * ReadyToRunCoreInfo::FindSection(ReadyToRunSectionType typ
return NULL;
}
+#ifdef TARGET_WASM
+DWORD ReadyToRunInfo::GetRuntimeFunctionRangeEnd(DWORD index) const
+{
+ LIMITED_METHOD_CONTRACT;
+ _ASSERTE(index < m_nRuntimeFunctions);
+#ifdef FEATURE_AUXILIARY_R2R_MODULES
+ if (m_pAuxiliaryModuleMetadata != nullptr)
+ {
+ PTR_READYTORUN_AUXILIARY_MODULE entry = GetAuxiliaryModule(index);
+ return entry == nullptr ? m_pAuxiliaryModuleMetadata->PrimaryModuleMethodCount :
+ entry->RelativeTableBase + entry->MethodCount;
+ }
+#endif
+ return m_nRuntimeFunctions;
+}
+
+bool ReadyToRunInfo::IsRuntimeFunctionActive(DWORD index) const
+{
+ LIMITED_METHOD_DAC_CONTRACT;
+ if (index >= m_nRuntimeFunctions)
+ return false;
+#ifdef FEATURE_AUXILIARY_R2R_MODULES
+ if (m_pAuxiliaryModuleMetadata != nullptr)
+ {
+ PTR_DWORD activationFlag = GetAuxiliaryModuleActivationFlag(index);
+ return activationFlag == nullptr || IsAuxiliaryModuleActivated(activationFlag);
+ }
+#endif // FEATURE_AUXILIARY_R2R_MODULES
+ return true;
+}
+
+int ReadyToRunInfo::FindRuntimeFunction(DWORD relativeVirtualIP) const
+{
+ LIMITED_METHOD_DAC_CONTRACT;
+
+ DWORD rangeCount = 1;
+#ifdef FEATURE_AUXILIARY_R2R_MODULES
+ PTR_READYTORUN_AUXILIARY_MODULE entries = nullptr;
+ if (m_pAuxiliaryModuleMetadata != nullptr)
+ {
+ rangeCount += m_pAuxiliaryModuleMetadata->AuxiliaryModuleCount;
+ entries = dac_cast(
+ dac_cast(m_pAuxiliaryModuleMetadata) + sizeof(READYTORUN_AUXILIARY_MODULE_METADATA));
+ }
+#endif // FEATURE_AUXILIARY_R2R_MODULES
+
+ for (DWORD range = 0; range < rangeCount; range++)
+ {
+ DWORD first = 0;
+ DWORD count = m_nRuntimeFunctions;
+#ifdef FEATURE_AUXILIARY_R2R_MODULES
+ if (entries != nullptr)
+ {
+ if (range == 0)
+ count = m_pAuxiliaryModuleMetadata->PrimaryModuleMethodCount;
+ else
+ {
+ if (!IsAuxiliaryModuleActivated(&entries[range - 1].ActivationStatus))
+ continue;
+ first = entries[range - 1].RelativeTableBase;
+ count = entries[range - 1].MethodCount;
+ }
+ }
+#endif // FEATURE_AUXILIARY_R2R_MODULES
+ if (count == 0 || relativeVirtualIP < RUNTIME_FUNCTION__BeginAddress(m_pRuntimeFunctions + first))
+ continue;
+
+ DWORD low = first;
+ DWORD high = first + count;
+ while (low + 1 < high)
+ {
+ DWORD middle = low + (high - low) / 2;
+ if (RUNTIME_FUNCTION__BeginAddress(m_pRuntimeFunctions + middle) <= relativeVirtualIP)
+ low = middle;
+ else
+ high = middle;
+ }
+
+ // The following entry can belong to an absent module. Read this function's own unwind data instead.
+ if (relativeVirtualIP < RUNTIME_FUNCTION__EndAddress(m_pRuntimeFunctions + low,
+ dac_cast(GetImage()->GetBase())))
+ return static_cast(low);
+ }
+ return -1;
+}
+#endif // TARGET_WASM
+
PTR_MethodDesc ReadyToRunInfo::GetMethodDescForEntryPoint(PCODE entryPoint)
{
CONTRACTL
@@ -720,6 +807,7 @@ bool ReadyToRunInfo::IsNativeImageSharedBy(PTR_Module pModule1, PTR_Module pModu
// tightly associated with the module itself.
ReadyToRunInfo* s_pGlobalR2RModules = NULL;
PTR_ReadyToRunInfo ReadyToRunInfo::GetUnrelatedR2RModules() { return s_pGlobalR2RModules; }
+
void ReadyToRunInfo::RegisterUnrelatedR2RModule()
{
LIMITED_METHOD_CONTRACT; // This function takes no locks, and can never fail
@@ -744,10 +832,17 @@ void ReadyToRunInfo::RegisterUnrelatedR2RModule()
oldGlobalValue = s_pGlobalR2RModules;
m_pNextR2RForUnrelatedCode = dac_cast(dac_cast(oldGlobalValue) | 0x1);
}
+
+#ifdef FEATURE_AUXILIARY_R2R_MODULES
+ // Generic methods that missed in every image searched so far may have code in this new image.
+ IncrementAuxiliaryEntryPointsEra();
+#endif // FEATURE_AUXILIARY_R2R_MODULES
}
}
else
{
+ // Publish only a tenured component, so failed module initialization cannot leave a dangling context.
+ InterlockedCompareExchangeT(&m_pCompositeInfo->m_pEntryPointModule, m_pModule, nullptr);
m_pCompositeInfo->RegisterUnrelatedR2RModule();
}
}
@@ -822,6 +917,7 @@ PTR_ReadyToRunInfo ReadyToRunInfo::ComputeAlternateGenericLocationForR2RCode(Met
ReadyToRunInfo::ReadyToRunInfo(Module * pModule, LoaderAllocator* pLoaderAllocator, READYTORUN_HEADER * pHeader, NativeImage *pNativeImage, ReadyToRunLoadedImage * pLayout, AllocMemTracker *pamTracker)
: m_pModule(pModule),
+ m_pEntryPointModule(pModule),
m_pHeader(pHeader),
m_pNativeImage(pModule != NULL ? pNativeImage: NULL), // m_pNativeImage is only set for composite image components, not the composite R2R info itself
m_readyToRunCodeDisabled(FALSE),
@@ -921,6 +1017,53 @@ ReadyToRunInfo::ReadyToRunInfo(Module * pModule, LoaderAllocator* pLoaderAllocat
m_nRuntimeFunctions = 0;
}
+ IMAGE_DATA_DIRECTORY* pAuxiliaryMetadataDir = m_pComposite->FindSection(ReadyToRunSectionType::AuxiliaryModuleMetadata);
+ if (pAuxiliaryMetadataDir != nullptr)
+ {
+#ifdef FEATURE_AUXILIARY_R2R_MODULES
+ if (pAuxiliaryMetadataDir->Size < sizeof(READYTORUN_AUXILIARY_MODULE_METADATA) ||
+ (pAuxiliaryMetadataDir->VirtualAddress & (alignof(DWORD) - 1)) != 0 ||
+ (pNativeImage == nullptr && !m_isComponentAssembly) ||
+ HasStrippedILBodies())
+ COMPlusThrowHR(COR_E_BADIMAGEFORMAT);
+
+ m_pAuxiliaryModuleMetadata = dac_cast(
+ m_pComposite->GetLayout()->GetDirectoryData(pAuxiliaryMetadataDir));
+ if (m_pAuxiliaryModuleMetadata->AuxiliaryModuleCount >
+ (pAuxiliaryMetadataDir->Size - sizeof(READYTORUN_AUXILIARY_MODULE_METADATA)) / sizeof(READYTORUN_AUXILIARY_MODULE) ||
+ m_pAuxiliaryModuleMetadata->PrimaryModuleMethodCount > m_nRuntimeFunctions ||
+ m_pAuxiliaryModuleMetadata->EntryPointsModuleActivationStatus > 1)
+ COMPlusThrowHR(COR_E_BADIMAGEFORMAT);
+
+ PTR_READYTORUN_AUXILIARY_MODULE entries = dac_cast(
+ dac_cast(m_pAuxiliaryModuleMetadata) + sizeof(READYTORUN_AUXILIARY_MODULE_METADATA));
+ DWORD nextFunction = m_pAuxiliaryModuleMetadata->PrimaryModuleMethodCount;
+ for (DWORD i = 0; i < m_pAuxiliaryModuleMetadata->AuxiliaryModuleCount; i++)
+ {
+ if (entries[i].RelativeTableBase != nextFunction ||
+ entries[i].MethodCount > m_nRuntimeFunctions - nextFunction ||
+ entries[i].ActivationStatus > 1)
+ COMPlusThrowHR(COR_E_BADIMAGEFORMAT);
+ nextFunction += entries[i].MethodCount;
+ }
+ if (nextFunction != m_nRuntimeFunctions)
+ COMPlusThrowHR(COR_E_BADIMAGEFORMAT);
+#else
+ COMPlusThrowHR(COR_E_BADIMAGEFORMAT);
+#endif // FEATURE_AUXILIARY_R2R_MODULES
+ }
+#ifdef FEATURE_AUXILIARY_R2R_MODULES
+ else if (!m_isComponentAssembly && pNativeImage != nullptr)
+ {
+ EnsureAuxiliaryEntryPointsModuleStatus();
+ m_pAuxiliaryEntryPointsModuleStatus = &s_simulatedAuxiliaryEntryPointsCallsRemaining;
+ m_pAuxiliaryModuleActivationFlag = (PTR_DWORD)pamTracker->Track(
+ pLoaderAllocator->GetLowFrequencyHeap()->AllocMem(S_SIZE_T(sizeof(DWORD))));
+ DWORD callsRemaining = CLRConfig::GetConfigValue(CLRConfig::EXTERNAL_R2RDelayedActivationCallCount);
+ *m_pAuxiliaryModuleActivationFlag = callsRemaining == UINT32_MAX ? callsRemaining : callsRemaining + 1;
+ }
+#endif // FEATURE_AUXILIARY_R2R_MODULES
+
#ifdef TARGET_WASM
// For WASM, the min function table index is stored as a u32 immediately after the
// sentinel entry (0xFFFFFFFF) at the end of the RUNTIME_FUNCTION table.
@@ -1184,6 +1327,34 @@ static bool SigMatchesMethodDesc(MethodDesc* pMD, SigPointer &sig, ModuleBase *
return true;
}
+static bool TryGetEntryPointOffset(
+ MethodDesc* pMD, Module* pModule, NativeArray& methodDefEntryPoints,
+ NativeHashtable& instanceEntryPoints, uint* pOffset)
+{
+ STANDARD_VM_CONTRACT;
+
+ if (!pMD->HasClassOrMethodInstantiation() &&
+ !pMD->IsAsyncVariantMethod() && !pMD->IsUnboxingStub())
+ return methodDefEntryPoints.TryGetAt(RidFromToken(pMD->GetMemberDef()) - 1, pOffset);
+
+ if (instanceEntryPoints.IsNull())
+ return false;
+
+ NativeHashtable::Enumerator lookup = instanceEntryPoints.Lookup(GetVersionResilientMethodHashCode(pMD));
+ NativeParser entryParser;
+ while (lookup.GetNext(entryParser))
+ {
+ PCCOR_SIGNATURE pBlob = (PCCOR_SIGNATURE)entryParser.GetBlob();
+ SigPointer sig(pBlob);
+ if (SigMatchesMethodDesc(pMD, sig, pModule))
+ {
+ *pOffset = entryParser.GetOffset() + static_cast(sig.GetPtr() - pBlob);
+ return true;
+ }
+ }
+ return false;
+}
+
bool ReadyToRunInfo::GetPgoInstrumentationData(MethodDesc * pMD, BYTE** pAllocatedMemory, ICorJitInfo::PgoInstrumentationSchema**ppSchema, UINT *pcSchema, BYTE** pInstrumentationData)
{
STANDARD_VM_CONTRACT;
@@ -1314,9 +1485,13 @@ PCODE ReadyToRunInfo::GetEntryPoint(MethodDesc * pMD, PrepareCodeConfig* pConfig
#endif
PCODE pEntryPoint = (PCODE)NULL;
+ Module* entryPointModule = m_pEntryPointModule;
#ifdef PROFILING_SUPPORTED
BOOL fShouldSearchCache = TRUE;
#endif // PROFILING_SUPPORTED
+#ifdef FEATURE_AUXILIARY_R2R_MODULES
+ bool mayDelayR2RCode = false;
+#endif // FEATURE_AUXILIARY_R2R_MODULES
mdToken token = pMD->GetMemberDef();
int rid = RidFromToken(token);
if (rid == 0)
@@ -1324,6 +1499,8 @@ PCODE ReadyToRunInfo::GetEntryPoint(MethodDesc * pMD, PrepareCodeConfig* pConfig
// If R2R code is disabled for this module, simply behave as if it is never found
if (ReadyToRunCodeDisabled())
goto done;
+ if (entryPointModule == nullptr)
+ goto done;
// Return-dropping async thunks are VM-synthesized. They share the same metadata
// token and signature shape as a regular async variant, so the R2R lookup below
@@ -1335,39 +1512,69 @@ PCODE ReadyToRunInfo::GetEntryPoint(MethodDesc * pMD, PrepareCodeConfig* pConfig
ETW::MethodLog::GetR2RGetEntryPointStart(pMD);
- uint offset;
- // Async variants and unboxing stubs are stored in the instance methods table.
- if (pMD->HasClassOrMethodInstantiation()
- || pMD->IsAsyncVariantMethod()
- || pMD->IsUnboxingStub())
+#ifdef FEATURE_AUXILIARY_R2R_MODULES
+ // There are conditions where we cannot delay the use of R2R code. In the future, the compiler
+ // should ensure these conditions for us.
+ mayDelayR2RCode = pConfig != NULL
+ && pConfig->MayDelayAuxiliaryModule()
+ && !HasStrippedILBodies()
+ && pMD->IsIL()
+ && !pMD->IsWrapperStub()
+ && (!pMD->GetModule()->IsReadyToRun() || !pMD->GetModule()->GetReadyToRunInfo()->HasStrippedILBodies())
+ && !pMD->HasUnmanagedCallersOnlyAttribute();
+
+ // The EntryPoints for this method may be in an auxiliary EntryPoints module that is not yet activated.
+ if (mayDelayR2RCode && m_pCompositeInfo->m_pAuxiliaryModuleMetadata == nullptr &&
+ !m_pCompositeInfo->IsAuxiliaryEntryPointsModuleActivated())
{
- if (m_instMethodEntryPoints.IsNull())
- goto done;
+ goto done;
+ }
+#endif // FEATURE_AUXILIARY_R2R_MODULES
- NativeHashtable::Enumerator lookup = m_instMethodEntryPoints.Lookup(GetVersionResilientMethodHashCode(pMD));
- NativeParser entryParser;
- offset = (uint)-1;
- while (lookup.GetNext(entryParser))
- {
- PCCOR_SIGNATURE pBlob = (PCCOR_SIGNATURE)entryParser.GetBlob();
- SigPointer sig(pBlob);
- if (SigMatchesMethodDesc(pMD, sig, m_pModule))
+ uint offset;
+ if (!TryGetEntryPointOffset(pMD, entryPointModule, m_methodDefEntryPoints, m_instMethodEntryPoints, &offset))
+ {
+#ifdef FEATURE_AUXILIARY_R2R_MODULES
+ if (m_pAuxiliaryModuleMetadata != nullptr && m_pCompositeInfo->IsAuxiliaryEntryPointsModuleActivated())
+ {
+ NativeArray auxiliaryMethodDefs;
+ NativeHashtable auxiliaryInstances;
+ IMAGE_DATA_DIRECTORY* methodDefs = m_component.FindSection(ReadyToRunSectionType::AuxiliaryMethodDefEntryPoints);
+ if (methodDefs != nullptr)
+ auxiliaryMethodDefs = NativeArray(&m_nativeReader, methodDefs->VirtualAddress);
+ IMAGE_DATA_DIRECTORY* instances = m_pComposite->FindSection(ReadyToRunSectionType::AuxiliaryInstanceMethodEntryPoints);
+ if (instances != nullptr)
{
- // Get the updated SigPointer location, so we can calculate the size of the blob,
- // in order to skip the blob and find the entry point data.
- offset = entryParser.GetOffset() + (uint)(sig.GetPtr() - pBlob);
- break;
+ NativeParser parser(&m_nativeReader, instances->VirtualAddress);
+ auxiliaryInstances = NativeHashtable(parser);
}
+ if (!TryGetEntryPointOffset(pMD, entryPointModule, auxiliaryMethodDefs, auxiliaryInstances, &offset))
+ goto done;
}
-
- if (offset == (uint)-1)
+ else
+#endif // FEATURE_AUXILIARY_R2R_MODULES
goto done;
}
- else
+
+ uint id;
+ offset = m_nativeReader.DecodeUnsigned(offset, &id);
+ uint runtimeFunctionIndex;
+ runtimeFunctionIndex = (id & 1) != 0 ? id >> 2 : id >> 1;
+ if (runtimeFunctionIndex >= m_nRuntimeFunctions)
+ COMPlusThrowHR(COR_E_BADIMAGEFORMAT);
+
+#ifdef FEATURE_AUXILIARY_R2R_MODULES
{
- if (!m_methodDefEntryPoints.TryGetAt(rid - 1, &offset))
+ PTR_DWORD activationFlag = m_pCompositeInfo->GetAuxiliaryModuleActivationFlag(runtimeFunctionIndex);
+ if (activationFlag != nullptr && !IsAuxiliaryModuleActivated(activationFlag) &&
+ (m_pAuxiliaryModuleMetadata != nullptr || mayDelayR2RCode))
+ {
+ if (mayDelayR2RCode)
+ pConfig->SetAuxiliaryModuleActivationFlag(activationFlag);
goto done;
+ }
}
+#endif // FEATURE_AUXILIARY_R2R_MODULES
#ifdef PROFILING_SUPPORTED
{
@@ -1388,9 +1595,6 @@ PCODE ReadyToRunInfo::GetEntryPoint(MethodDesc * pMD, PrepareCodeConfig* pConfig
}
#endif // PROFILING_SUPPORTED
- uint id;
- offset = m_nativeReader.DecodeUnsigned(offset, &id);
-
if (id & 1)
{
if (id & 2)
@@ -1405,7 +1609,7 @@ PCODE ReadyToRunInfo::GetEntryPoint(MethodDesc * pMD, PrepareCodeConfig* pConfig
BOOL mayUsePrecompiledPInvokeMethods = TRUE;
mayUsePrecompiledPInvokeMethods = !pConfig->IsForMulticoreJit();
- if (!m_pModule->FixupDelayList(dac_cast(GetImage()->GetBase()) + offset, mayUsePrecompiledPInvokeMethods))
+ if (!entryPointModule->FixupDelayList(dac_cast(GetImage()->GetBase()) + offset, mayUsePrecompiledPInvokeMethods))
{
pConfig->SetReadyToRunRejectedPrecompiledCode();
goto done;
@@ -1552,25 +1756,64 @@ BOOL ReadyToRunInfo::MethodIterator::Next()
if (m_pInfo->ReadyToRunCodeDisabled())
return FALSE;
- // Enumerate non-generic methods
- while (++m_methodDefIndex < (int)m_pInfo->m_methodDefEntryPoints.GetCount())
+ for (;;)
{
- uint offset;
- if (m_pInfo->m_methodDefEntryPoints.TryGetAt(m_methodDefIndex, &offset))
+ // Enumerate non-generic methods.
+ while (++m_methodDefIndex < (int)m_methodDefEntryPoints.GetCount())
{
+ uint offset;
+ if (!m_methodDefEntryPoints.TryGetAt(m_methodDefIndex, &offset))
+ continue;
+#ifdef TARGET_WASM
+ uint id;
+ m_pInfo->m_nativeReader.DecodeUnsigned(offset, &id);
+ if (!m_pInfo->IsRuntimeFunctionActive((id & 1) != 0 ? id >> 2 : id >> 1))
+ continue;
+#endif // TARGET_WASM
return TRUE;
}
- }
- // Enumerate generic instantiations
- m_genericParser = m_genericEnum.GetNext();
- if (!m_genericParser.IsNull())
- {
- ParseGenericMethodSignatureAndRid(&m_genericCurrentOffset, &m_genericCurrentRid);
- return TRUE;
- }
+ // Enumerate generic instantiations.
+ while (!(m_genericParser = m_genericEnum.GetNext()).IsNull())
+ {
+ ParseGenericMethodSignatureAndRid(&m_genericCurrentOffset, &m_genericCurrentRid);
+#ifdef TARGET_WASM
+ if (m_genericCurrentOffset != static_cast(-1))
+ {
+ uint id;
+ m_pInfo->m_nativeReader.DecodeUnsigned(m_genericCurrentOffset, &id);
+ if (!m_pInfo->IsRuntimeFunctionActive((id & 1) != 0 ? id >> 2 : id >> 1))
+ continue;
+ }
+#endif // TARGET_WASM
+ return TRUE;
+ }
- return FALSE;
+#ifdef FEATURE_AUXILIARY_R2R_MODULES
+ if (!m_enumeratingAuxiliary && m_pInfo->m_pAuxiliaryModuleMetadata != nullptr &&
+ m_pInfo->m_pCompositeInfo->IsAuxiliaryEntryPointsModuleActivated())
+ {
+ m_enumeratingAuxiliary = true;
+ m_methodDefIndex = -1;
+ m_methodDefEntryPoints = NativeArray();
+ m_instMethodEntryPoints = NativeHashtable();
+ IMAGE_DATA_DIRECTORY* methodDefs = m_pInfo->m_component.FindSection(ReadyToRunSectionType::AuxiliaryMethodDefEntryPoints);
+ if (methodDefs != nullptr)
+ m_methodDefEntryPoints = NativeArray(&m_pInfo->m_nativeReader, methodDefs->VirtualAddress);
+ IMAGE_DATA_DIRECTORY* instances = m_pInfo->m_pComposite->FindSection(ReadyToRunSectionType::AuxiliaryInstanceMethodEntryPoints);
+ if (instances != nullptr)
+ {
+ NativeParser parser(&m_pInfo->m_nativeReader, instances->VirtualAddress);
+ m_instMethodEntryPoints = NativeHashtable(parser);
+ }
+ NativeFormat::PTR_NativeHashtable hash = m_instMethodEntryPoints.IsNull()
+ ? nullptr : NativeFormat::PTR_NativeHashtable(&m_instMethodEntryPoints);
+ m_genericEnum = NativeHashtable::AllEntriesEnumerator(hash);
+ continue;
+ }
+#endif // FEATURE_AUXILIARY_R2R_MODULES
+ return FALSE;
+ }
}
MethodDesc * ReadyToRunInfo::MethodIterator::GetMethodDesc()
@@ -1578,15 +1821,15 @@ MethodDesc * ReadyToRunInfo::MethodIterator::GetMethodDesc()
STANDARD_VM_CONTRACT;
mdMethodDef methodToken = mdTokenNil;
- if (m_methodDefIndex < (int)m_pInfo->m_methodDefEntryPoints.GetCount())
+ if (m_methodDefIndex < (int)m_methodDefEntryPoints.GetCount())
{
methodToken = mdtMethodDef | (m_methodDefIndex + 1);
- return MemberLoader::GetMethodDescFromMethodDef(m_pInfo->m_pModule, methodToken, FALSE);
+ return MemberLoader::GetMethodDescFromMethodDef(m_pInfo->m_pEntryPointModule, methodToken, FALSE);
}
else
{
_ASSERTE(m_genericCurrentOffset > 0 && m_genericCurrentSig != NULL);
- return ZapSig::DecodeMethod(m_pInfo->m_pModule, m_pInfo->m_pModule, m_genericCurrentSig);
+ return ZapSig::DecodeMethod(m_pInfo->m_pEntryPointModule, m_pInfo->m_pEntryPointModule, m_genericCurrentSig);
}
}
@@ -1602,9 +1845,9 @@ MethodDesc * ReadyToRunInfo::MethodIterator::GetMethodDesc_NoRestore()
CONTRACTL_END;
uint offset;
- if (m_methodDefIndex < (int)m_pInfo->m_methodDefEntryPoints.GetCount())
+ if (m_methodDefIndex < (int)m_methodDefEntryPoints.GetCount())
{
- if (!m_pInfo->m_methodDefEntryPoints.TryGetAt(m_methodDefIndex, &offset))
+ if (!m_methodDefEntryPoints.TryGetAt(m_methodDefIndex, &offset))
{
return NULL;
}
diff --git a/src/coreclr/vm/readytoruninfo.h b/src/coreclr/vm/readytoruninfo.h
index 469ca418252297..b8677717fbfa8d 100644
--- a/src/coreclr/vm/readytoruninfo.h
+++ b/src/coreclr/vm/readytoruninfo.h
@@ -14,8 +14,11 @@
#include "inlinetracking.h"
#include "wellknownattributes.h"
#include "nativeimage.h"
+#include "auxiliaryentrypointsera.h"
typedef DPTR(struct READYTORUN_SECTION) PTR_READYTORUN_SECTION;
+typedef DPTR(struct READYTORUN_AUXILIARY_MODULE_METADATA) PTR_READYTORUN_AUXILIARY_MODULE_METADATA;
+typedef DPTR(struct READYTORUN_AUXILIARY_MODULE) PTR_READYTORUN_AUXILIARY_MODULE;
class NativeImage;
class PrepareCodeConfig;
@@ -144,6 +147,7 @@ class ReadyToRunInfo
friend class HotColdMappingLookupTable;
PTR_Module m_pModule;
+ PTR_Module m_pEntryPointModule = nullptr;
PTR_ModuleBase m_pNativeManifestModule;
PTR_READYTORUN_HEADER m_pHeader;
bool m_isComponentAssembly;
@@ -201,6 +205,17 @@ class ReadyToRunInfo
PTR_ReadyToRunInfo m_pNextR2RForUnrelatedCode;
TADDR m_pLoadedImageBase;
+#ifdef FEATURE_AUXILIARY_R2R_MODULES
+ PTR_READYTORUN_AUXILIARY_MODULE_METADATA m_pAuxiliaryModuleMetadata = nullptr;
+ PTR_DWORD m_pAuxiliaryModuleActivationFlag = 0;
+ PTR_DWORD m_pAuxiliaryEntryPointsModuleStatus = 0;
+
+ // Starts at 1 so a recorded era is never 0, which callers use to mean "not awaiting EntryPoints".
+ inline static DWORD s_auxiliaryEntryPointsEra = 1;
+ // Legacy call-count simulation for unpartitioned composite images; (DWORD)-1 means not configured.
+ inline static DWORD s_simulatedAuxiliaryEntryPointsCallsRemaining = (DWORD)-1;
+#endif // FEATURE_AUXILIARY_R2R_MODULES
+
public:
ReadyToRunInfo(Module * pModule, LoaderAllocator* pLoaderAllocator, READYTORUN_HEADER * pHeader, NativeImage * pNativeImage, ReadyToRunLoadedImage * pLayout, AllocMemTracker *pamTracker);
@@ -247,8 +262,127 @@ class ReadyToRunInfo
return (PCODE)(GetMinVirtualIP() + RUNTIME_FUNCTION__BeginAddress(&m_pRuntimeFunctions[r2rFunctionIndex]));
}
void RegisterVirtualIPRange(Module* pModule);
+ int FindRuntimeFunction(DWORD relativeVirtualIP) const;
+ bool IsRuntimeFunctionActive(DWORD index) const;
+ DWORD GetRuntimeFunctionRangeEnd(DWORD index) const;
#endif // TARGET_WASM
+#ifdef FEATURE_AUXILIARY_R2R_MODULES
+ PTR_READYTORUN_AUXILIARY_MODULE GetAuxiliaryModule(DWORD runtimeFunctionIndex) const
+ {
+ LIMITED_METHOD_CONTRACT;
+ if (m_pAuxiliaryModuleMetadata != nullptr)
+ {
+ PTR_READYTORUN_AUXILIARY_MODULE entries = dac_cast(
+ dac_cast(m_pAuxiliaryModuleMetadata) + sizeof(READYTORUN_AUXILIARY_MODULE_METADATA));
+ for (DWORD i = 0; i < m_pAuxiliaryModuleMetadata->AuxiliaryModuleCount; i++)
+ {
+ if (runtimeFunctionIndex >= entries[i].RelativeTableBase &&
+ runtimeFunctionIndex - entries[i].RelativeTableBase < entries[i].MethodCount)
+ return entries + i;
+ }
+ }
+ return nullptr;
+ }
+
+ PTR_DWORD GetAuxiliaryModuleActivationFlag(DWORD runtimeFunctionIndex) const
+ {
+ LIMITED_METHOD_CONTRACT;
+ if (m_pAuxiliaryModuleMetadata != nullptr)
+ {
+ PTR_READYTORUN_AUXILIARY_MODULE entry = GetAuxiliaryModule(runtimeFunctionIndex);
+ return entry == nullptr ? nullptr : &entry->ActivationStatus;
+ }
+ return m_pAuxiliaryModuleActivationFlag;
+ }
+
+ // The era advances each time an auxiliary EntryPoints module is activated in any R2R image, or a new R2R image
+ // with code for unrelated generics is registered.
+ static AuxiliaryEntryPointsEra GetCurrentAuxiliaryEntryPointsEra()
+ {
+ LIMITED_METHOD_CONTRACT;
+ return AuxiliaryEntryPointsEra(VolatileLoad(&s_auxiliaryEntryPointsEra));
+ }
+
+ // Returns true if auxiliary EntryPoints modules have been activated since the given era was observed.
+ static bool IsAuxiliaryEntryPointsEraStale(AuxiliaryEntryPointsEra era)
+ {
+ LIMITED_METHOD_CONTRACT;
+ _ASSERTE(era.IsAwaiting());
+ return era.m_value != VolatileLoad(&s_auxiliaryEntryPointsEra);
+ }
+
+ // Indicates that one or more new auxiliary EntryPoints modules have been activated.
+ static void IncrementAuxiliaryEntryPointsEra()
+ {
+ LIMITED_METHOD_CONTRACT;
+ VolatileStore(&s_auxiliaryEntryPointsEra, VolatileLoad(&s_auxiliaryEntryPointsEra) + 1);
+ }
+
+ PTR_DWORD GetAuxiliaryEntryPointsModuleStatus() const
+ {
+ LIMITED_METHOD_CONTRACT;
+ return m_pAuxiliaryEntryPointsModuleStatus;
+ }
+
+ static bool IsAuxiliaryEntryPointsModuleActivated(PTR_DWORD pModuleStatus)
+ {
+ LIMITED_METHOD_CONTRACT;
+ _ASSERTE(pModuleStatus != nullptr);
+ return VolatileLoad(pModuleStatus) == 0;
+ }
+
+ bool IsAuxiliaryEntryPointsModuleActivated() const
+ {
+ LIMITED_METHOD_CONTRACT;
+ if (m_pAuxiliaryModuleMetadata != nullptr)
+ return VolatileLoad(&m_pAuxiliaryModuleMetadata->EntryPointsModuleActivationStatus) == 1;
+ return m_pAuxiliaryEntryPointsModuleStatus == nullptr ||
+ IsAuxiliaryEntryPointsModuleActivated(m_pAuxiliaryEntryPointsModuleStatus);
+ }
+
+ // The encoding of the activation flag is owned by the R2R image; every reader goes through this.
+ static bool IsAuxiliaryModuleActivated(PTR_DWORD pActivationFlag)
+ {
+ LIMITED_METHOD_CONTRACT;
+ _ASSERTE(pActivationFlag != nullptr);
+ return VolatileLoad(pActivationFlag) == 1;
+ }
+
+ static void AdvanceSimulatedAuxiliaryModuleDownload(PTR_DWORD pActivationFlag)
+ {
+ LIMITED_METHOD_CONTRACT;
+ _ASSERTE(pActivationFlag != nullptr);
+ // Real activation flags are 0 or 1. Only the legacy countdown simulation uses values above 1.
+ if (VolatileLoad(pActivationFlag) > 1)
+ VolatileStore(pActivationFlag, VolatileLoad(pActivationFlag) - 1);
+ }
+
+ static void EnsureAuxiliaryEntryPointsModuleStatus()
+ {
+ LIMITED_METHOD_CONTRACT;
+ if (s_simulatedAuxiliaryEntryPointsCallsRemaining == (DWORD)-1)
+ {
+ s_simulatedAuxiliaryEntryPointsCallsRemaining =
+ CLRConfig::IsConfigOptionSpecified(W("R2RDelayedEntryPointsActivationCallCount"))
+ ? CLRConfig::GetConfigValue(CLRConfig::EXTERNAL_R2RDelayedEntryPointsActivationCallCount)
+ : CLRConfig::GetConfigValue(CLRConfig::EXTERNAL_R2RDelayedActivationCallCount);
+ }
+ }
+
+ static void AdvanceSimulatedAuxiliaryEntryPointsDownload()
+ {
+ LIMITED_METHOD_CONTRACT;
+ if (s_simulatedAuxiliaryEntryPointsCallsRemaining == (DWORD)-1)
+ return;
+ if (s_simulatedAuxiliaryEntryPointsCallsRemaining == 0)
+ return;
+
+ if (--s_simulatedAuxiliaryEntryPointsCallsRemaining == 0)
+ IncrementAuxiliaryEntryPointsEra();
+ }
+#endif // FEATURE_AUXILIARY_R2R_MODULES
+
void RegisterResumptionStub(PCODE stubEntryPoint);
PCODE GetEntryPoint(MethodDesc * pMD, PrepareCodeConfig* pConfig, BOOL fFixups);
@@ -353,6 +487,11 @@ class ReadyToRunInfo
ReadyToRunInfo * m_pInfo;
int m_methodDefIndex;
+ NativeFormat::NativeArray m_methodDefEntryPoints;
+ NativeFormat::NativeHashtable m_instMethodEntryPoints;
+#ifdef FEATURE_AUXILIARY_R2R_MODULES
+ bool m_enumeratingAuxiliary = false;
+#endif // FEATURE_AUXILIARY_R2R_MODULES
NativeFormat::NativeHashtable::AllEntriesEnumerator m_genericEnum;
NativeFormat::NativeParser m_genericParser;
uint m_genericCurrentOffset;
@@ -365,6 +504,8 @@ class ReadyToRunInfo
MethodIterator(ReadyToRunInfo * pInfo) :
m_pInfo(pInfo),
m_methodDefIndex(-1),
+ m_methodDefEntryPoints(pInfo->m_methodDefEntryPoints),
+ m_instMethodEntryPoints(pInfo->m_instMethodEntryPoints),
m_genericEnum(),
m_genericParser(),
m_genericCurrentOffset(-1),
@@ -372,9 +513,9 @@ class ReadyToRunInfo
m_genericCurrentSig(NULL)
{
NativeFormat::PTR_NativeHashtable pHash = NULL;
- if (!pInfo->m_instMethodEntryPoints.IsNull())
+ if (!m_instMethodEntryPoints.IsNull())
{
- pHash = NativeFormat::PTR_NativeHashtable(&pInfo->m_instMethodEntryPoints);
+ pHash = NativeFormat::PTR_NativeHashtable(&m_instMethodEntryPoints);
}
m_genericEnum = NativeFormat::NativeHashtable::AllEntriesEnumerator(pHash);
diff --git a/src/coreclr/vm/wasm/calldescrworkerwasm.cpp b/src/coreclr/vm/wasm/calldescrworkerwasm.cpp
index ce68dee60941ac..84fc52198dc35f 100644
--- a/src/coreclr/vm/wasm/calldescrworkerwasm.cpp
+++ b/src/coreclr/vm/wasm/calldescrworkerwasm.cpp
@@ -43,7 +43,15 @@ extern "C" void STDCALL CallDescrWorkerInternal(CallDescrData* pCallDescrData)
MethodDesc* pMethod = PortableEntryPoint::GetMethodDesc(pCallDescrData->pTarget);
InterpByteCodeStart* targetIp = pMethod->GetInterpreterCode();
- if (targetIp == NULL && pMethod->ShouldCallPrestub())
+ bool activateDelayedR2RCode = false;
+#ifdef FEATURE_AUXILIARY_R2R_MODULES
+ if (targetIp != NULL)
+ {
+ activateDelayedR2RCode = targetIp->Method->auxiliaryMethodInfo.ShouldTryActivateMethod();
+ }
+#endif // FEATURE_AUXILIARY_R2R_MODULES
+
+ if ((targetIp == NULL && pMethod->ShouldCallPrestub()) || activateDelayedR2RCode)
{
// DoPrestub may trigger a GC. Report the arguments through a PrestubMethodFrame, as the
// prestub does for calls from managed code. DispatchCallSimple provides no TransitionBlock,
diff --git a/src/coreclr/vm/wasm/helpers.cpp b/src/coreclr/vm/wasm/helpers.cpp
index 3906e64ae5b743..1f2dcf0137ad86 100644
--- a/src/coreclr/vm/wasm/helpers.cpp
+++ b/src/coreclr/vm/wasm/helpers.cpp
@@ -14,6 +14,13 @@
#define WASM_STRINGIFY_HELPER(value) #value
#define WASM_STRINGIFY(value) WASM_STRINGIFY_HELPER(value)
+extern "C" void dotnet_wasm_notify_auxiliary_entrypoints_activated()
+{
+ LIMITED_METHOD_CONTRACT;
+#ifdef FEATURE_AUXILIARY_R2R_MODULES
+ ReadyToRunInfo::IncrementAuxiliaryEntryPointsEra();
+#endif // FEATURE_AUXILIARY_R2R_MODULES
+}
extern "C" void STDCALL CallCountingStubCode()
{
diff --git a/src/installer/pkg/sfx/Microsoft.NETCore.App/Directory.Build.props b/src/installer/pkg/sfx/Microsoft.NETCore.App/Directory.Build.props
index c12c12479224ae..89215fd909fa53 100644
--- a/src/installer/pkg/sfx/Microsoft.NETCore.App/Directory.Build.props
+++ b/src/installer/pkg/sfx/Microsoft.NETCore.App/Directory.Build.props
@@ -232,6 +232,8 @@
+
+
diff --git a/src/mono/browser/build/README.md b/src/mono/browser/build/README.md
index c4aa79a7092970..639fc204f7f8ba 100644
--- a/src/mono/browser/build/README.md
+++ b/src/mono/browser/build/README.md
@@ -139,6 +139,46 @@ If encountering build performance issues, you can use the rollup `--perf` option
The .cpuprofile file generated by node can be opened in the Performance tab of Chrome or Edge's devtools.
+## Partitioned CoreCLR ReadyToRun images
+
+Single-threaded CoreCLR browser apps can opt in to partitioned composite ReadyToRun output:
+
+```console
+dotnet publish -p:PublishReadyToRun=true -p:PublishReadyToRunComposite=true -p:WasmEnableAuxiliaryR2RModules=true
+```
+
+`WasmEnableAuxiliaryR2RModules` defaults to `false`. It passes `--wasm-auxiliary-modules` to
+crossgen2 and requires retained IL; `--strip-il` and threaded builds are not supported.
+In-tree browser samples also forward this property through `BuildSampleInTree`.
+
+To use MIBC profile data for the hot/cold split, add the existing ReadyToRun profile item to
+the app project:
+
+```xml
+
+
+
+```
+
+The publish pipeline passes each item to crossgen2 as `--mibc` (or its `-m` alias).
+Multiple items combine the methods from their profiles. With an explicit profile, methods
+present in the profile stay in the primary image and the remaining eligible methods go to
+auxiliary bodies. Without an explicit profile, the compiler uses its default hot/cold heuristic.
+Auxiliary publishing still compiles the full eligible method set; supplying a profile does not
+implicitly enable crossgen2's `--partial` compilation mode.
+Use a profile collected from the intended application scenario when choosing production hot methods.
+
+The compiler emits an `.auxiliary.json` manifest beside the primary image. Its explicit
+EntryPoints and body filenames are collected as sibling `_framework` assets, fingerprinted
+and compressed by the normal static-web-assets pipeline, and recorded in the boot config's
+`auxiliaryAssembly` resources. Sidecars are not managed assemblies and are never converted
+to Webcil or added to the trusted platform assembly list.
+
+The browser starts independent background loads after the primary image is registered.
+Neither EntryPoints nor body activation waits for the other, and cold downloads are not
+part of the startup barrier. A failed sidecar is reported through the host logger while
+its methods retain their interpreter fallback.
+
## Blazor
You can test local build of runtime in blazor by overriding location where blazor resolves runtime pack from
diff --git a/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.CoreCLR.targets b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.CoreCLR.targets
index d5d6a22615f67a..3b541989174336 100644
--- a/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.CoreCLR.targets
+++ b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.CoreCLR.targets
@@ -35,8 +35,38 @@ Copyright (c) .NET Foundation. All rights reserved.
$(PublishReadyToRunCrossgen2ExtraArgs);--opt-cross-module:*
$(PublishReadyToRunCrossgen2ExtraArgs);--instruction-set:relaxed-simd
+ $(PublishReadyToRunCrossgen2ExtraArgs);--wasm-auxiliary-modules
+
+
+
+ <_WasmAuxiliaryPrimaryImage Include="@(_ReadyToRunCompileList->WithMetadataValue('CreateCompositeImage', 'true')->'%(OutputR2RImage)')" />
+
+
+
+
+
+
+
+ <_WasmReadyToRunAuxiliaryStamp>$([MSBuild]::NormalizePath('$(IntermediateOutputPath)', 'R2R', 'auxiliary-modules.stamp'))
+ <_PublishReadyToRunCrossgen2ExtraArgs Condition="'$(WasmEnableAuxiliaryR2RModules)' == 'true' and !$([System.String]::Copy('$(PublishReadyToRunCrossgen2ExtraArgs)').Contains('--wasm-auxiliary-modules'))">$(_PublishReadyToRunCrossgen2ExtraArgs);--wasm-auxiliary-modules
+
+
+
+
+ <_ReadyToRunCompilerInputs Include="$(_WasmReadyToRunAuxiliaryStamp)" />
+
+
+
+
<_WasmWebcilStampProperty Include="PublishTrimmed" />
<_WasmWebcilStampProperty Include="PublishReadyToRunComposite" />
+ <_WasmWebcilStampProperty Include="WasmEnableAuxiliaryR2RModules" />
- $(PublishReadyToRunCrossgen2ExtraArgs);--partial
+
+ $(PublishReadyToRunCrossgen2ExtraArgs);--partial
@@ -317,10 +349,16 @@ Copyright (c) .NET Foundation. All rights reserved.
Condition="'$(PublishReadyToRun)' == 'true'"
DependsOnTargets="_WasmCoreClrSelectR2RDirectories"
AfterTargets="_CreateR2RImages">
+
+
+
+
<_WasmEmittedPerAppR2R Include="@(_ReadyToRunCompileList->'%(OutputR2RImage)')" />
+ <_WasmEmittedPerAppR2R Include="@(_WasmEmittedAuxiliaryR2R)" />
<_WasmEmittedPerAppR2R Include="@(_ReadyToRunCompositeBuildInput->'$(_WasmPublishR2RDir)%(FileName).wasm');@(_ReadyToRunCompositeUnrootedBuildInput->'$(_WasmPublishR2RDir)%(FileName).wasm')"
Condition="'$(PublishReadyToRunComposite)' == 'true'" />
diff --git a/src/mono/sample/wasm/Directory.Build.props b/src/mono/sample/wasm/Directory.Build.props
index b63362260ba5e2..ec36531fc4ef6f 100644
--- a/src/mono/sample/wasm/Directory.Build.props
+++ b/src/mono/sample/wasm/Directory.Build.props
@@ -54,6 +54,7 @@
$(Nested_RuntimeFlavor)
$(Nested_PublishReadyToRun)
false
+ $(Nested_WasmEnableAuxiliaryR2RModules)
diff --git a/src/mono/sample/wasm/Directory.Build.targets b/src/mono/sample/wasm/Directory.Build.targets
index bd4f2de1aa2de6..2dc4d0c188b406 100644
--- a/src/mono/sample/wasm/Directory.Build.targets
+++ b/src/mono/sample/wasm/Directory.Build.targets
@@ -45,6 +45,7 @@
+
diff --git a/src/mono/wasm/Wasm.Build.Tests/CoreCLRWasmNativeDefaultsTests.cs b/src/mono/wasm/Wasm.Build.Tests/CoreCLRWasmNativeDefaultsTests.cs
index 8836129ed57410..30d439b9ffa460 100644
--- a/src/mono/wasm/Wasm.Build.Tests/CoreCLRWasmNativeDefaultsTests.cs
+++ b/src/mono/wasm/Wasm.Build.Tests/CoreCLRWasmNativeDefaultsTests.cs
@@ -186,14 +186,17 @@ public void PublishReadyToRunDirectoryMatchesSdkOutputCasing()
}
// Verify that explicit PGO items remain available to the SDK ReadyToRun pipeline and enable
- // --partial only when ReadyToRun is on. Stops before crossgen2; the compiler arguments only let
- // restore resolve a compiler (in-build when BASE_DIR is set, SDK pack otherwise).
+ // --partial only when ReadyToRun is on without auxiliary modules. Stops before crossgen2;
+ // the compiler arguments only let restore resolve a compiler (in-build when BASE_DIR is set,
+ // SDK pack otherwise).
[Theory]
- [InlineData(/*profileSet*/ true, /*r2r*/ true, /*expectPartial*/ true)]
- [InlineData(/*profileSet*/ false, /*r2r*/ true, /*expectPartial*/ false)]
- [InlineData(/*profileSet*/ true, /*r2r*/ false, /*expectPartial*/ false)]
- [InlineData(/*profileSet*/ false, /*r2r*/ false, /*expectPartial*/ false)]
- public void PublishReadyToRunPgoFilesControlsPartialCompilation(bool profileSet, bool r2r, bool expectPartial)
+ [InlineData(/*profileSet*/ true, /*r2r*/ true, /*auxiliary*/ false, /*expectPartial*/ true)]
+ [InlineData(/*profileSet*/ false, /*r2r*/ true, /*auxiliary*/ false, /*expectPartial*/ false)]
+ [InlineData(/*profileSet*/ true, /*r2r*/ false, /*auxiliary*/ false, /*expectPartial*/ false)]
+ [InlineData(/*profileSet*/ false, /*r2r*/ false, /*auxiliary*/ false, /*expectPartial*/ false)]
+ [InlineData(/*profileSet*/ true, /*r2r*/ true, /*auxiliary*/ true, /*expectPartial*/ false)]
+ [InlineData(/*profileSet*/ false, /*r2r*/ true, /*auxiliary*/ true, /*expectPartial*/ false)]
+ public void PublishReadyToRunPgoFilesControlsPartialCompilation(bool profileSet, bool r2r, bool auxiliary, bool expectPartial)
{
Configuration config = Configuration.Release;
string profilePath = Path.Combine(BuildEnvironment.TestDataPath, "DotNet_Blazor_Wasm.mibc");
@@ -211,8 +214,12 @@ public void PublishReadyToRunPgoFilesControlsPartialCompilation(bool profileSet,
config,
aot: false,
TestAsset.WasmBasicTestApp,
- $"coreclr_r2r_profile_{profileSet}_{r2r}",
- extraProperties: $"{r2r}",
+ $"coreclr_r2r_profile_{profileSet}_{r2r}_{auxiliary}",
+ extraProperties: $"""
+ {r2r}
+ {auxiliary}
+ {auxiliary}
+ """,
extraItems: profileItem,
insertAtEnd: printValueTarget);
diff --git a/src/mono/wasm/Wasm.Build.Tests/ReadyToRunTests.cs b/src/mono/wasm/Wasm.Build.Tests/ReadyToRunTests.cs
index c6a3ddb6b5e508..bcc2bbcf1e5103 100644
--- a/src/mono/wasm/Wasm.Build.Tests/ReadyToRunTests.cs
+++ b/src/mono/wasm/Wasm.Build.Tests/ReadyToRunTests.cs
@@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.NET.Sdk.WebAssembly;
@@ -68,6 +69,13 @@ public Task PublishRunAllPages(Configuration config, bool trimmed)
public Task PublishRunAllPagesComposite(Configuration config, bool trimmed, bool nativeRelink)
=> PublishRunAllPagesCore(config, trimmed, nativeRelink, composite: true);
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ [TestCategory("no-workload")]
+ public Task PublishRunAllPagesAuxiliary(bool nativeRelink)
+ => PublishRunAllPagesCore(Configuration.Release, trimmed: true, nativeRelink, composite: true, auxiliaryModules: true);
+
// CoreCLR relinks dotnet.native.wasm for Blazor when WasmBuildNative=true; the relink is driven by the
// IsBrowserWasmProject triggers in BrowserWasmApp.CoreCLR.targets. AssertBundle(isNativeBuild: true)
// proves the served dotnet.native.wasm was relinked rather than the runtime-pack prebuilt.
@@ -78,11 +86,11 @@ public Task PublishRunAllPagesComposite(Configuration config, bool trimmed, bool
public Task PublishRunAllPagesNativeRelink(Configuration config, bool trimmed)
=> PublishRunAllPagesCore(config, trimmed, nativeRelink: true, composite: false);
- private async Task PublishRunAllPagesCore(Configuration config, bool trimmed, bool nativeRelink, bool composite)
+ private async Task PublishRunAllPagesCore(Configuration config, bool trimmed, bool nativeRelink, bool composite, bool auxiliaryModules = false)
{
// Publish runs per-app crossgen2 for the whole closure, trimmed or not: even the untrimmed CoreLib
// is a per-app image, not the runtime pack's. nativeRelink also relinks dotnet.native.wasm.
- string label = $"r2r_pub_{(trimmed ? "trim" : "notrim")}{(nativeRelink ? "_native" : "")}{(composite ? "_composite" : "")}";
+ string label = $"r2r_pub_{(trimmed ? "trim" : "notrim")}{(nativeRelink ? "_native" : "")}{(composite ? "_composite" : "")}{(auxiliaryModules ? "_auxiliary" : "")}";
string extraItems = composite
? """"""
: string.Empty;
@@ -92,6 +100,8 @@ private async Task PublishRunAllPagesCore(Configuration config, bool trimmed, bo
if (composite)
LogR2RSuffixLibraryMarker();
string extraArgs = GetR2RBuildArgs(config, composite);
+ if (auxiliaryModules)
+ extraArgs += " -p:WasmEnableAuxiliaryR2RModules=true";
if (nativeRelink)
{
// CoreCLR relinks dotnet.native.wasm via the in-tree targets + EMSDK_PATH, not the browser
@@ -113,14 +123,26 @@ private async Task PublishRunAllPagesCore(Configuration config, bool trimmed, bo
AssertNoManagedAssembliesOutsideFramework(frameworkDir);
AssertTrimmedClosureIsFullyStaged(config, frameworkDir);
AssertPerAppCrossgenRan(config, expected: true);
+ if (auxiliaryModules)
+ AssertAuxiliaryModulesStaged(config, frameworkDir);
if (trimmed && !nativeRelink)
{
+ string? removedSidecar = auxiliaryModules
+ ? Directory.EnumerateFiles(GetObjSubDir(config, "R2R"), "*.entrypoints.wasm").Single()
+ : null;
+ if (removedSidecar is not null)
+ File.Delete(removedSidecar);
BlazorPublish(info, config, new PublishOptions(UseCache: false, ExtraMSBuildArgs: extraArgs));
+ if (removedSidecar is not null)
+ {
+ Assert.True(File.Exists(removedSidecar), $"Missing auxiliary output '{removedSidecar}' was not recompiled.");
+ AssertAuxiliaryModulesStaged(config, frameworkDir);
+ }
if (composite)
AssertCompositeReadyToRun(frameworkDir);
AssertNoDuplicateAssemblies(frameworkDir);
- if (composite)
+ if (composite && !auxiliaryModules)
AssertSwitchingCompositeModeRecompiles(info, config, extraArgs, frameworkDir);
}
@@ -478,6 +500,25 @@ private void AssertCompositeReadyToRun(string frameworkDir)
$"Expected a component stub for System.Private.CoreLib in '{frameworkDir}'.");
}
+ private void AssertAuxiliaryModulesStaged(Configuration config, string frameworkDir)
+ {
+ string manifest = Assert.Single(Directory.EnumerateFiles(GetObjSubDir(config, "R2R"), "*.auxiliary.json"));
+ using JsonDocument document = JsonDocument.Parse(File.ReadAllText(manifest));
+ string[] filenames = document.RootElement.GetProperty("modules").EnumerateArray()
+ .Select(module => module.GetString()!)
+ .Prepend(document.RootElement.GetProperty("entrypoints").GetString()!)
+ .OrderBy(name => name, System.StringComparer.Ordinal)
+ .ToArray();
+ AssetsData assets = (AssetsData)_provider.GetBootJson(_provider.GetBootConfigPath(frameworkDir)).resources;
+ Assert.NotNull(assets.auxiliaryAssembly);
+ Assert.Equal(filenames, assets.auxiliaryAssembly.Select(asset => asset.virtualPath).OrderBy(name => name, System.StringComparer.Ordinal));
+ foreach (GeneralAsset sidecar in assets.auxiliaryAssembly)
+ {
+ Assert.True(File.Exists(Path.Combine(frameworkDir, sidecar.name)), $"Missing published sidecar '{sidecar.name}'.");
+ Assert.DoesNotContain(assets.coreAssembly.Concat(assets.assembly), asset => asset.virtualPath == sidecar.virtualPath);
+ }
+ }
+
private const string SuffixLibraryLoadedMessage = "Loaded R2rSuffixLibrary.r2r: 42";
// Make the app call into the R2rSuffixLibrary test asset (referenced for composite runs) so the run proves
diff --git a/src/mono/wasm/Wasm.Build.Tests/WebcilInWasmSizesTests.cs b/src/mono/wasm/Wasm.Build.Tests/WebcilInWasmSizesTests.cs
index baba622b2e778b..44fec8623d5ec1 100644
--- a/src/mono/wasm/Wasm.Build.Tests/WebcilInWasmSizesTests.cs
+++ b/src/mono/wasm/Wasm.Build.Tests/WebcilInWasmSizesTests.cs
@@ -7,6 +7,7 @@
using System.Linq;
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;
+using System.Text.Json;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using Microsoft.NET.Sdk.WebAssembly;
@@ -361,6 +362,102 @@ public void ConvertDllsToWebcil_FallsBackToIL_WhenPrebuiltMvidMismatches(bool wr
Assert.Equal(0, tableSize);
}
+ [Theory]
+ [InlineData(null)]
+ [InlineData("missing")]
+ [InlineData("path")]
+ [InlineData("duplicate")]
+ [InlineData("invalid-json")]
+ public void AuxiliaryModules_AreCollectedFromManifest(string? failure)
+ {
+ using var directory = new TempDirectory();
+ string primary = Path.Combine(directory.Path, "unusual.primary.dll");
+ string entrypoints = "arbitrary-mapping.wasm";
+ string body = failure is "path" ? "../escape.wasm" : failure is "duplicate" ? entrypoints : "arbitrary-body.wasm";
+ File.WriteAllText(Path.ChangeExtension(primary, ".auxiliary.json"),
+ failure is "invalid-json" ? "invalid" : JsonSerializer.Serialize(new { entrypoints, modules = new[] { body } }));
+ File.WriteAllBytes(Path.Combine(directory.Path, entrypoints), [0]);
+ if (failure is null)
+ File.WriteAllBytes(Path.Combine(directory.Path, body), [0]);
+
+ var engine = new TestBuildEngine { AllowErrors = failure is not null };
+ var task = new CollectWasmAuxiliaryR2RModules { BuildEngine = engine, PrimaryImages = [new TaskItem(primary)] };
+ Assert.Equal(failure is null, task.Execute());
+ if (failure is not null)
+ {
+ Assert.Single(engine.Errors);
+ return;
+ }
+ Assert.Equal(new[] { entrypoints, body }, task.Modules.Select(item => Path.GetFileName(item.ItemSpec)));
+ Assert.All(task.Modules, item => Assert.Equal("_framework/" + Path.GetFileName(item.ItemSpec), item.GetMetadata("RelativePath")));
+ Assert.Equal(3, task.FileWrites.Length);
+ }
+
+ [Fact]
+ public void AuxiliaryModules_AreNotParsedAsWebcilOrAddedToManagedAssemblies()
+ {
+ using var directory = new TempDirectory();
+ string path = Path.Combine(directory.Path, "arbitrary-body.wasm");
+ File.WriteAllBytes(path, [0]);
+ var resource = new TaskItem(path);
+ resource.SetMetadata("OriginalItemSpec", path);
+ resource.SetMetadata("AssetTraitName", "WasmResource");
+ resource.SetMetadata("AssetTraitValue", "readyToRunAuxiliary");
+ resource.SetMetadata("Integrity", "sidecar-hash");
+ var sizes = new AttachWebcilSizes { BuildEngine = new TestBuildEngine(), Candidates = [resource] };
+ Assert.True(sizes.Execute());
+ Assert.Equal(string.Empty, Assert.Single(sizes.CandidatesWithSizes).GetMetadata("PayloadSize"));
+
+ var endpoint = new TaskItem("_framework/arbitrary-body.fingerprint.wasm");
+ endpoint.SetMetadata("AssetFile", path);
+ string output = Path.Combine(directory.Path, "boot.json");
+ var boot = new GenerateWasmBootJson
+ {
+ BuildEngine = new TestBuildEngine(),
+ AssemblyPath = typeof(WebcilInWasmSizesTests).Assembly.Location,
+ Resources = sizes.CandidatesWithSizes,
+ Endpoints = [endpoint],
+ FingerprintAssets = true,
+ InvariantGlobalization = "true",
+ TargetFrameworkVersion = "v11.0",
+ UseMonoRuntime = "false",
+ OutputPath = output,
+ };
+ Assert.True(boot.Execute());
+ using JsonDocument document = JsonDocument.Parse(File.ReadAllText(output));
+ JsonElement resources = document.RootElement.GetProperty("resources");
+ JsonElement auxiliary = Assert.Single(resources.GetProperty("auxiliaryAssembly").EnumerateArray());
+ Assert.Equal("arbitrary-body.wasm", auxiliary.GetProperty("virtualPath").GetString());
+ Assert.Equal("arbitrary-body.fingerprint.wasm", auxiliary.GetProperty("name").GetString());
+ Assert.Equal("sha256-sidecar-hash", auxiliary.GetProperty("hash").GetString());
+ Assert.False(auxiliary.TryGetProperty("payloadSize", out _));
+ Assert.Empty(resources.GetProperty("assembly").EnumerateArray());
+ Assert.Empty(resources.GetProperty("coreAssembly").EnumerateArray());
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void AuxiliaryModules_MissingOutputsInvalidateIncrementalCompilation(bool missingManifest)
+ {
+ using var directory = new TempDirectory();
+ string primary = Path.Combine(directory.Path, "app.wasm");
+ string manifest = Path.ChangeExtension(primary, ".auxiliary.json");
+ if (!missingManifest)
+ {
+ File.WriteAllText(manifest, """{"entrypoints":"mapping.wasm","modules":["body.wasm"]}""");
+ File.WriteAllBytes(Path.Combine(directory.Path, "mapping.wasm"), [0]);
+ }
+ var task = new CollectWasmAuxiliaryR2RModules
+ {
+ BuildEngine = new TestBuildEngine(),
+ PrimaryImages = [new TaskItem(primary)],
+ AllowMissingFiles = true,
+ };
+ Assert.True(task.Execute());
+ Assert.Equal(missingManifest ? manifest : Path.Combine(directory.Path, "body.wasm"), Assert.Single(task.MissingOutputs).ItemSpec);
+ }
+
private const byte SectionCustom = 0x00;
private const byte SectionType = 0x01;
private const byte SectionFunction = 0x03;
@@ -642,6 +739,8 @@ private sealed class TempDirectory : IDisposable
private sealed class TestBuildEngine : IBuildEngine
{
+ public bool AllowErrors { get; set; }
+ public List Errors { get; } = [];
public bool ContinueOnError => false;
public int LineNumberOfTaskNode => 0;
public int ColumnNumberOfTaskNode => 0;
@@ -654,7 +753,13 @@ public void LogCustomEvent(CustomBuildEventArgs e)
{
}
- public void LogErrorEvent(BuildErrorEventArgs e) => Assert.Fail(e.Message ?? "Build error");
+ public void LogErrorEvent(BuildErrorEventArgs e)
+ {
+ if (AllowErrors)
+ Errors.Add(e.Message ?? "Build error");
+ else
+ Assert.Fail(e.Message ?? "Build error");
+ }
public void LogMessageEvent(BuildMessageEventArgs e)
{
diff --git a/src/native/libs/Common/JavaScript/CMakeLists.txt b/src/native/libs/Common/JavaScript/CMakeLists.txt
index eafcfbb4ee38af..fb0a75331e02b0 100644
--- a/src/native/libs/Common/JavaScript/CMakeLists.txt
+++ b/src/native/libs/Common/JavaScript/CMakeLists.txt
@@ -23,12 +23,15 @@ set(ROLLUP_TS_SOURCES
"${CLR_SRC_NATIVE_DIR}/libs/Common/JavaScript/cross-module/index.ts"
"${CLR_SRC_NATIVE_DIR}/libs/Common/JavaScript/ems-ambient/index.ts"
"${CLR_SRC_NATIVE_DIR}/libs/Common/JavaScript/host/assets.ts"
+ "${CLR_SRC_NATIVE_DIR}/libs/Common/JavaScript/host/auxiliary-modules.ts"
"${CLR_SRC_NATIVE_DIR}/libs/Common/JavaScript/host/cross-module.ts"
"${CLR_SRC_NATIVE_DIR}/libs/Common/JavaScript/host/host.ts"
"${CLR_SRC_NATIVE_DIR}/libs/Common/JavaScript/host/index.ts"
"${CLR_SRC_NATIVE_DIR}/libs/Common/JavaScript/host/per-module.ts"
"${CLR_SRC_NATIVE_DIR}/libs/Common/JavaScript/host/types.ts"
"${CLR_SRC_NATIVE_DIR}/libs/Common/JavaScript/loader/assets.ts"
+ "${CLR_SRC_NATIVE_DIR}/libs/Common/JavaScript/loader/auxiliary-module-worker-client.ts"
+ "${CLR_SRC_NATIVE_DIR}/libs/Common/JavaScript/loader/auxiliary-module-worker.ts"
"${CLR_SRC_NATIVE_DIR}/libs/Common/JavaScript/loader/bootstrap.ts"
"${CLR_SRC_NATIVE_DIR}/libs/Common/JavaScript/loader/config.ts"
"${CLR_SRC_NATIVE_DIR}/libs/Common/JavaScript/loader/cross-module.ts"
@@ -122,6 +125,8 @@ set(ROLLUP_TS_SOURCES
set(ROLLUP_OUTPUTS
"${STATIC_LIB_DESTINATION}/dotnet.js"
"${STATIC_LIB_DESTINATION}/dotnet.js.map"
+ "${STATIC_LIB_DESTINATION}/dotnet.auxiliary.worker.js"
+ "${STATIC_LIB_DESTINATION}/dotnet.auxiliary.worker.js.map"
"${STATIC_LIB_DESTINATION}/dotnet.d.ts"
"${STATIC_LIB_DESTINATION}/libSystem.Native.Browser.js"
"${STATIC_LIB_DESTINATION}/libSystem.Native.Browser.js.map"
diff --git a/src/native/libs/Common/JavaScript/host/assets.ts b/src/native/libs/Common/JavaScript/host/assets.ts
index d3e12d1756aa08..a43d8b9297cad8 100644
--- a/src/native/libs/Common/JavaScript/host/assets.ts
+++ b/src/native/libs/Common/JavaScript/host/assets.ts
@@ -5,9 +5,14 @@ import type { CharPtr, VfsAsset, VoidPtr, VoidPtrPtr } from "../types";
import { _ems_ } from "../ems-ambient";
import { browserVirtualAppBase, sizeOfPtr } from "../per-module";
+import { loadAuxiliaryModules, readAuxiliaryModuleMetadata } from "./auxiliary-modules";
+import type { AuxiliaryModuleSource } from "./auxiliary-modules";
const hasInstantiateStreaming = typeof WebAssembly !== "undefined" && typeof WebAssembly.instantiateStreaming === "function";
const loadedAssemblies: Map = new Map();
+type StartAuxiliaryModules = () => void;
+const pendingAuxiliaryModules = new Map();
+const auxiliaryModuleLoads: Promise[] = [];
// Self-installing wrapper version; keep in sync with WebcilConstants.WASM_WRAPPER_VERSION_SELF_INSTALLING.
const webcilWrapperVersion = 2;
@@ -49,7 +54,7 @@ export function registerDllBytes(bytes: Uint8Array, virtualPath: string, shortNa
}
}
-export async function instantiateWebcilModule(webcilPromise: Promise, memory: WebAssembly.Memory, virtualPath: string, tableSize?: number, payloadSize?: number): Promise {
+export async function instantiateWebcilModule(webcilPromise: Promise, memory: WebAssembly.Memory, virtualPath: string, tableSize?: number, payloadSize?: number, prepareAuxiliaryModule?: (name: string) => Promise): Promise {
// Boot-config sizes let us reserve the payload and table ranges before streaming instantiation.
// Assets without a tableSize are IL-only images.
if (typeof payloadSize !== "number" || payloadSize === 0) {
@@ -71,7 +76,20 @@ export async function instantiateWebcilModule(webcilPromise: Promise,
const data = await res.arrayBuffer();
instance = (await WebAssembly.instantiate(data, imports)).instance;
}
+ const modules = readAuxiliaryModuleMetadata(instance, memory, payloadPtr, payloadSize, tableEntries);
+ if (modules.length !== 0 && !prepareAuxiliaryModule) {
+ throw new Error(`Webcil asset '${virtualPath}' has auxiliary modules but no auxiliary resource loader.`);
+ }
finishWebcilInstance(instance, payloadPtr, payloadSize, tableEntries, virtualPath);
+ if (modules.length !== 0) {
+ pendingAuxiliaryModules.set(payloadPtr, () => {
+ auxiliaryModuleLoads.push(loadAuxiliaryModules(modules, memory, imports, prepareAuxiliaryModule!, () => {
+ const notify = _ems_.wasmExports.dotnet_wasm_notify_auxiliary_entrypoints_activated;
+ if (typeof notify !== "function") throw new Error("Runtime does not support auxiliary R2R modules.");
+ notify();
+ }, error => _ems_.dotnetLogger.error(error.message, error)));
+ });
+ }
} catch (err) {
// Instantiation failed after the payload buffer was allocated; free it to avoid leaking
// unmanaged memory. (A grown R2R table cannot be shrunk back, but a failed R2R instantiate is fatal.)
@@ -82,6 +100,11 @@ export async function instantiateWebcilModule(webcilPromise: Promise,
}
}
+export async function waitForAuxiliaryModules(): Promise {
+ await Promise.resolve();
+ await Promise.all(auxiliaryModuleLoads);
+}
+
async function checkWebcilResponse(webcilPromise: Promise, virtualPath: string): Promise {
const res = await webcilPromise;
if (!res || res.ok === false) {
@@ -183,6 +206,12 @@ export function BrowserHost_ExternalAssemblyProbe(pathPtr: CharPtr, outDataStart
// int64_t target
_ems_.HEAPU32[outSize as any >>> 2] = assembly.length;
_ems_.HEAPU32[((outSize as any) + 4) >>> 2] = 0;
+ const startAuxiliaryModules = pendingAuxiliaryModules.get(assembly.ptr);
+ if (startAuxiliaryModules) {
+ pendingAuxiliaryModules.delete(assembly.ptr);
+ // The synchronous probe must return before native image registration and any activation.
+ queueMicrotask(startAuxiliaryModules);
+ }
return true;
}
_ems_.dotnetLogger.debug(`Assembly not found: '${path}'`);
diff --git a/src/native/libs/Common/JavaScript/host/auxiliary-modules.test.mjs b/src/native/libs/Common/JavaScript/host/auxiliary-modules.test.mjs
new file mode 100644
index 00000000000000..59d099f1e2d51a
--- /dev/null
+++ b/src/native/libs/Common/JavaScript/host/auxiliary-modules.test.mjs
@@ -0,0 +1,559 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+import assert from "node:assert/strict";
+import { readFileSync } from "node:fs";
+import { createRequire } from "node:module";
+import { test } from "node:test";
+import vm from "node:vm";
+
+const require = createRequire(import.meta.url);
+const ts = require(process.env.TYPESCRIPT_PATH || "typescript");
+const source = readFileSync(new URL("./auxiliary-modules.ts", import.meta.url), "utf8");
+const javascript = ts.transpileModule(source, { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ES2022 } }).outputText;
+const { readAuxiliaryModuleMetadata, loadAuxiliaryModules } = await import(`data:text/javascript;base64,${Buffer.from(javascript).toString("base64")}`);
+const workerClientSource = readFileSync(new URL("../loader/auxiliary-module-worker-client.ts", import.meta.url), "utf8");
+const workerClientJavascript = ts.transpileModule(workerClientSource, { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ES2022 } }).outputText;
+const { fetchAndCompileAuxiliaryModuleInWorker, compileAuxiliaryResponseInWorker } = await import(`data:text/javascript;base64,${Buffer.from(workerClientJavascript).toString("base64")}`);
+const workerSource = readFileSync(new URL("../loader/auxiliary-module-worker.ts", import.meta.url), "utf8");
+const workerJavascript = ts.transpileModule(workerSource, { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS } }).outputText;
+const corerunSource = readFileSync(new URL("../../../../../coreclr/hosts/corerun/wasm/libCorerun.js", import.meta.url), "utf8");
+
+test("browser: auxiliary boot resources survive configuration merges", () => {
+ const configSource = readFileSync(new URL("../loader/config.ts", import.meta.url), "utf8");
+ const javascript = ts.transpileModule(configSource, { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS } }).outputText;
+ const context = { exports: {}, require: () => ({ browserVirtualAppBase: "/" }) };
+ vm.runInNewContext(javascript, context);
+ const first = { name: "app.entrypoints.hash.wasm", virtualPath: "app.entrypoints.wasm" };
+ const second = { name: "app.1.hash.wasm", virtualPath: "app.1.wasm" };
+ const worker = { name: "dotnet.auxiliary.worker.hash.js" };
+ context.exports.mergeLoaderConfig({ resources: { auxiliaryAssembly: [first], jsModuleAuxiliaryWorker: [worker] } });
+ context.exports.mergeLoaderConfig({ environmentVariables: { CUSTOM: "1" } });
+ context.exports.mergeLoaderConfig({ resources: { auxiliaryAssembly: [second] } });
+ assert.deepEqual([...context.exports.loaderConfig.resources.auxiliaryAssembly], [first, second]);
+ assert.deepEqual([...context.exports.loaderConfig.resources.jsModuleAuxiliaryWorker], [worker]);
+});
+
+function uleb(value) {
+ const bytes = [];
+ do {
+ const byte = value & 127;
+ value >>>= 7;
+ bytes.push(byte | (value ? 128 : 0));
+ } while (value);
+ return bytes;
+}
+function string(value) {
+ const bytes = [...new TextEncoder().encode(value)];
+ return [...uleb(bytes.length), ...bytes];
+}
+function section(id, bytes) {
+ return [id, ...uleb(bytes.length), ...bytes];
+}
+function auxiliaryWasm(statusOffset, installTable) {
+ const address = offset => [0x23, 0, 0x41, ...uleb(offset), 0x6a];
+ const instructions = [
+ ...address(statusOffset), 0x28, 2, 0, 0x04, 0x40, 0x0f, 0x0b,
+ ...address(180 + statusOffset), 0x41, 0, 0x41, 4, 0xfc, 8, 0, 0,
+ ];
+ if (installTable) instructions.push(0x23, 1, 0x41, 4, 0x6a, 0x41, 0, 0x41, 1, 0xfc, 12, 0, 0);
+ instructions.push(...address(statusOffset), 0x41, 1, 0x36, 2, 0, 0x0b);
+ const activate = [0, ...instructions];
+ const body = [0, 0x41, 42, 0x0b];
+ return new Uint8Array([
+ 0, 97, 115, 109, 1, 0, 0, 0,
+ ...section(1, [2, 0x60, 0, 0, 0x60, 0, 1, 0x7f]),
+ ...section(2, [
+ 4,
+ ...string("webcil"), ...string("memory"), 2, 0, 1,
+ ...string("webcil"), ...string("__memory_base"), 3, 0x7f, 0,
+ ...string("webcil"), ...string("__table_base"), 3, 0x7f, 0,
+ ...string("webcil"), ...string("__indirect_function_table"), 1, 0x70, 0, 0,
+ ]),
+ ...section(3, [2, 0, 1]),
+ ...section(7, [1, ...string("ActivateModule"), 0, 0]),
+ ...section(9, [1, 1, 0, 1, 1]),
+ ...section(12, [1]),
+ ...section(10, [2, ...uleb(activate.length), ...activate, ...uleb(body.length), ...body]),
+ ...section(11, [1, 1, 4, 101, 102, 103, 104]),
+ ]);
+}
+function fixture() {
+ const memory = new WebAssembly.Memory({ initial: 1 });
+ const table = new WebAssembly.Table({ element: "anyfunc", initial: 12 });
+ const payloadPtr = 256;
+ const view = new DataView(memory.buffer);
+ const values = [0, 4, 1, 64, 96, 4, 1, 0];
+ values.forEach((value, i) => view.setUint32(payloadPtr + i * 4, value, true));
+ new Uint8Array(memory.buffer).set(new TextEncoder().encode("app.entrypoints.wasm\0"), payloadPtr + 64);
+ new Uint8Array(memory.buffer).set(new TextEncoder().encode("app.1.wasm\0"), payloadPtr + 96);
+ const primary = { exports: { getAuxiliaryModuleMetadata: () => payloadPtr } };
+ const imports = { webcil: {
+ memory,
+ __memory_base: new WebAssembly.Global({ value: "i32", mutable: false }, payloadPtr),
+ __table_base: new WebAssembly.Global({ value: "i32", mutable: false }, 0),
+ __indirect_function_table: table,
+ } };
+ let notifications = 0;
+ const errors = [];
+ const modules = readAuxiliaryModuleMetadata(primary, memory, payloadPtr, 512, 5);
+ const bytes = { "app.entrypoints.wasm": auxiliaryWasm(0, false), "app.1.wasm": auxiliaryWasm(28, true) };
+ const notify = () => {
+ assert.equal(view.getUint32(payloadPtr, true), 1);
+ notifications++;
+ };
+ return { memory, table, payloadPtr, view, primary, imports, errors, modules, bytes, notify, notifications: () => notifications };
+}
+async function until(predicate) {
+ for (let i = 0; i < 100 && !predicate(); i++) await new Promise(resolve => setImmediate(resolve));
+ assert.ok(predicate());
+}
+
+for (const first of ["app.entrypoints.wasm", "app.1.wasm"]) {
+ test(`browser: independent activation, ${first} first`, async () => {
+ const f = fixture();
+ // A primary-owned import can already be resolved before either sidecar arrives.
+ const primaryImport = f.payloadPtr + 192;
+ f.view.setUint32(primaryImport, 0x12345678, true);
+ const pending = new Map();
+ const completion = loadAuxiliaryModules(f.modules, f.memory, f.imports,
+ name => new Promise(resolve => pending.set(name, resolve)), f.notify, error => f.errors.push(error));
+ assert.equal(f.view.getUint32(f.payloadPtr, true), 0);
+ assert.equal(f.view.getUint32(f.payloadPtr + 28, true), 0);
+ assert.equal(f.table.get(4), null);
+ pending.get(first)(new Response(f.bytes[first]));
+ await until(() => f.view.getUint32(f.payloadPtr + (first === "app.1.wasm" ? 28 : 0), true) === 1);
+ assert.equal(f.view.getUint32(f.payloadPtr + (first === "app.1.wasm" ? 0 : 28), true), 0);
+ const resolvedImport = f.payloadPtr + (first === "app.1.wasm" ? 208 : 180);
+ assert.deepEqual([...new Uint8Array(f.memory.buffer, resolvedImport, 4)], [101, 102, 103, 104]);
+ f.view.setUint32(resolvedImport, 0x11223344, true);
+ const second = first === "app.1.wasm" ? "app.entrypoints.wasm" : "app.1.wasm";
+ pending.get(second)(new Response(f.bytes[second]));
+ await completion;
+ assert.equal(f.notifications(), 1);
+ assert.equal(f.table.get(4)(), 42);
+ assert.equal(f.table.length, 12);
+ assert.equal(f.view.getUint32(primaryImport, true), 0x12345678);
+ assert.equal(f.view.getUint32(resolvedImport, true), 0x11223344);
+ if (first === "app.entrypoints.wasm")
+ assert.deepEqual([...new Uint8Array(f.memory.buffer, f.payloadPtr + 208, 4)], [101, 102, 103, 104]);
+ await loadAuxiliaryModules(f.modules, f.memory, f.imports, () => assert.fail("duplicate download"), f.notify, error => f.errors.push(error));
+ assert.equal(f.notifications(), 1);
+ assert.deepEqual(f.errors, []);
+ const duplicate = await WebAssembly.instantiate(f.bytes["app.1.wasm"], f.imports);
+ new Uint8Array(f.memory.buffer)[f.payloadPtr + 208] = 77;
+ duplicate.instance.exports.ActivateModule();
+ assert.equal(new Uint8Array(f.memory.buffer)[f.payloadPtr + 208], 77);
+ });
+}
+
+for (const first of ["app.entrypoints.wasm", "app.1.wasm"]) {
+ test(`browser: prepared-module overload activates in its continuation, ${first} first`, async () => {
+ const f = fixture();
+ const pending = new Map();
+ const completion = loadAuxiliaryModules(f.modules, f.memory, f.imports,
+ name => new Promise(resolve => pending.set(name, resolve)), f.notify, error => f.errors.push(error));
+ const originalInstantiate = WebAssembly.instantiate;
+ const observedStatuses = [];
+ WebAssembly.instantiate = (module, imports) => {
+ assert(module instanceof WebAssembly.Module);
+ const instance = new WebAssembly.Instance(module, imports);
+ queueMicrotask(() => queueMicrotask(() => observedStatuses.push([
+ f.view.getUint32(f.payloadPtr, true), f.view.getUint32(f.payloadPtr + 28, true),
+ ])));
+ return Promise.resolve(instance);
+ };
+ try {
+ const second = first === "app.1.wasm" ? "app.entrypoints.wasm" : "app.1.wasm";
+ const prepared = name => new WebAssembly.Module(f.bytes[name]);
+ pending.get(first)(prepared(first));
+ await until(() => observedStatuses.length === 1 || f.errors.length !== 0);
+ assert.deepEqual(f.errors, []);
+ assert.deepEqual(observedStatuses, [first === "app.1.wasm" ? [0, 1] : [1, 0]]);
+ pending.get(second)(prepared(second));
+ await completion;
+ assert.equal(f.notifications(), 1);
+ assert.equal(f.table.get(4)(), 42);
+ assert.deepEqual(observedStatuses.at(-1), [1, 1]);
+ } finally {
+ WebAssembly.instantiate = originalInstantiate;
+ }
+ });
+}
+
+test("browser: worker preparation preserves requests, custom bodies, failures and cleanup", async () => {
+ const originalWorker = globalThis.Worker;
+ const originalDocument = globalThis.document;
+ const originalLocation = globalThis.location;
+ const workers = [];
+ const requests = [];
+ const options = { cache: "no-cache", credentials: "include", integrity: "sha256-example" };
+ const workerUrl = "https://example.test/app/dotnet.auxiliary.worker.hash.js";
+ const bytes = auxiliaryWasm(0, false);
+ globalThis.document = { baseURI: "https://example.test/app/" };
+ globalThis.Worker = class {
+ constructor(url, options) {
+ assert.equal(url, workerUrl);
+ assert.deepEqual(options, { type: "module", name: "dotnet-auxiliary-compiler" });
+ workers.push(this);
+ this.ready = Promise.resolve().then(() => {
+ const context = {
+ exports: {}, WebAssembly, Response, Error,
+ fetch: async (url, options) => {
+ requests.push({ url, options });
+ if (url.endsWith("missing.wasm")) return new Response(null, { status: 404 });
+ return new Response(url.endsWith("invalid.wasm") ? new Uint8Array([0]) : bytes,
+ { headers: { "Content-Type": "application/wasm" } });
+ },
+ postMessage: result => {
+ if (!this.terminated) this.onmessage({ data: result });
+ },
+ };
+ vm.runInNewContext(workerJavascript, context);
+ return context;
+ });
+ }
+ postMessage(request, transfer) {
+ this.transfer = transfer;
+ void this.ready.then(context => {
+ if (!this.terminated) return context.onmessage({ data: request });
+ });
+ }
+ terminate() {
+ this.terminated = true;
+ }
+ };
+ try {
+ const [prepared, missing] = await Promise.all([
+ fetchAndCompileAuxiliaryModuleInWorker(workerUrl, "app.wasm", options),
+ fetchAndCompileAuxiliaryModuleInWorker(workerUrl, "missing.wasm", {}),
+ ]);
+ assert(prepared.module instanceof WebAssembly.Module);
+ assert.equal(prepared.ok, true);
+ assert.equal(missing.status, 404);
+ assert.equal(workers.length, 1);
+ assert.equal(workers[0].terminated, true);
+ assert.deepEqual(requests[0], { url: "https://example.test/app/app.wasm", options });
+ await assert.rejects(fetchAndCompileAuxiliaryModuleInWorker(workerUrl, "invalid.wasm", {}), WebAssembly.CompileError);
+ assert.equal(workers[1].terminated, true);
+ const custom = await compileAuxiliaryResponseInWorker(workerUrl, new Response(bytes));
+ assert(custom.module instanceof WebAssembly.Module);
+ assert.equal(requests.length, 3);
+ assert.equal(workers[2].transfer.length, 1);
+ assert.equal(workers[2].terminated, true);
+ await assert.rejects(compileAuxiliaryResponseInWorker(workerUrl, new Response(null)), /no body/);
+ const crashed = fetchAndCompileAuxiliaryModuleInWorker(workerUrl, "app.wasm", {});
+ const deserializeFailed = fetchAndCompileAuxiliaryModuleInWorker(workerUrl, "app.wasm", {});
+ workers[3].onerror({ message: "worker crashed", preventDefault() {} });
+ await assert.rejects(crashed, /worker crashed/);
+ await assert.rejects(deserializeFailed, /worker crashed/);
+ assert.equal(workers[3].terminated, true);
+ delete globalThis.document;
+ globalThis.location = { href: "https://example.test/app/worker.js" };
+ const workerHosted = await fetchAndCompileAuxiliaryModuleInWorker(workerUrl, "app.wasm", {});
+ assert(workerHosted.module instanceof WebAssembly.Module);
+ assert.equal(requests.at(-1).url, "https://example.test/app/app.wasm");
+ assert.equal(workers[4].terminated, true);
+ } finally {
+ if (originalWorker === undefined) delete globalThis.Worker;
+ else globalThis.Worker = originalWorker;
+ if (originalDocument === undefined) delete globalThis.document;
+ else globalThis.document = originalDocument;
+ if (originalLocation === undefined) delete globalThis.location;
+ else globalThis.location = originalLocation;
+ }
+});
+
+for (const provider of ["network", "custom", "preloaded", "pending", "redirect", "default", "disabled", "retry", "compile-error"]) {
+ test(`browser: auxiliary preparation preserves resource policy, ${provider}`, async () => {
+ const bytes = auxiliaryWasm(0, false);
+ const compiled = new WebAssembly.Module(bytes);
+ const asset = { name: "app.1.hash.wasm", virtualPath: "app.1.wasm", hash: "sha256-example", cache: "reload" };
+ if (provider === "preloaded") asset.buffer = bytes.buffer;
+ if (provider === "pending") asset.pendingDownload = { response: Promise.resolve(new Response(bytes)) };
+ const config = {
+ maxParallelDownloads: 16,
+ enableDownloadRetry: true,
+ environmentVariables: { DOTNET_WasmAuxiliaryWorkerCompilation: provider === "disabled" ? "0" : "1" },
+ resources: {
+ auxiliaryAssembly: [asset],
+ jsModuleAuxiliaryWorker: [{ name: "dotnet.auxiliary.worker.hash.js" }],
+ },
+ };
+ const fetched = [];
+ const prepared = [];
+ const compiledResponses = [];
+ const createPromiseCompletionSource = () => {
+ let resolve, reject;
+ const promise = new Promise((a, b) => {
+ resolve = a; reject = b;
+ });
+ return { promise, resolve, reject };
+ };
+ const dependencies = {
+ "./cross-module": {
+ dotnetAssert: { check: (condition, message) => assert(condition, message) },
+ dotnetLogger: { debug() {} },
+ },
+ "./per-module": { ENVIRONMENT_IS_WEB: true },
+ "./promise-completion-source": { createPromiseCompletionSource, delay: () => Promise.resolve() },
+ "./bootstrap": {
+ locateFile: name => `https://example.test/app/${name}`,
+ makeURLAbsoluteWithApplicationBase: name => new URL(name, "https://example.test/").href,
+ },
+ "./polyfills": {
+ fetchLike: async (url, options) => {
+ fetched.push({ url, options });
+ return new Response(bytes);
+ },
+ responseLike: (_url, body, options) => new Response(body, options),
+ },
+ "./config": { loaderConfig: config },
+ "./callbacks": {},
+ "./auxiliary-module-worker-client": {
+ fetchAndCompileAuxiliaryModuleInWorker: async (workerUrl, url, options) => {
+ prepared.push({ workerUrl, url, options });
+ if (provider === "compile-error") throw new WebAssembly.CompileError("Invalid auxiliary body");
+ if (provider === "retry" && prepared.length === 1) return new Response(null, { status: 500 });
+ return { ok: true, status: 200, statusText: "OK", module: compiled };
+ },
+ compileAuxiliaryResponseInWorker: async (workerUrl, response) => {
+ assert.equal(workerUrl, "https://example.test/app/dotnet.auxiliary.worker.hash.js");
+ compiledResponses.push(await response.arrayBuffer());
+ return { ok: true, status: 200, statusText: "OK", module: compiled };
+ },
+ },
+ };
+ const loaderSource = readFileSync(new URL("../loader/assets.ts", import.meta.url), "utf8")
+ + "\nexport { prepareAuxiliaryModule, loadResource };\nexport const activeDownloads = () => currentParallelDownloads;\n";
+ const loaderJavascript = ts.transpileModule(loaderSource,
+ { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS } }).outputText;
+ const context = {
+ exports: {}, WebAssembly, Error,
+ require: name => {
+ assert(name in dependencies, `Unexpected loader dependency '${name}'`);
+ return dependencies[name];
+ },
+ };
+ vm.runInNewContext(loaderJavascript, context);
+ context.exports.resolveAllDownloadsQueued();
+ context.exports.setLoadBootResourceCallback((type, name) => {
+ if (name.endsWith(".js")) return undefined;
+ if (provider === "custom") return Promise.resolve(new Response(bytes));
+ if (provider === "redirect") return "cdn/app.wasm";
+ return null;
+ });
+ if (provider === "compile-error") {
+ await assert.rejects(context.exports.prepareAuxiliaryModule(asset.virtualPath), WebAssembly.CompileError);
+ assert.equal(prepared.length, 1);
+ } else if (provider === "default") {
+ const response = await context.exports.loadResource({ ...asset, behavior: "webcil", resolvedUrl: "https://example.test/app/app.1.hash.wasm" });
+ assert(response instanceof Response);
+ assert.equal(prepared.length, 0);
+ assert.equal(fetched.length, 1);
+ } else if (provider === "disabled") {
+ const result = await context.exports.prepareAuxiliaryModule(asset.virtualPath);
+ assert(result instanceof Response);
+ assert.equal(prepared.length, 0);
+ assert.equal(fetched.length, 1);
+ } else {
+ const result = await context.exports.prepareAuxiliaryModule(asset.virtualPath);
+ assert.equal(result, compiled);
+ assert.equal(fetched.length, 0);
+ if (["custom", "preloaded", "pending"].includes(provider)) {
+ assert.equal(prepared.length, 0);
+ assert.equal(compiledResponses.length, 1);
+ assert.deepEqual(new Uint8Array(compiledResponses[0]), bytes);
+ } else {
+ assert.equal(prepared.length, provider === "retry" ? 2 : 1);
+ const request = prepared.at(-1);
+ assert.equal(request.workerUrl, "https://example.test/app/dotnet.auxiliary.worker.hash.js");
+ assert.equal(request.url, provider === "redirect" ? "https://example.test/cdn/app.wasm" : "https://example.test/app/app.1.hash.wasm");
+ assert.deepEqual({ ...request.options }, { cache: "reload", integrity: asset.hash });
+ }
+ }
+ if (provider !== "default") {
+ assert.equal(context.exports.activeDownloads(), 0);
+ assert.equal(asset.inprogress, false);
+ }
+ });
+}
+
+test("browser: all-hot image activates an empty auxiliary body range", async () => {
+ const f = fixture();
+ f.view.setUint32(f.payloadPtr + 4, 5, true);
+ f.view.setUint32(f.payloadPtr + 20, 5, true);
+ f.view.setUint32(f.payloadPtr + 24, 0, true);
+ f.bytes["app.1.wasm"] = auxiliaryWasm(28, false);
+ const modules = readAuxiliaryModuleMetadata(f.primary, f.memory, f.payloadPtr, 512, 5);
+ await loadAuxiliaryModules(modules, f.memory, f.imports,
+ async name => new Response(f.bytes[name]), f.notify, error => f.errors.push(error));
+ assert.equal(f.view.getUint32(f.payloadPtr, true), 1);
+ assert.equal(f.view.getUint32(f.payloadPtr + 28, true), 1);
+ assert.equal(f.notifications(), 1);
+ assert.equal(f.table.get(4), null);
+ assert.equal(f.table.length, 12);
+ assert.deepEqual(f.errors, []);
+});
+
+for (const failure of ["missing", "invalid", "no-export"]) {
+ test(`browser: ${failure} sidecar is reported and awaitable`, async () => {
+ const f = fixture();
+ const completion = loadAuxiliaryModules(f.modules, f.memory, f.imports, async name => {
+ if (name === "app.entrypoints.wasm") return new Response(f.bytes[name]);
+ if (failure === "missing") return new Response(null, { status: 404 });
+ return new Response(failure === "invalid" ? new Uint8Array([0]) : new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0]));
+ }, f.notify, error => f.errors.push(error));
+ await assert.rejects(completion, /app\.1\.wasm/);
+ assert.equal(f.errors.length, 1);
+ assert.equal(f.notifications(), 1);
+ assert.equal(f.view.getUint32(f.payloadPtr + 28, true), 0);
+ });
+}
+
+test("browser: rejects corrupt metadata before loading sidecars", () => {
+ const f = fixture();
+ assert.deepEqual(readAuxiliaryModuleMetadata({ exports: {} }, f.memory, 256, 512, 5), []);
+ assert.throws(() => readAuxiliaryModuleMetadata({ exports: { getAuxiliaryModuleMetadata: 1 } }, f.memory, 256, 512, 5), /export/);
+ f.view.setUint32(f.payloadPtr + 8, 0, true);
+ f.view.setUint32(f.payloadPtr + 4, 6, true);
+ assert.throws(() => readAuxiliaryModuleMetadata(f.primary, f.memory, 256, 512, 5), /table range/);
+ f.view.setUint32(f.payloadPtr + 4, 4, true);
+ f.view.setUint32(f.payloadPtr + 8, 1000, true);
+ assert.throws(() => readAuxiliaryModuleMetadata(f.primary, f.memory, 256, 512, 5), /truncated/);
+ f.view.setUint32(f.payloadPtr + 8, 1, true);
+ new Uint8Array(f.memory.buffer).set(new TextEncoder().encode("../bad.wasm\0"), f.payloadPtr + 96);
+ assert.throws(() => readAuxiliaryModuleMetadata(f.primary, f.memory, 256, 512, 5), /filename/);
+});
+
+for (const stage of ["eager", "missing", "delay-entrypoints", "delay-body", "all-hot"]) {
+ test(`corerun: activation is deferred until probe returns; ${stage}`, async () => {
+ const f = fixture();
+ if (stage === "all-hot") {
+ f.view.setUint32(f.payloadPtr + 4, 5, true);
+ f.view.setUint32(f.payloadPtr + 20, 5, true);
+ f.view.setUint32(f.payloadPtr + 24, 0, true);
+ f.bytes["app.1.wasm"] = auxiliaryWasm(28, false);
+ }
+ let probed = false;
+ let library;
+ const timers = [];
+ const env = stage === "delay-entrypoints"
+ ? { DOTNET_WasmAuxiliaryEntryPointsDelayMilliseconds: "100" }
+ : stage === "delay-body" ? { DOTNET_WasmAuxiliaryModuleDelayMilliseconds: "100" } : {};
+ const context = {
+ LibraryManager: { library: {} }, autoAddDeps: () => {}, addToLibrary: value => library = value,
+ WebAssembly, TextDecoder, Error, DataView, Uint8Array, Set, Promise, ENV: env,
+ setTimeout: callback => timers.push(callback),
+ wasmMemory: f.memory,
+ wasmExports: { dotnet_wasm_notify_auxiliary_entrypoints_activated: f.notify },
+ FS: { readFile: path => {
+ assert.ok(probed);
+ const name = path.substring(path.lastIndexOf("/") + 1);
+ if (stage === "missing" && name === "app.1.wasm") throw new Error("Missing file");
+ return f.bytes[name];
+ } },
+ err: message => f.errors.push(message),
+ };
+ vm.runInNewContext(corerunSource, context);
+ context.CORERUN = library.$CORERUN;
+ context.CORERUN.queueAuxiliaryModules(f.primary, f.imports, f.payloadPtr, 512, 5, "/app/app.wasm");
+ assert.equal(f.view.getUint32(f.payloadPtr, true), 0);
+ probed = true;
+ if (stage === "missing") {
+ await assert.rejects(context.CORERUN.waitForAuxiliaryModules(), /Missing file/);
+ assert.equal(f.errors.length, 1);
+ } else {
+ if (stage.startsWith("delay")) {
+ const pendingOffset = stage === "delay-body" ? 28 : 0;
+ await until(() => f.view.getUint32(f.payloadPtr + (28 - pendingOffset), true) === 1);
+ assert.equal(f.view.getUint32(f.payloadPtr + pendingOffset, true), 0);
+ assert.equal(timers.length, 1);
+ timers[0]();
+ }
+
+ await context.CORERUN.waitForAuxiliaryModules();
+ assert.equal(f.view.getUint32(f.payloadPtr + 28, true), 1);
+ if (stage === "all-hot")
+ assert.equal(f.table.get(4), null);
+ else
+ assert.equal(f.table.get(4)(), 42);
+ }
+ assert.equal(f.notifications(), 1);
+ });
+}
+
+for (const [first, rawFs] of [["ENTRYPOINTS", false], ["MODULES", false], ["ENTRYPOINTS", true], ["MODULES", true]]) {
+ test(`corerun: ${rawFs ? "raw FS" : "stdout"} gate releases ${first} first on a later event-loop turn`, async () => {
+ const f = fixture();
+ const timers = [];
+ const printed = [];
+ const reads = [];
+ let finishFirstCompile;
+ const firstCompile = new Promise(resolve => finishFirstCompile = resolve);
+ const firstFilename = first === "MODULES" ? "app.1.wasm" : "app.entrypoints.wasm";
+ let library;
+ const context = {
+ LibraryManager: { library: {} }, autoAddDeps: () => {}, addToLibrary: value => library = value,
+ WebAssembly: { instantiate: async (bytes, imports) => {
+ if (bytes === f.bytes[firstFilename]) await firstCompile;
+ return WebAssembly.instantiate(bytes, imports);
+ } },
+ TextDecoder, Error, DataView, Uint8Array, Set, Promise,
+ ENV: { DOTNET_WasmAuxiliaryManualActivation: "1" },
+ Module: { preRun: [] },
+ setTimeout: callback => timers.push(callback),
+ out: text => printed.push(text),
+ wasmMemory: f.memory,
+ wasmExports: { dotnet_wasm_notify_auxiliary_entrypoints_activated: f.notify },
+ FS: {
+ write: (stream, buffer, offset, length) => {
+ printed.push(new TextDecoder().decode(buffer.subarray(offset, offset + length)));
+ return length;
+ },
+ readFile: path => {
+ const name = path.substring(path.lastIndexOf("/") + 1);
+ reads.push(name);
+ return f.bytes[name];
+ },
+ },
+ err: message => f.errors.push(message),
+ };
+ vm.runInNewContext(corerunSource, context);
+ context.CORERUN = library.$CORERUN;
+ context.CORERUN.installAuxiliaryModuleGates();
+ context.Module.preRun.forEach(callback => callback());
+ context.CORERUN.queueAuxiliaryModules(f.primary, f.imports, f.payloadPtr, 512, 5, "/app/app.wasm");
+ await new Promise(resolve => setImmediate(resolve));
+ assert.equal(f.view.getUint32(f.payloadPtr, true), 0);
+ assert.equal(f.view.getUint32(f.payloadPtr + 28, true), 0);
+ const emit = stage => {
+ const marker = "AUXILIARY_R2R_ACTIVATE_" + stage;
+ if (rawFs) {
+ const bytes = new TextEncoder().encode("xx" + marker + "\n");
+ context.FS.write({ fd: 1 }, bytes, 2, 9);
+ context.FS.write({ fd: 1 }, bytes, 11, bytes.length - 11);
+ } else {
+ context.out(marker);
+ }
+ };
+ emit(first);
+ assert.equal(timers.length, 1);
+ assert.equal(f.view.getUint32(f.payloadPtr + (first === "MODULES" ? 28 : 0), true), 0);
+ timers.shift()();
+ const second = first === "MODULES" ? "ENTRYPOINTS" : "MODULES";
+ emit(second);
+ timers.shift()();
+ await new Promise(resolve => setImmediate(resolve));
+ assert.deepEqual(reads, [firstFilename]);
+ assert.equal(f.view.getUint32(f.payloadPtr, true), 0);
+ assert.equal(f.view.getUint32(f.payloadPtr + 28, true), 0);
+ finishFirstCompile();
+ await context.CORERUN.waitForAuxiliaryModules();
+ assert.deepEqual(reads, [firstFilename, second === "MODULES" ? "app.1.wasm" : "app.entrypoints.wasm"]);
+ assert.equal(f.notifications(), 1);
+ assert.equal(f.table.get(4)(), 42);
+ const newline = rawFs ? "\n" : "";
+ assert.equal(printed.join(""), "AUXILIARY_R2R_ACTIVATE_" + first + newline + "AUXILIARY_R2R_ACTIVATE_" + second + newline);
+ });
+}
diff --git a/src/native/libs/Common/JavaScript/host/auxiliary-modules.ts b/src/native/libs/Common/JavaScript/host/auxiliary-modules.ts
new file mode 100644
index 00000000000000..2a30aa992f4292
--- /dev/null
+++ b/src/native/libs/Common/JavaScript/host/auxiliary-modules.ts
@@ -0,0 +1,112 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+type AuxiliaryModule = { name: string, statusAddress: number, entrypoints: boolean };
+export type AuxiliaryModuleSource = Response | WebAssembly.Module;
+
+export function readAuxiliaryModuleMetadata(instance: WebAssembly.Instance, memory: WebAssembly.Memory, payloadPtr: number, payloadSize: number, tableSize: number): AuxiliaryModule[] {
+ const getMetadata = instance.exports.getAuxiliaryModuleMetadata;
+ if (getMetadata === undefined) {
+ return [];
+ }
+ if (typeof getMetadata !== "function") {
+ throw new Error("Invalid getAuxiliaryModuleMetadata export.");
+ }
+ const address = getMetadata() >>> 0;
+ if (address === 0) {
+ return [];
+ }
+ const heap = new Uint8Array(memory.buffer);
+ const payloadEnd = payloadPtr + payloadSize;
+ if (payloadEnd > heap.length || address < payloadPtr || address % 4 !== 0 || address + 16 > payloadEnd) {
+ throw new Error("Auxiliary R2R module metadata is outside the primary payload.");
+ }
+ const view = new DataView(memory.buffer);
+ const count = view.getUint32(address + 8, true);
+ if (count > (payloadEnd - address - 16) / 16) {
+ throw new Error("Auxiliary R2R module metadata is truncated.");
+ }
+ const names = new Set();
+ function readModule(filenameRva: number, statusAddress: number, entrypoints: boolean): AuxiliaryModule {
+ const status = view.getUint32(statusAddress, true);
+ const start = payloadPtr + filenameRva;
+ if (status > 1 || start < payloadPtr || start >= payloadEnd) {
+ throw new Error("Invalid auxiliary R2R module metadata.");
+ }
+ let end = start;
+ while (end < payloadEnd && heap[end] !== 0) end++;
+ if (end === payloadEnd) {
+ throw new Error("Unterminated auxiliary R2R module filename.");
+ }
+ const name = new TextDecoder("utf-8", { fatal: true }).decode(heap.subarray(start, end));
+ if (!/^[^./\\][^/\\?#:]*\.wasm$/.test(name) || names.has(name)) {
+ throw new Error(`Invalid or duplicate auxiliary R2R module filename '${name}'.`);
+ }
+ names.add(name);
+ return { name, statusAddress, entrypoints };
+ }
+ const modules = [readModule(view.getUint32(address + 12, true), address, true)];
+ let nextTableIndex = view.getUint32(address + 4, true);
+ if (nextTableIndex > tableSize) {
+ throw new Error("Invalid primary R2R module table range.");
+ }
+ for (let i = 0; i < count; i++) {
+ const entry = address + 16 + i * 16;
+ const tableBase = view.getUint32(entry + 4, true);
+ const methodCount = view.getUint32(entry + 8, true);
+ if (tableBase !== nextTableIndex || tableBase + methodCount > tableSize) {
+ throw new Error("Invalid auxiliary R2R module table range.");
+ }
+ nextTableIndex += methodCount;
+ modules.push(readModule(view.getUint32(entry, true), entry + 12, false));
+ }
+ return modules;
+}
+
+// Each operation settles independently: EntryPoints must not wait for a cold body (or vice versa).
+// Settled errors are retained for test/host waiters and reported immediately, without unhandled rejections.
+export function loadAuxiliaryModules(modules: AuxiliaryModule[], memory: WebAssembly.Memory, imports: WebAssembly.Imports, prepare: (name: string) => Promise, notifyEntryPoints: () => void, reportError: (error: Error) => void): Promise {
+ const operations = modules.map(async module => {
+ try {
+ if (new DataView(memory.buffer).getUint32(module.statusAddress, true) === 1) {
+ return;
+ }
+ const source = await prepare(module.name);
+ let instance: WebAssembly.Instance;
+ if (source instanceof WebAssembly.Module) {
+ // Chromium limits the synchronous constructor to modules below 8 MB, even when compiled.
+ instance = await WebAssembly.instantiate(source, imports);
+ } else {
+ if (!source.ok) {
+ throw new Error(`HTTP status: ${source.status} ${source.statusText}`);
+ }
+ instance = (await WebAssembly.instantiate(await source.arrayBuffer(), imports)).instance;
+ }
+ const activate = instance.exports.ActivateModule;
+ if (typeof activate !== "function") {
+ throw new Error("Missing ActivateModule export.");
+ }
+ // A second load may have completed while this instance was being compiled.
+ if (new DataView(memory.buffer).getUint32(module.statusAddress, true) === 0) {
+ activate();
+ if (new DataView(memory.buffer).getUint32(module.statusAddress, true) !== 1) {
+ throw new Error("ActivateModule did not publish its activation status.");
+ }
+ if (module.entrypoints) {
+ notifyEntryPoints();
+ }
+ }
+ } catch (reason) {
+ const error = new Error(`Failed to load auxiliary R2R module '${module.name}': ${reason instanceof Error ? reason.message : String(reason)}`, { cause: reason });
+ reportError(error);
+ return error;
+ }
+ });
+ const completion = Promise.all(operations).then(errors => {
+ const error = errors.find(error => error !== undefined);
+ if (error) throw error;
+ });
+ // Keep the rejecting completion awaitable while marking background failures as observed.
+ void completion.catch(() => {});
+ return completion;
+}
diff --git a/src/native/libs/Common/JavaScript/loader/assets.ts b/src/native/libs/Common/JavaScript/loader/assets.ts
index 73ee929b60e42c..37af0afe05ae6e 100644
--- a/src/native/libs/Common/JavaScript/loader/assets.ts
+++ b/src/native/libs/Common/JavaScript/loader/assets.ts
@@ -10,6 +10,11 @@ import { locateFile, makeURLAbsoluteWithApplicationBase } from "./bootstrap";
import { fetchLike, responseLike } from "./polyfills";
import { loaderConfig } from "./config";
import { loaderCallbacks } from "./callbacks";
+import type { AuxiliaryModuleSource } from "../host/auxiliary-modules";
+import { fetchAndCompileAuxiliaryModuleInWorker, compileAuxiliaryResponseInWorker } from "./auxiliary-module-worker-client";
+
+type ResourceStatus = Pick;
+type ResourceRequest = { response: Response } | { url: string, options: RequestInit, contentType: string };
let throttlingPCS: PromiseCompletionSource | undefined;
let currentParallelDownloads = 0;
@@ -50,26 +55,30 @@ export async function loadJSModule(asset: JsAsset): Promise {
}
totalAssetsToDownload++;
if (!mod) {
- if (assetInternal.name && !asset.resolvedUrl) {
- asset.resolvedUrl = locateFile(assetInternal.name, true);
- }
- assetInternal.behavior = "js-module-dotnet";
- if (typeof loadBootResourceCallback === "function") {
- const blazorType = behaviorToBlazorAssetTypeMap[assetInternal.behavior];
- dotnetAssert.check(blazorType, `Unsupported asset behavior: ${assetInternal.behavior}`);
- const customLoadResult = loadBootResourceCallback(blazorType, assetInternal.name, asset.resolvedUrl!, assetInternal.hash ?? "", assetInternal.behavior);
- dotnetAssert.check(typeof customLoadResult === "string", "loadBootResourceCallback for JS modules must return string URL");
- asset.resolvedUrl = makeURLAbsoluteWithApplicationBase(customLoadResult);
- }
-
- if (!asset.resolvedUrl) throw new Error("Invalid config, resources is not set");
- mod = await import(/* webpackIgnore: true */ asset.resolvedUrl);
+ const url = resolveJSModuleUrl(assetInternal);
+ mod = await import(/* webpackIgnore: true */ url);
asset.moduleExports = mod;
}
onDownloadedAsset(assetInternal);
return mod;
}
+function resolveJSModuleUrl(asset: AssetEntryInternal): string {
+ asset.resolvedUrl ??= locateFile(asset.name, true);
+ asset.behavior = "js-module-dotnet";
+ if (typeof loadBootResourceCallback === "function") {
+ const blazorType = behaviorToBlazorAssetTypeMap[asset.behavior];
+ dotnetAssert.check(blazorType, `Unsupported asset behavior: ${asset.behavior}`);
+ const customLoadResult = loadBootResourceCallback(blazorType, asset.name, asset.resolvedUrl, asset.hash ?? "", asset.behavior);
+ if (customLoadResult !== undefined && customLoadResult !== null) {
+ dotnetAssert.check(typeof customLoadResult === "string", "loadBootResourceCallback for JS modules must return string URL");
+ asset.resolvedUrl = makeURLAbsoluteWithApplicationBase(customLoadResult);
+ }
+ }
+ if (!asset.resolvedUrl) throw new Error("Invalid config, resources is not set");
+ return asset.resolvedUrl;
+}
+
export async function callLibraryInitializerOnRuntimeConfigLoaded(asset: JsAsset): Promise {
const module = await loadJSModule(asset);
const name = asset.name || asset.resolvedUrl || "unknown";
@@ -177,13 +186,46 @@ async function fetchWebcil(assetInternal: AssetEntryInternal): Promise {
const webcilPromise = loadResource(assetInternal);
const memory = await wasmMemoryPromiseController.promise;
- const instancePromise = dotnetBrowserHostExports.instantiateWebcilModule(webcilPromise, memory, assetInternal.virtualPath!, assetInternal.tableSize, assetInternal.payloadSize);
+ const instancePromise = dotnetBrowserHostExports.instantiateWebcilModule(webcilPromise, memory, assetInternal.virtualPath!, assetInternal.tableSize, assetInternal.payloadSize, prepareAuxiliaryModule);
await instancePromise;
} finally {
onDownloadedAsset(assetInternal);
}
}
+async function prepareAuxiliaryModule(name: string): Promise {
+ const asset = loaderConfig.resources?.auxiliaryAssembly?.find(asset => asset.virtualPath === name);
+ if (!asset) {
+ throw new Error(`Auxiliary R2R module '${name}' is missing from boot resources.`);
+ }
+ const assetInternal = asset as AssetEntryInternal;
+ assetInternal.behavior = "webcil";
+ assetInternal.resolvedUrl ??= locateFile(assetInternal.name);
+ // These are background resources and must not enter the startup download barrier/counters.
+ try {
+ if (!ENVIRONMENT_IS_WEB || loaderConfig.environmentVariables?.["DOTNET_WasmAuxiliaryWorkerCompilation"] !== "1") {
+ return await loadResource(assetInternal);
+ }
+ const workers = loaderConfig.resources?.jsModuleAuxiliaryWorker;
+ if (!workers || workers.length !== 1) {
+ throw new Error("Auxiliary R2R compilation requires one worker script in boot resources.");
+ }
+ const workerUrl = resolveJSModuleUrl(workers[0] as AssetEntryInternal);
+ const result = await withResourceDownloadPolicy(assetInternal, async () => {
+ const request = await resolveResourceRequest(assetInternal);
+ if ("response" in request) {
+ if (!request.response.ok) return request.response;
+ return compileAuxiliaryResponseInWorker(workerUrl, request.response);
+ }
+ return fetchAndCompileAuxiliaryModuleInWorker(workerUrl, request.url, request.options);
+ });
+ return "module" in result ? result.module : result;
+ } finally {
+ if (assetInternal.inprogress) finishThrottling(assetInternal);
+ assetInternal.buffer = null!;
+ assetInternal.pendingDownload = undefined;
+ }
+}
async function fetchDll(assetInternal: AssetEntryInternal): Promise {
const virtualPath = assetInternal.virtualPath!;
assetInternal.behavior = "assembly";
@@ -387,22 +429,39 @@ async function fetchText(asset: AssetEntryInternal): Promise {
return response.text();
}
+function isLocalResource(asset: AssetEntryInternal): boolean {
+ return ENVIRONMENT_IS_SHELL || ENVIRONMENT_IS_NODE || !!asset.resolvedUrl?.startsWith("file://");
+}
+
function loadResource(asset: AssetEntryInternal): Promise {
- if (ENVIRONMENT_IS_SHELL || ENVIRONMENT_IS_NODE || asset.resolvedUrl && asset.resolvedUrl.indexOf("file://") !== -1) {
- // no need to retry or throttle local file access
+ if (isLocalResource(asset)) {
return loadResourceFetch(asset);
}
+ return withResourceDownloadPolicy(asset, async () => {
+ const response = await loadResourceFetch(asset);
+ if (!noBuffer[asset.behavior]) {
+ asset.buffer = await response.arrayBuffer();
+ }
+ return response;
+ });
+}
+
+function withResourceDownloadPolicy(asset: AssetEntryInternal, download: () => Promise): Promise {
+ if (isLocalResource(asset)) {
+ // no need to retry or throttle local file access
+ return download();
+ }
if (!loaderConfig.enableDownloadRetry || noRetry[asset.behavior]) {
// only throttle, no retry
- return loadResourceThrottle(asset);
+ return loadResourceThrottle(asset, download);
}
// retry and throttle
- return loadResourceRetry(asset);
+ return loadResourceRetry(asset, download);
}
const noRetryStatusCodes = new Set([400, 401, 403, 404, 405, 406, 409, 410, 411, 413, 414, 415, 422, 426, 501, 505,]);
-async function loadResourceRetry(asset: AssetEntryInternal): Promise {
- let response: Response;
+async function loadResourceRetry(asset: AssetEntryInternal, download: () => Promise): Promise {
+ let response: T | Response;
response = await loadResourceAttempt();
if (response.ok || asset.isOptional || noRetryStatusCodes.has(response.status)) {
return response;
@@ -426,10 +485,10 @@ async function loadResourceRetry(asset: AssetEntryInternal): Promise {
}
throw new Error(`Failed to load resource '${asset.name}' from '${asset.resolvedUrl}' after multiple attempts. Last HTTP status: ${response.status} ${response.statusText}`);
- async function loadResourceAttempt(): Promise {
- let response: Response;
+ async function loadResourceAttempt(): Promise {
+ let response: T | Response;
try {
- response = await loadResourceThrottle(asset);
+ response = await loadResourceThrottle(asset, download);
if (!response) {
response = responseLike(asset.resolvedUrl!, null, {
status: 404,
@@ -437,6 +496,7 @@ async function loadResourceRetry(asset: AssetEntryInternal): Promise {
});
}
} catch (err: any) {
+ if (err instanceof WebAssembly.CompileError) throw err;
response = responseLike(asset.resolvedUrl!, null, {
status: 500,
statusText: err.message || "Exception during fetch",
@@ -448,21 +508,17 @@ async function loadResourceRetry(asset: AssetEntryInternal): Promise {
// in order to prevent net::ERR_INSUFFICIENT_RESOURCES if we start downloading too many files at same time on a device with low resources
// also when downloading in background
-async function loadResourceThrottle(asset: AssetEntryInternal): Promise {
+async function loadResourceThrottle(asset: AssetEntryInternal, download: () => Promise): Promise {
while (throttlingPCS && !asset.priority) {
await throttlingPCS.promise;
}
try {
startThrottling(asset);
- const responsePromise = loadResourceFetch(asset);
- const response = await responsePromise;
+ const response = await download();
dotnetAssert.check(response, "Bad response in loadResourceThrottle");
- if (!noBuffer[asset.behavior]) {
- asset.buffer = await response.arrayBuffer();
- }
- if (!leaveAfterInstantiation[asset.behavior]) {
+ if (!response.ok || !leaveAfterInstantiation[asset.behavior]) {
finishThrottling(asset);
}
return response;
@@ -494,21 +550,28 @@ function finishThrottling(asset: AssetEntryInternal) {
}
async function loadResourceFetch(asset: AssetEntryInternal): Promise {
+ const request = await resolveResourceRequest(asset);
+ if ("response" in request) return request.response;
+ dotnetLogger.debug(`Attempting to download '${sanitizeUrl(request.url)}'`);
+ return fetchLike(request.url, request.options, request.contentType);
+}
+
+async function resolveResourceRequest(asset: AssetEntryInternal): Promise {
const expectedContentType = behaviorToContentTypeMap[asset.behavior];
dotnetAssert.check(expectedContentType, `Unsupported asset behavior: ${asset.behavior}`);
if (asset.buffer) {
const arrayBuffer = await asset.buffer;
- return responseLike(asset.resolvedUrl!, arrayBuffer, {
+ return { response: responseLike(asset.resolvedUrl!, arrayBuffer, {
status: 200,
statusText: "OK",
headers: {
"Content-Length": arrayBuffer.byteLength.toString(),
"Content-Type": expectedContentType,
}
- });
+ }) };
}
if (asset.pendingDownload) {
- return asset.pendingDownload.response;
+ return { response: await asset.pendingDownload.response };
}
if (typeof loadBootResourceCallback === "function") {
const blazorType = behaviorToBlazorAssetTypeMap[asset.behavior];
@@ -516,8 +579,12 @@ async function loadResourceFetch(asset: AssetEntryInternal): Promise {
const customLoadResult = loadBootResourceCallback(blazorType, asset.name, asset.resolvedUrl!, asset.hash ?? "", asset.behavior);
if (typeof customLoadResult === "string") {
asset.resolvedUrl = makeURLAbsoluteWithApplicationBase(customLoadResult);
- } else if (typeof customLoadResult === "object") {
- return customLoadResult as any;
+ } else if (customLoadResult !== null && typeof customLoadResult === "object") {
+ const response = await customLoadResult;
+ if (!("arrayBuffer" in response)) {
+ throw new Error(`Custom loader for '${asset.name}' must return a Response.`);
+ }
+ return { response };
}
}
dotnetAssert.check(asset.resolvedUrl, "Bad asset.resolvedUrl");
@@ -543,8 +610,7 @@ async function loadResourceFetch(asset: AssetEntryInternal): Promise {
}
}
- dotnetLogger.debug(`Attempting to download '${sanitizeUrl(asset.resolvedUrl!)}'`);
- return fetchLike(asset.resolvedUrl!, fetchOptions, expectedContentType);
+ return { url: asset.resolvedUrl!, options: fetchOptions, contentType: expectedContentType };
}
function onDownloadedAsset(asset: AssetEntryInternal): void {
@@ -644,6 +710,7 @@ export async function prefetchAllResources(extraVfs?: VfsAsset[]): Promise
// Data assets: fetch and discard
if (resources.coreAssembly) resources.coreAssembly.forEach(a => enqueueAsset(a, "assembly"));
if (resources.assembly) resources.assembly.forEach(a => enqueueAsset(a, "assembly"));
+ if (resources.auxiliaryAssembly) resources.auxiliaryAssembly.forEach(a => enqueueAsset(a, "webcil"));
if (resources.vfs) resources.vfs.forEach(a => enqueueAsset(a, "vfs"));
if (extraVfs) extraVfs.forEach(a => enqueueAsset(a, "vfs"));
if (resources.icu) resources.icu.forEach(a => enqueueAsset(a, "icu"));
diff --git a/src/native/libs/Common/JavaScript/loader/auxiliary-module-worker-client.ts b/src/native/libs/Common/JavaScript/loader/auxiliary-module-worker-client.ts
new file mode 100644
index 00000000000000..2425319ff7063a
--- /dev/null
+++ b/src/native/libs/Common/JavaScript/loader/auxiliary-module-worker-client.ts
@@ -0,0 +1,76 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+export type AuxiliaryWorkerRequest = { id: number, url: string, options?: RequestInit, body?: ReadableStream, contentType?: string };
+export type AuxiliaryWorkerResult = { id: number, status: number, statusText: string, module?: WebAssembly.Module, error?: string, errorName?: string };
+type CompiledAuxiliaryDownload = { ok: true, status: number, statusText: string, module: WebAssembly.Module };
+type PendingCompilation = { resolve: (value: Response | CompiledAuxiliaryDownload) => void, reject: (error: Error) => void };
+const pending = new Map();
+let compiler: Worker | undefined;
+let nextId = 0;
+
+export function fetchAndCompileAuxiliaryModuleInWorker(workerUrl: string, url: string, options: RequestInit): Promise {
+ const baseUrl = globalThis.document?.baseURI ?? globalThis.location.href;
+ return prepareModule(workerUrl, { id: nextId++, url: new URL(url, baseUrl).href, options });
+}
+
+export async function compileAuxiliaryResponseInWorker(workerUrl: string, response: Response): Promise {
+ if (!response.body) throw new Error("Auxiliary R2R module response has no body.");
+ return prepareModule(workerUrl, { id: nextId++, url: response.url, body: response.body,
+ contentType: response.headers.get("Content-Type") ?? "" });
+}
+
+function stopCompiler(): void {
+ compiler?.terminate();
+ compiler = undefined;
+}
+
+function failCompilations(error: Error): void {
+ for (const operation of pending.values()) operation.reject(error);
+ pending.clear();
+ stopCompiler();
+}
+
+async function prepareModule(workerUrl: string, request: AuxiliaryWorkerRequest): Promise {
+ if (!compiler) {
+ try {
+ compiler = new Worker(workerUrl, { type: "module", name: "dotnet-auxiliary-compiler" });
+ } catch (error) {
+ stopCompiler();
+ throw error;
+ }
+ compiler.onerror = event => {
+ event.preventDefault();
+ failCompilations(new Error(`Auxiliary compilation worker failed: ${event.message}`));
+ };
+ compiler.onmessageerror = () => failCompilations(new Error("Cannot deserialize auxiliary compilation worker result."));
+ compiler.onmessage = (event: MessageEvent) => {
+ const result = event.data;
+ const operation = pending.get(result.id);
+ if (!operation) {
+ failCompilations(new Error("Unexpected auxiliary compilation worker result."));
+ return;
+ }
+ pending.delete(result.id);
+ if (result.error !== undefined) {
+ operation.reject(result.errorName === "CompileError"
+ ? new WebAssembly.CompileError(result.error) : new Error(result.error));
+ } else if (result.module) {
+ operation.resolve({ ok: true, status: result.status, statusText: result.statusText, module: result.module });
+ } else {
+ operation.resolve(new Response(null, { status: result.status, statusText: result.statusText }));
+ }
+ if (pending.size === 0) stopCompiler();
+ };
+ }
+ return new Promise((resolve, reject) => {
+ pending.set(request.id, { resolve, reject });
+ try {
+ compiler!.postMessage(request, request.body ? [request.body] : []);
+ } catch (error) {
+ pending.delete(request.id);
+ if (pending.size === 0) stopCompiler();
+ reject(error);
+ }
+ });
+}
diff --git a/src/native/libs/Common/JavaScript/loader/auxiliary-module-worker.ts b/src/native/libs/Common/JavaScript/loader/auxiliary-module-worker.ts
new file mode 100644
index 00000000000000..d0207bb10def64
--- /dev/null
+++ b/src/native/libs/Common/JavaScript/loader/auxiliary-module-worker.ts
@@ -0,0 +1,22 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+import type { AuxiliaryWorkerRequest } from "./auxiliary-module-worker-client";
+
+globalThis.onmessage = async (event: MessageEvent) => {
+ const { id, url, options, body, contentType } = event.data;
+ try {
+ const response = body ? new Response(body, { headers: { "Content-Type": contentType! } }) : await fetch(url, options);
+ if (!response.ok) {
+ globalThis.postMessage({ id, status: response.status, statusText: response.statusText });
+ return;
+ }
+ const module = response.headers.get("Content-Type") === "application/wasm" && typeof WebAssembly.compileStreaming === "function"
+ ? await WebAssembly.compileStreaming(response)
+ : await WebAssembly.compile(await response.arrayBuffer());
+ globalThis.postMessage({ id, status: response.status, statusText: response.statusText, module });
+ } catch (error) {
+ globalThis.postMessage({ id, error: error instanceof Error ? error.message : String(error),
+ errorName: error instanceof Error ? error.name : "Error" });
+ }
+};
diff --git a/src/native/libs/Common/JavaScript/loader/config.ts b/src/native/libs/Common/JavaScript/loader/config.ts
index 11c9c19e6115f9..46c7e6376fceb5 100644
--- a/src/native/libs/Common/JavaScript/loader/config.ts
+++ b/src/native/libs/Common/JavaScript/loader/config.ts
@@ -63,10 +63,12 @@ function mergeResources(target: Assets, source: Assets): Assets {
source.hash = source.hash ?? target.hash;
source.coreAssembly = [...target.coreAssembly!, ...source.coreAssembly || []];
source.assembly = [...target.assembly!, ...source.assembly || []];
+ source.auxiliaryAssembly = [...target.auxiliaryAssembly!, ...source.auxiliaryAssembly || []];
source.lazyAssembly = [...target.lazyAssembly!, ...source.lazyAssembly || []];
source.corePdb = [...target.corePdb!, ...source.corePdb || []];
source.pdb = [...target.pdb!, ...source.pdb || []];
source.jsModuleNative = [...target.jsModuleNative!, ...source.jsModuleNative || []];
+ source.jsModuleAuxiliaryWorker = [...target.jsModuleAuxiliaryWorker!, ...source.jsModuleAuxiliaryWorker || []];
source.jsModuleDiagnostics = [...target.jsModuleDiagnostics!, ...source.jsModuleDiagnostics || []];
source.jsModuleRuntime = [...target.jsModuleRuntime!, ...source.jsModuleRuntime || []];
source.wasmSymbols = [...target.wasmSymbols!, ...source.wasmSymbols || []];
@@ -114,10 +116,12 @@ function normalizeConfig(target: LoaderConfigInternal) {
function normalizeResources(target: Assets) {
if (!target.coreAssembly) target.coreAssembly = [];
if (!target.assembly) target.assembly = [];
+ if (!target.auxiliaryAssembly) target.auxiliaryAssembly = [];
if (!target.lazyAssembly) target.lazyAssembly = [];
if (!target.corePdb) target.corePdb = [];
if (!target.pdb) target.pdb = [];
if (!target.jsModuleNative) target.jsModuleNative = [];
+ if (!target.jsModuleAuxiliaryWorker) target.jsModuleAuxiliaryWorker = [];
if (!target.jsModuleDiagnostics) target.jsModuleDiagnostics = [];
if (!target.jsModuleRuntime) target.jsModuleRuntime = [];
if (!target.wasmSymbols) target.wasmSymbols = [];
diff --git a/src/native/libs/Common/JavaScript/loader/dotnet.d.ts b/src/native/libs/Common/JavaScript/loader/dotnet.d.ts
index 4220f6abea87fa..a2fe28cc9a509f 100644
--- a/src/native/libs/Common/JavaScript/loader/dotnet.d.ts
+++ b/src/native/libs/Common/JavaScript/loader/dotnet.d.ts
@@ -258,6 +258,9 @@ interface Assets {
jsModuleRuntime: JsAsset[];
wasmSymbols?: SymbolsAsset[];
wasmNative: WasmAsset[];
+ auxiliaryAssembly?: (WasmAsset & {
+ virtualPath: string;
+ })[];
icu?: IcuAsset[];
satelliteResources?: {
[cultureName: string]: AssemblyAsset[];
diff --git a/src/native/libs/Common/JavaScript/types/public-api.ts b/src/native/libs/Common/JavaScript/types/public-api.ts
index 895335f8d1b44c..eadc63f98be92e 100644
--- a/src/native/libs/Common/JavaScript/types/public-api.ts
+++ b/src/native/libs/Common/JavaScript/types/public-api.ts
@@ -217,11 +217,13 @@ export interface Assets {
lazyAssembly?: AssemblyAsset[];
corePdb?: PdbAsset[];
pdb?: PdbAsset[];
+ jsModuleAuxiliaryWorker?: JsAsset[];
jsModuleDiagnostics?: JsAsset[];
jsModuleNative: JsAsset[];
jsModuleRuntime: JsAsset[];
wasmSymbols?: SymbolsAsset[];
wasmNative: WasmAsset[];
+ auxiliaryAssembly?: (WasmAsset & { virtualPath: string })[];
icu?: IcuAsset[];
satelliteResources?: {
[cultureName: string]: AssemblyAsset[];
diff --git a/src/native/package.json b/src/native/package.json
index 9f76cf353138ad..91eba44d2faa69 100644
--- a/src/native/package.json
+++ b/src/native/package.json
@@ -15,6 +15,7 @@
"rollup:release": "rollup -c --environment Configuration:Release,ProductVersion:12.0,ContinuousIntegrationBuild:false,RuntimeFlavor:CoreCLR",
"rollup:debug": "rollup -c --environment Configuration:Debug,ProductVersion:12.0,ContinuousIntegrationBuild:false,RuntimeFlavor:CoreCLR",
"lint": "eslint --no-color --max-warnings=0 \"./**/*.ts\" \"./**/*.js\"",
+ "test:auxiliary-modules": "node --test ./libs/Common/JavaScript/host/auxiliary-modules.test.mjs",
"format": "eslint --fix \"./**/*.ts\" \"./*.js\""
},
"keywords": [
diff --git a/src/native/rollup.config.js b/src/native/rollup.config.js
index 32f3e3260f493e..fb5adee0d94e01 100644
--- a/src/native/rollup.config.js
+++ b/src/native/rollup.config.js
@@ -50,6 +50,17 @@ const dotnetJS = configure({
}
});
+const dotnetAuxiliaryWorkerJS = configure({
+ input: "./libs/Common/JavaScript/loader/auxiliary-module-worker.ts",
+ output: [{
+ file: staticLibDestination + "/dotnet.auxiliary.worker.js",
+ }],
+ terser: {
+ compress: { module: true },
+ mangle: { module: true },
+ }
+});
+
const libNativeBrowser = configure({
input: "./libs/System.Native.Browser/native/index.ts",
output: [{
@@ -162,6 +173,7 @@ const libBrowserHost = configure({
export default defineConfig([
dotnetJS,
+ dotnetAuxiliaryWorkerJS,
dotnetDTS,
libNativeBrowser,
libBrowserUtils,
diff --git a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/AssetsComputingHelper.cs b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/AssetsComputingHelper.cs
index 320d57bdb3a37f..57ceb327e9369f 100644
--- a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/AssetsComputingHelper.cs
+++ b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/AssetsComputingHelper.cs
@@ -27,6 +27,7 @@ public class AssetsComputingHelper
private static readonly string[] dotnetJsSingleThreadNames = new[]
{
"dotnet",
+ "dotnet.auxiliary.worker",
"dotnet.native",
"dotnet.runtime"
};
@@ -135,6 +136,7 @@ public static string GetCandidateRelativePath(ITaskItem candidate, bool fingerpr
relativePath = (fileName, extension) switch
{
("dotnet", ".js") => string.Concat(fileName, fingerprintDotNetJs ? requiredFingerprint : optionalFingerprint, extension),
+ ("dotnet.auxiliary.worker", ".js") => string.Concat(fileName, requiredFingerprint, extension),
("dotnet.runtime", ".js") => string.Concat(fileName, requiredFingerprint, extension),
("dotnet.diagnostics", ".js") => string.Concat(fileName, requiredFingerprint, extension),
("dotnet.native", ".js") => string.Concat(fileName, requiredFingerprint, extension),
diff --git a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/AttachWebcilSizes.cs b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/AttachWebcilSizes.cs
index e19d2f21c029be..b3f1dd0a80f903 100644
--- a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/AttachWebcilSizes.cs
+++ b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/AttachWebcilSizes.cs
@@ -65,7 +65,9 @@ private static bool IsWebcilInWasm(ITaskItem candidate)
if (!string.Equals(candidate.GetMetadata("Extension"), ".wasm", StringComparison.OrdinalIgnoreCase))
return false;
+ string traitValue = candidate.GetMetadata("AssetTraitValue");
return !(string.Equals(candidate.GetMetadata("AssetTraitName"), "WasmResource", StringComparison.OrdinalIgnoreCase)
- && string.Equals(candidate.GetMetadata("AssetTraitValue"), "native", StringComparison.OrdinalIgnoreCase));
+ && (string.Equals(traitValue, "native", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(traitValue, "readyToRunAuxiliary", StringComparison.OrdinalIgnoreCase)));
}
}
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 554ae0f476f01f..c9f342d888ad10 100644
--- a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonBuilderHelper.cs
+++ b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonBuilderHelper.cs
@@ -123,9 +123,11 @@ static void AddDictionary(StringBuilder sb, Dictionary? res)
}
AddDictionary(sb, resources.assembly);
+ AddDictionary(sb, resources.auxiliaryAssembly);
AddDictionary(sb, resources.coreAssembly);
AddDictionary(sb, resources.jsModuleWorker);
+ AddDictionary(sb, resources.jsModuleAuxiliaryWorker);
AddDictionary(sb, resources.jsModuleDiagnostics);
AddDictionary(sb, resources.jsModuleNative);
AddDictionary(sb, resources.jsModuleRuntime);
@@ -161,7 +163,9 @@ static void AddDictionary(StringBuilder sb, Dictionary? res)
ResourcesData resources = (ResourcesData)bootConfig.resources;
string resourceExtension = Path.GetExtension(resourceName);
- if (TargetFrameworkVersion < version110 && resourceName.StartsWith("dotnet.native.worker", StringComparison.OrdinalIgnoreCase) && string.Equals(resourceExtension, ".mjs", StringComparison.OrdinalIgnoreCase))
+ if (resourceName.StartsWith("dotnet.auxiliary.worker", StringComparison.OrdinalIgnoreCase) && string.Equals(resourceExtension, ".js", StringComparison.OrdinalIgnoreCase))
+ return resources.jsModuleAuxiliaryWorker ??= new();
+ else if (TargetFrameworkVersion < version110 && resourceName.StartsWith("dotnet.native.worker", StringComparison.OrdinalIgnoreCase) && string.Equals(resourceExtension, ".mjs", StringComparison.OrdinalIgnoreCase))
return resources.jsModuleWorker ??= new();
else if (resourceName.StartsWith("dotnet.diagnostics", StringComparison.OrdinalIgnoreCase) && string.Equals(resourceExtension, ".js", StringComparison.OrdinalIgnoreCase))
return resources.jsModuleDiagnostics ??= new();
@@ -224,6 +228,17 @@ public string TransformResourcesToAssets(BootJsonData config, bool bundlerFriend
assets.jsModuleRuntime = MapJsAssets(resources.jsModuleRuntime);
assets.jsModuleNative = MapJsAssets(resources.jsModuleNative);
assets.jsModuleWorker = MapJsAssets(resources.jsModuleWorker);
+ assets.jsModuleAuxiliaryWorker = resources.jsModuleAuxiliaryWorker?.Select(a =>
+ {
+ var asset = new JsAsset() { name = a.Key };
+ if (bundlerFriendly)
+ {
+ string escaped = EscapeName(a.Key);
+ imports.Add($"const {escaped} = new URL(\"./{a.Key}\", import.meta.url).href;");
+ asset.resolvedUrl = EncodeJavascriptVariableInJson(escaped);
+ }
+ return asset;
+ }).ToList();
assets.jsModuleDiagnostics = MapJsAssets(resources.jsModuleDiagnostics);
assets.wasmNative = resources.wasmNative?.Select(a =>
@@ -254,6 +269,7 @@ public string TransformResourcesToAssets(BootJsonData config, bool bundlerFriend
assets.icu = MapGeneralAssets(resources.icu);
assets.coreAssembly = MapWebcilAssets(resources.coreAssembly);
assets.assembly = MapWebcilAssets(resources.assembly);
+ assets.auxiliaryAssembly = MapGeneralAssets(resources.auxiliaryAssembly);
assets.corePdb = MapGeneralAssets(resources.corePdb);
assets.pdb = MapGeneralAssets(resources.pdb);
assets.lazyAssembly = MapWebcilAssets(resources.lazyAssembly);
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 a161042c338bfa..64719c79c4d4ea 100644
--- a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonData.cs
+++ b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonData.cs
@@ -190,6 +190,9 @@ public class ResourcesData
[DataMember(EmitDefaultValue = false)]
public ResourceHashesByNameDictionary jsModuleDiagnostics { get; set; }
+ [DataMember(EmitDefaultValue = false)]
+ public ResourceHashesByNameDictionary jsModuleAuxiliaryWorker { get; set; }
+
[DataMember(EmitDefaultValue = false)]
public ResourceHashesByNameDictionary jsModuleNative { get; set; }
@@ -215,6 +218,9 @@ public class ResourcesData
///
public ResourceHashesByNameDictionary assembly { get; set; } = new ResourceHashesByNameDictionary();
+ [DataMember(EmitDefaultValue = false)]
+ public ResourceHashesByNameDictionary auxiliaryAssembly { get; set; }
+
///
/// "debug" (.pdb) resources needed to start MonoVM
///
@@ -288,6 +294,9 @@ public class AssetsData
[DataMember(EmitDefaultValue = false)]
public List jsModuleDiagnostics { get; set; }
+ [DataMember(EmitDefaultValue = false)]
+ public List jsModuleAuxiliaryWorker { get; set; }
+
[DataMember(EmitDefaultValue = false)]
public List jsModuleNative { get; set; }
@@ -313,6 +322,9 @@ public class AssetsData
///
public List assembly { get; set; } = new();
+ [DataMember(EmitDefaultValue = false)]
+ public List auxiliaryAssembly { get; set; }
+
///
/// "debug" (.pdb) resources needed to start MonoVM
///
@@ -368,6 +380,7 @@ public class JsAsset
{
public string name { get; set; }
public string moduleExports { get; set; }
+ public string resolvedUrl { get; set; }
}
[DataContract]
diff --git a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/CollectWasmAuxiliaryR2RModules.cs b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/CollectWasmAuxiliaryR2RModules.cs
new file mode 100644
index 00000000000000..658998a643edb9
--- /dev/null
+++ b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/CollectWasmAuxiliaryR2RModules.cs
@@ -0,0 +1,87 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text.Json;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+
+namespace Microsoft.NET.Sdk.WebAssembly;
+
+public sealed class CollectWasmAuxiliaryR2RModules : Task
+{
+ [Required]
+ public ITaskItem[] PrimaryImages { get; set; }
+
+ public bool AllowMissingFiles { get; set; }
+
+ [Output]
+ public ITaskItem[] Modules { get; set; } = Array.Empty();
+
+ [Output]
+ public ITaskItem[] FileWrites { get; set; } = Array.Empty();
+
+ [Output]
+ public ITaskItem[] MissingOutputs { get; set; } = Array.Empty();
+
+ public override bool Execute()
+ {
+ var modules = new List();
+ var writes = new List();
+ var missing = new List();
+ var names = new HashSet(StringComparer.OrdinalIgnoreCase);
+ try
+ {
+ foreach (ITaskItem primary in PrimaryImages)
+ {
+ string path = Path.GetFullPath(primary.ItemSpec);
+ string manifest = Path.ChangeExtension(path, ".auxiliary.json");
+ if (AllowMissingFiles && !File.Exists(manifest))
+ {
+ missing.Add(new TaskItem(manifest));
+ continue;
+ }
+ using JsonDocument document = JsonDocument.Parse(File.ReadAllText(manifest));
+ writes.Add(new TaskItem(manifest));
+ AddModule(document.RootElement.GetProperty("entrypoints").GetString());
+ foreach (JsonElement module in document.RootElement.GetProperty("modules").EnumerateArray())
+ AddModule(module.GetString());
+
+ void AddModule(string? name)
+ {
+ if (string.IsNullOrEmpty(name) || name != Path.GetFileName(name) ||
+ name.IndexOfAny(new[] { '/', '\\', ':', '?', '#' }) >= 0 ||
+ name.StartsWith(".", StringComparison.Ordinal) ||
+ !name.EndsWith(".wasm", StringComparison.Ordinal) || !names.Add(name))
+ {
+ throw new InvalidDataException($"Invalid or duplicate auxiliary module filename '{name}' in '{manifest}'.");
+ }
+ string modulePath = Path.Combine(Path.GetDirectoryName(path)!, name);
+ if (!File.Exists(modulePath))
+ {
+ if (!AllowMissingFiles)
+ throw new FileNotFoundException($"Auxiliary ReadyToRun module listed in '{manifest}' does not exist.", modulePath);
+ missing.Add(new TaskItem(modulePath));
+ }
+
+ var item = new TaskItem(modulePath);
+ item.SetMetadata("ContentRoot", Path.GetDirectoryName(modulePath) + Path.DirectorySeparatorChar);
+ item.SetMetadata("RelativePath", "_framework/" + name);
+ item.SetMetadata("OriginalItemSpec", modulePath);
+ modules.Add(item);
+ writes.Add(new TaskItem(modulePath));
+ }
+ }
+ }
+ catch (Exception exception) when (exception is IOException or InvalidDataException or JsonException or InvalidOperationException or KeyNotFoundException or UnauthorizedAccessException)
+ {
+ Log.LogErrorFromException(exception, showStackTrace: false);
+ }
+ Modules = modules.ToArray();
+ FileWrites = writes.ToArray();
+ MissingOutputs = missing.ToArray();
+ return !Log.HasLoggedErrors;
+ }
+}
diff --git a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/ComputeWasmPublishAssets.cs b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/ComputeWasmPublishAssets.cs
index 5bdc8ef3a656c3..9e212a0d37430c 100644
--- a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/ComputeWasmPublishAssets.cs
+++ b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/ComputeWasmPublishAssets.cs
@@ -217,7 +217,9 @@ private void ProcessNativeAssets(
{
var extension = ".js";
var baseName = Path.GetFileNameWithoutExtension(key);
- if (baseName.StartsWith("dotnet.native.worker")) {
+ if (baseName.StartsWith("dotnet.auxiliary.worker"))
+ baseName = "dotnet.auxiliary.worker";
+ else if (baseName.StartsWith("dotnet.native.worker")) {
baseName = "dotnet.native.worker";
extension = ".mjs";
} else if (baseName.StartsWith("dotnet.native"))
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 56104bbc936f3c..eec275cddb1714 100644
--- a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/GenerateWasmBootJson.cs
+++ b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/GenerateWasmBootJson.cs
@@ -294,6 +294,12 @@ private void WriteBootConfig(string entryAssemblyName)
}
}
}
+ else if (string.Equals("WasmResource", assetTraitName, StringComparison.OrdinalIgnoreCase)
+ && string.Equals("readyToRunAuxiliary", assetTraitValue, StringComparison.OrdinalIgnoreCase))
+ {
+ MapFingerprintedAsset(resourceData, resourceRoute, resourceName);
+ resourceList = resourceData.auxiliaryAssembly ??= new ResourceHashesByNameDictionary();
+ }
else if (string.Equals("WasmResource", assetTraitName, StringComparison.OrdinalIgnoreCase)
&& string.Equals("readyToRunComposite", assetTraitValue, StringComparison.OrdinalIgnoreCase))
{
@@ -422,7 +428,8 @@ private void WriteBootConfig(string entryAssemblyName)
bool isWebcilInWasmAssembly = IsTargeting110OrLater()
&& string.Equals(fileExtension, ".wasm", StringComparison.OrdinalIgnoreCase)
&& !(string.Equals(assetTraitName, "WasmResource", StringComparison.OrdinalIgnoreCase)
- && string.Equals(assetTraitValue, "native", StringComparison.OrdinalIgnoreCase));
+ && (string.Equals(assetTraitValue, "native", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(assetTraitValue, "readyToRunAuxiliary", StringComparison.OrdinalIgnoreCase)));
if (isWebcilInWasmAssembly)
{
diff --git a/src/tests/readytorun/wasm/DelayedR2RActivation/DelayedR2RActivation.cs b/src/tests/readytorun/wasm/DelayedR2RActivation/DelayedR2RActivation.cs
new file mode 100644
index 00000000000000..a0d360ce5568d2
--- /dev/null
+++ b/src/tests/readytorun/wasm/DelayedR2RActivation/DelayedR2RActivation.cs
@@ -0,0 +1,674 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Reflection;
+using System.Runtime;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+#if !PARTITIONED_WASM_R2R_BOUNDARY
+using DelayedR2RActivationHelpers;
+using Xunit;
+#endif
+
+#if PARTITIONED_WASM_R2R_BOUNDARY
+public static class PartitionedR2RBoundary
+{
+#if PARTITIONED_WASM_R2R_ALL_HOT
+ public static int Main()
+ {
+ const int PrefersInterpreterEntryPoint = 0x4;
+ MethodInfo method = typeof(PartitionedR2RBoundary).GetMethod(nameof(Main));
+ IntPtr entryPoint = method.MethodHandle.GetFunctionPointer();
+ return (Marshal.ReadInt32(entryPoint, 3 * IntPtr.Size) & PrefersInterpreterEntryPoint) == 0 ? 100 : 101;
+ }
+#else
+ private static int Main() => 100;
+#endif
+}
+#else
+namespace System.Runtime
+{
+ [AttributeUsage(AttributeTargets.Method)]
+ internal sealed class BypassReadyToRunAttribute : Attribute
+ {
+ }
+}
+
+public class DelayedR2RActivation
+{
+ // Mirrors the private layout of PortableEntryPoint in src/coreclr/vm/precode_portable.hpp: _flags follows
+ // the _pActualCode, _pMD, and _pInterpreterData pointers. Keep these in sync with that file.
+ private static readonly int PortableEntryPointFlagsOffset = 3 * IntPtr.Size;
+ private const int PrefersInterpreterEntryPoint = 0x4;
+ private const int PendingAuxiliaryModule = 0x10;
+ private const int AwaitingAuxiliaryEntryPoints = 0x20;
+
+ // A delayed method waits twice: first for the auxiliary EntryPoints module that says where its code is
+ // (awaiting EntryPoints), then for the auxiliary module that holds the code (pending auxiliary module).
+ // Each wait is simulated by counting calls to the waiting methods.
+ private static readonly int s_activationCallCount = ReadCallCount("DOTNET_R2RDelayedActivationCallCount", 0);
+ private static readonly int s_entryPointsCallCount =
+ ReadCallCount("DOTNET_R2RDelayedEntryPointsActivationCallCount", s_activationCallCount);
+ private static readonly bool s_entryPointsDelayed = s_entryPointsCallCount >= 1000;
+ private static readonly bool s_moduleDelayed = s_activationCallCount >= 1000;
+ // Counts between 1 and 999 can run out while the test sets up, so only check states for 0 or at least 1000.
+ private static readonly bool s_checkStates =
+ (s_entryPointsCallCount == 0 || s_entryPointsDelayed) && (s_activationCallCount == 0 || s_moduleDelayed);
+ private static Action s_atRecursiveBottom;
+
+#if PARTITIONED_WASM_R2R
+ [Fact]
+ [BypassReadyToRun]
+ [RuntimeAsyncMethodGeneration(false)]
+ public static async Task ActivatesDownloadedModulesAsync()
+ {
+ bool manualActivation = Environment.GetEnvironmentVariable("DOTNET_WasmAuxiliaryManualActivation") == "1";
+ bool bodyFirst = Environment.GetEnvironmentVariable("DOTNET_WasmAuxiliaryBodyFirst") == "1";
+ MethodInfo coldMethod = typeof(DelayedR2RActivation).GetMethod(
+ nameof(PartitionedColdTarget), BindingFlags.Static | BindingFlags.NonPublic);
+ MethodInfo genericMethod = typeof(DelayedR2RActivation).GetMethod(
+ nameof(PartitionedGenericTarget), BindingFlags.Static | BindingFlags.NonPublic).MakeGenericMethod(typeof(int));
+ IntPtr coldEntryPoint = PrepareMethod(coldMethod);
+ IntPtr genericEntryPoint = PrepareMethod(genericMethod);
+ IntPtr virtualEntryPoint = PrepareMethod(
+ typeof(PartitionedVirtualTarget).GetMethod(nameof(PartitionedVirtualTarget.Invoke)));
+ IntPtr exceptionEntryPoint = PrepareMethod(typeof(DelayedR2RActivation).GetMethod(
+ nameof(PartitionedExceptionTarget), BindingFlags.Static | BindingFlags.NonPublic));
+ IntPtr suspendedEntryPoint = PrepareMethod(typeof(DelayedR2RActivation).GetMethod(
+ nameof(PartitionedSuspendedTarget), BindingFlags.Static | BindingFlags.NonPublic));
+ IntPtr crossModuleEntryPoint = PrepareMethod(
+ typeof(List).GetMethod("AddWithResize", BindingFlags.Instance | BindingFlags.NonPublic));
+ // Unmanaged function pointers are Wasm table indices, not PortableEntryPoint addresses.
+ PrepareMethod(typeof(DelayedR2RActivation).GetMethod(nameof(PartitionedUnmanagedTarget)));
+ Assert.Equal(42, CallPartitionedUnmanagedTarget(1));
+ object marker = new();
+ TaskCompletionSource source = new();
+ Task