[wasm] Use ordinary struct alignment for Int128/UInt128 on wasm - #131421
[wasm] Use ordinary struct alignment for Int128/UInt128 on wasm#131421lewing wants to merge 2 commits into
Conversation
Int128/UInt128 were given a 16-byte alignment requirement on wasm by both the VM
and crossgen2, annotated "sizeof(v128)". Int128 is not a v128: the only 128-bit
value type in WebAssembly is v128, and WasmLowering.IsWasmV128Type recognizes
only Vector128<T> and a 128-bit Vector<T>, so Int128 uses the generic
by-reference struct ABI.
Every other alignment override in CheckForSystemTypes exists because the managed
type corresponds to a fundamental data type in the target ABI (Vector128<T> to
__m128, and so on). Int128 has no counterpart on wasm, so it takes ordinary
struct alignment, which is the 8 its two ulong fields produce. On the crossgen2
side that means routing wasm through the existing 32-bit path.
The 16 was observable as wrong codegen at the interpreter-to-R2R boundary. Wasm
signature encoding spells a by-reference struct argument as S<N>, and
RaiseSignature resolves S16 through GetCachedStructOfSize, which keys only on
size and keeps the first struct seen at that size. The thunk therefore used that
struct's 8-byte alignment while the interpreter used Int128's 16, placing the
argument at argsBase+24 instead of argsBase+32. Int128.Equals and CompareTo then
read a shifted value while operator== was unaffected.
This also makes S<N> sound rather than accidentally correct: single-field structs
wrapping a v128 are unwrapped by LowerType and encode as V, and a v128 field is
the only source of 16-byte alignment on wasm, so once Int128 takes its natural
alignment nothing reaching S<N> requires more than 8.
The VM and crossgen2 must change together or managed field layout desyncs.
Validated on browser wasm against builds with and without the change, rebuilding
both corerun and the R2R composite for each:
Int128 repro, R2R on: before, Equals=False and CompareTo=+/-1 on equal values;
after, all correct. R2R off correct in both.
System.Text.Json, the Int128-bearing classes, 1496 tests, on a branch carrying
the other outstanding wasm R2R fixes: before, R2R-on 20 failed (15 Int128);
after, 2 failed (0 Int128). Those 2 also fail with R2R off, so they are
pre-existing and unrelated.
Microsoft.Bcl.Memory 550/550 and Unsafe 128/128, R2R on and off.
Encoding Int128 as V instead does not work: V selects a different calling
convention, passing the value in a wasm v128 local rather than by reference, and
the JIT has no notion of Int128, so R2R code faults with "function signature
mismatch".
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 50ddfb2c-b6f8-4847-81e7-44d37d44a175
|
Azure Pipelines: Successfully started running 3 pipeline(s). 13 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
|
Tagging subscribers to this area: @agocke |
|
I believe clang sets |
|
cc @dotnet/wasm-contrib |
There was a problem hiding this comment.
Pull request overview
This PR changes how System.Int128/System.UInt128 alignment is treated on WebAssembly so that wasm uses ordinary struct alignment (8) instead of a forced 16-byte alignment, keeping VM and crossgen2/R2R field layout consistent and avoiding ABI mismatches at interpreter↔R2R boundaries.
Changes:
- In CoreCLR VM
CheckForSystemTypes, switch wasmInt128/UInt128alignment requirement from 16 to 8. - In crossgen2 field layout, treat
TargetArchitecture.Wasm32like existing 32-bit ARM behavior by using the standard metadata layout engine forInt128/UInt128.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/coreclr/vm/methodtablebuilder.cpp | Updates wasm Int128/UInt128 alignment requirement to 8 in system type layout overrides. |
| src/coreclr/tools/Common/Compiler/Int128FieldLayoutAlgorithm.cs | Routes wasm32 through the “32-bit metadata layout” path to match VM layout and avoid 16-byte override. |
| // 32bit platforms use standard metadata layout engine | ||
| if (defType.Context.Target.Architecture == TargetArchitecture.ARM) | ||
| if (defType.Context.Target.Architecture is TargetArchitecture.ARM or TargetArchitecture.Wasm32) | ||
| { |
There was a problem hiding this comment.
Correct that the comment is imprecise — X86 is 32-bit and does take the 16-byte override. That imprecision predates this change though, and the condition it describes is right there on the next line, so I'd rather not expand it here: rewording a pre-existing comment is unrelated churn, and per repo convention comments are kept minimal because inaccurate ones mislead and need maintenance.
Happy to fix it if a maintainer would prefer.
Note
This comment was generated by GitHub Copilot.
| if (defType.Context.Target.Architecture is TargetArchitecture.ARM or TargetArchitecture.Wasm32) | ||
| { | ||
| layoutFromMetadata.LayoutAbiStable = true; | ||
| layoutFromMetadata.IsInt128OrHasInt128Fields = true; |
There was a problem hiding this comment.
Added Wasm32 cases to TestAlignmentBehavior_AutoAlignmentRulesWithOSDependence in 5fe365d. Both fail without the layout change (alignment 16 rather than 8), so they actually guard the new branch rather than just documenting it.
Note
This comment was generated by GitHub Copilot.
Covers the new Wasm32 branch in Int128FieldLayoutAlgorithm alongside the existing ARM/ARM64/X86/X64 cases. Both new cases fail without the layout change (alignment 16 rather than 8). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 50ddfb2c-b6f8-4847-81e7-44d37d44a175
| // Wasm has no 128-bit scalar type to match, so use the natural field alignment. | ||
| pLayout->SetAlignmentRequirement(8); | ||
| #else | ||
| #error Unknown architecture | ||
| #endif // TARGET_64BIT |
This impacts packing and layout, something we really need to have correct from v1 as it can be breaking (behavioral, impact interop, invalidate R2R images, etc) to change it later. The general tooling docs describing the basic C ABI (not required, just existing tooling convention) explicitly calls out 16: https://github.com/WebAssembly/tool-conventions/blob/main/BasicCABI.md#data-representation
Rust notably had a similar bug where they didn't use 16-byte alignment as expected and had to fix it last year: rust-lang/rust#133991 So I don't think we can take this and rather need to ensure the interpreter correctly handles this for stack data. |
|
we have the same problem for v256/512, for which there are no intrinsic types either |
Those ones will require every ecosystem to take a break though, if WASM adds official types in the future. As of now, the layout/alignment should be correct as they should be recognized as 2 or 4x V128 fields I think it's quite a bit different from Is getting the interpreter to handle the 16-byte stack alignment required here (and for |
|
You're right, and the BasicCABI table refutes my reasoning rather than just outweighing it. The row is:
My argument was "there is no corresponding wasm value type, therefore no ABI basis for the alignment override." The spec assigns alignment 16 to a type whose value type is explicitly On the interpreter question: the interpreter looks like the side that's already correct. Tracing the original failure, it placed the Teaching That divergence is reachable rather than theoretical, since Separately, the signature round-trip loses alignment for the vector types too, and that one is independent of whatever we decide for Thunk signatures read out of the R2R image show the collision — Agreed no ecosystem contract is at stake for those yet. Worth noting the alignment isn't currently what you'd expect from 2x/4x V128 though: wasm has no branch in the Note This comment was generated by GitHub Copilot. |
it requires another type signature I think, probably not huge but let me look. |
My point was that they are also currently broken and have 32/64 alignment which probably doesn't make sense for the current wasm abi. So the question becomes how should we pass them? multiple v128s, or as a struct? |
They are defined as So a struct containing 2x V128 fields and a struct containing 2x structs, each containing 2x V128 fields. -- I expect WASM treats this the same as a struct containing 2x and 4x V128 fields, respectively. But some ABIs and our own layout algorithm/recursion should already be taking that into account when reporting things back. |
Right, I'm telling you the current handling is active broken because there is an alignment mismatch. |
|
Right, I got that. I was explaining what the expected behavior is. If its incorrect today, then we probably need to fix it (I have no preference on whether that's the same or a separate PR) |
|
Working on a separate prototype that can express the alignment and decomposition that matches the abi in the call signature without adding entirely new arg types. |
## Summary CoreCLR browser-WASM cannot use `fork` or managed process creation, so runtime tests marked `RequiresProcessIsolation` were omitted from merged runners. This PR enables those tests without adding Node-specific process APIs to managed runtime code: - the merged runner emits a plan using its existing filtering and striping; - the host Bash or Batch script executes each existing per-test wrapper in a fresh child shell; - the merged runner imports the child results into its normal xUnit report. The PR also includes the runtime-test infrastructure, platform gating, and focused runtime/test fixes needed to make priority-0 and priority-1 CoreCLR browser-WASM coverage pass consistently. This reuses the existing CoreCLR `corerun` wrappers; it does not introduce a parallel `browserhost` scheduler or duplicate per-test execution semantics. ## Execution model The browser run has three phases: 1. **Plan:** the generated merged runner writes the selected out-of-process assembly paths. 2. **Execute:** the host runs each existing `.sh` or `.cmd` wrapper and records its output, exit code, pass/skip state, and skip reason. 3. **Import:** the normal merged run executes in-process tests and reports the precomputed child outcomes through the existing xUnit summary. Existing wrappers remain authoritative for test arguments, environment variables, pre/post commands, expected exit codes, and per-test timeouts. Merged-runner arguments and stripe state are cleared while child wrappers run and restored afterward. The result format is versioned and bound to a per-run token. Missing plans, wrappers, results, malformed records, or stale tokens fail as infrastructure errors. A child in which every test is skipped is reported as skipped rather than passed. Empty plan/status environment settings are treated as unset. The host orchestration marker is emitted only for CoreCLR browser-WASM merged runners that contain out-of-process tests. ## Browser-WASM enablement and fixes - **Discovery:** only buildable projects with runnable wrappers are added to merged-runner OOP plans. - **Interpreter:** `InterpreterTester` runs `Interpreter.dll` directly through the host OOP path with the required interpreter environment instead of calling `Process.Start` inside the browser. - **ReadyToRun assets:** host Crossgen2 assets are staged for cross-architecture runs, and emitted WASM components, including composite output, are validated. - **Composite ReadyToRun tests:** affected CoreCLR browser tests carry an ActiveIssue for [#131767](#131767). Their browser projects do not force Crossgen2, allowing the managed wrapper to report the skip before CoreCLR probes the unsupported composite WebCIL layout. Other targets retain their existing composite R2R coverage. - **Host paths:** Batch orchestration retains Windows paths for host file operations while exposing Unix-style absolute paths to browser managed code. - **Multithreading:** `FeatureMultithreading` is available during test project evaluation. Tests whose only requirement is managed multithreading use that global capability, so they remain excluded on single-threaded targets and become buildable when threads are enabled. - **Precise platform scope:** tests requiring background/server GC, native threads, child processes, unsupported browser APIs, mutable host filesystem behavior, or impractical interpreter workloads are gated or excluded at the narrowest applicable level. - **Shutdown lifecycle:** the CoreCLR browser shutdown wrapper is installed from Emscripten `preRun`, after native exports are bound, so explicit `Environment.Exit` values are preserved and no finalization work is scheduled while the runtime exits. - **Background GC information test:** the browser CoreCLR `GetGCMemoryInfo` test carries an ActiveIssue for [#131766](#131766), which tracks the assertion from requesting `GCKind.Background` when background GC is not compiled. This PR does not change the product behavior. - **Test assumptions:** focused adaptations remove incidental process, threading, filesystem, and host-tool assumptions while preserving the behavior each test is intended to cover. - **Post-merge CI coverage:** build 1528104 exposed 30 deterministic OOP failures after newer `main` changes activated more browser scenarios. The eager-fixup lock-order failure is fixed on `main` by [#131355](#131355). Composite WebCIL loading and background-GC information remain tracked separately by #131767 and #131766. The `Int128` field-layout correctness fix is proceeding in [#131421](#131421); other unsupported cases are gated only for CoreCLR browser-WASM with explicit re-enable prerequisites. No shipping public API is added. ## Deferred scope Per-test native assets, UCO thunk generation, and native relinking are intentionally deferred. Tests that need them remain guarded by their existing tracking issues. The remaining non-multithreading/non-relinking exclusions were audited: 41 represent recoverable coverage and 16 exercise browser-inapplicable contracts. The recoverable set and its prerequisites are tracked by [#131321](#131321), with links from each suppression site. The newly exposed native-relinking test remains tracked by [#123946](#123946). ## Validation The following complete-suite results were recorded before the two product fixes were moved to their dedicated tracking issues: | CoreCLR browser-WASM run | Total | Passed | Skipped | Failed | | --- | ---: | ---: | ---: | ---: | | Priority 0 after the `main` merge | 3,970 | 3,412 | 558 | 0 | | Priority 0 OOP subset | 485 | 408 | 77 | 0 | | Earlier full priority 0+1 validation | 14,726 | 13,850 | 876 | 0 | | Earlier priority 0+1 OOP subset | 740 | 651 | 89 | 0 | Current-head validation after removing those fixes: - rebuilt the Checked CoreCLR browser-WASM runtime and libraries with zero warnings or errors; - cleanly built all ten affected browser test projects; - ran all ten generated wrappers against the unfixed runtime: every wrapper exited with the expected status and reported #131766 or #131767 through the OOP skip-status protocol, without either runtime assertion; - verified that the shared non-R2R browser `BasicTest` remains enabled; - verified on macOS that the R2R variant still forces Crossgen2, emits and validates its composite image, and passes. Additional earlier validation: - composite and non-composite WASM ReadyToRun output; - native `NativeLibraryTests` asset staging; - plan/import success, failure, filtering, striping, stale/malformed/missing records, environment-sensitive wrappers, and all-skipped children; - unset, empty, and non-empty plan/status environment settings; - direct browser-WASM `InterpreterTester` execution under Node; - generated Bash syntax and generated Batch structure/path conversion; - Checked browser runtime rebuild plus a fresh priority-0 payload and test layout after the `main` merge; - all 22 new gates across CoreCLR browser, WASI, desktop, and Mono browser evaluations, preserving the pre-existing Mono ReadyToRun exclusions; - the complete 72-runner Node run, including the ReadyToRun, base services, JIT, Loader, and tracing work items that failed in build 1528104. | Representative CoreCLR browser library suite | Total | Passed | Skipped | Failed | | --- | ---: | ---: | ---: | ---: | | `System.Threading.Timer.Tests` | 33 | 16 | 17 | 0 | | `System.Runtime.InteropServices.JavaScript.Tests` | 476 | 474 | 2 | 0 | | `System.Reflection.Emit.Tests` (Debug) | 2,028 | 2,017 | 11 | 0 | | `System.Runtime.Tests` (Debug) | 75,726 | 75,559 | 167 | 0 | The generated Batch path has been inspected and generated successfully but still requires execution on Windows CI. > [!NOTE] > This pull request description was updated with GitHub Copilot. --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 44f4b4ea-409c-4a52-89a0-f488a89488b9
…-1 tests (#131883) ## Summary PR #131110 enabled CoreCLR browser-WASM out-of-process runtime tests. The newly enabled priority-1 coverage exposed eight unique failures. Four were composite WebCIL/ReadyToRun failures tracked by #131767 and are fixed on `main` by #131354. This change removes their temporary `ActiveIssue` annotations and Crossgen2 bypasses, restoring the intended R2R coverage. The unrelated field-layout suppression for #131421 remains. The other four tests are narrowly gated: - `GC/API/GC/GetTotalAllocatedBytesServerGC` is excluded on CoreCLR browser, which does not provide Server GC. - `GC/API/GC/GetTotalMemoryConcurrent` uses `PlatformDetection.IsMultithreadingSupported` through `ConditionalFact`. - `baseservices/threading/regressions/2164/foreground-shutdown` retains its explicit `Main` and returns success before creating a thread when multithreading is unsupported. - `JIT/jit64/regress/ddb/113574` is excluded only on CoreCLR browser because its multi-billion-iteration optimizing-JIT workload exceeds the browser interpreter's CI timeout. The runtime capability checks skip single-threaded browser and WASI while preserving desktop and threaded-browser coverage. The Server GC and interpreter-workload exclusions are tracked in #131321. ## Testing Built the Checked browser runtime from `main`, including #131354, then built and ran the complete priority-1 browser-WASM Node suite: | Scope | Total | Passed | Failed | Skipped | | --- | ---: | ---: | ---: | ---: | | Full suite | 14,745 | 13,823 | 0 | 922 | | Out-of-process subset | 672 | 560 | 0 | 112 | The four former WebCIL/R2R failures now pass: - `Regressions/coreclr/GitHub_49826/test49826` - `Regressions/coreclr/GitHub_49982/test49982` - `readytorun/tests/genericsload/callgenericctor` - `readytorun/tests/genericsload/usegenericfield` Additional focused validation for the runtime multithreading gates: - 8 MSBuild evaluations across desktop, threaded browser, single-threaded browser, and single-threaded WASI; neither project is permanently marked unsupported. - 2 targeted Checked browser builds, both with 0 warnings and 0 errors. - 2 single-threaded browser wrapper runs, both passing with expected/actual exit code 100. - Native `foreground-shutdown` execution exited with 100 after 2.28 seconds, confirming that its foreground thread still keeps the process alive after `Main` returns. > [!NOTE] > This pull request was created with assistance from GitHub Copilot. Copilot-Session: 19e61792-38f9-4f80-8d38-3e3e12d1275e
Int128/UInt128were given a 16-byte alignment requirement on wasm by both the VM and crossgen2, annotated// sizeof(v128).Int128is not a v128 — the only 128-bit value type in WebAssembly isv128, andWasmLowering.IsWasmV128Typerecognizes onlyVector128<T>and a 128-bitVector<T>, soInt128uses the generic by-reference struct ABI.Every other alignment override in
CheckForSystemTypesexists because the managed type corresponds to a fundamental data type in the target ABI (Vector128<T>↔__m128, and so on).Int128has no counterpart on wasm, so it takes ordinary struct alignment — the 8 its twoulongfields produce. On the crossgen2 side that means routing wasm through the existing 32-bit path alongside ARM.Symptom
The 16 was observable as wrong codegen at the interpreter→R2R boundary:
Root cause
Wasm signature encoding spells a by-reference struct argument as
S<N>, andRaiseSignatureresolvesS16throughGetCachedStructOfSize, which keys only on size and keeps the first struct seen at that size. The thunk therefore used that struct's 8-byte alignment while the interpreter usedInt128's 16, placing the argument atargsBase+24instead ofargsBase+32.EqualsandCompareToread a shifted value;operator ==takes its operands differently and was unaffected.This also makes
S<N>sound rather than accidentally correct. Single-field structs wrapping a v128 are unwrapped byLowerTypeand encode asV, and a v128 field is the only source of 16-byte alignment on wasm — so onceInt128takes its natural alignment, nothing reachingS<N>requires more than 8.The VM and crossgen2 must change together or managed field layout desyncs.
Validation
Browser wasm, against builds with and without the change, rebuilding both
corerunand the R2R composite for each variant:Equals=False,CompareTo=±1on equal valuesSystem.Text.Json, theInt128-bearing classes, 1496 tests, run on a branch carrying the other outstanding wasm R2R fixes so the suite completes:Those 2 fail with R2R off in both arms — pre-existing and unrelated.
Microsoft.Bcl.Memory550/550 andSystem.Runtime.CompilerServices.Unsafe128/128, R2R on and off.Alternative considered
Encoding
Int128asVinstead does not work.Vis not an alignment tag — it selects a different calling convention, passing the value in a wasmv128local rather than by reference. The JIT has no notion ofInt128at all, so R2R code faults withRuntimeError: function signature mismatch. Confirmed experimentally.clangon wasm32 agrees with the by-reference treatment: it legalizes__int128into twoi64parameters rather than any 128-bit entity, and passes an equivalent 16-byte struct asbyval align 8._Alignof(__int128) == 16there is a memory-layout artifact, not a calling-convention requirement, since there is no 128-bit slot to align to.wasm-only by construction — the VM change is inside
#elif defined(TARGET_WASM)and the crossgen2 change addsTargetArchitecture.Wasm32to an existing condition.Note
This pull request was created with the assistance of GitHub Copilot.