diff --git a/docs/design/coreclr/botr/readytorun-format.md b/docs/design/coreclr/botr/readytorun-format.md index a6b6c8f11817ec..2ab5c55c3dbbda 100644 --- a/docs/design/coreclr/botr/readytorun-format.md +++ b/docs/design/coreclr/botr/readytorun-format.md @@ -1077,6 +1077,7 @@ The string format is: | `d` | returns `f64` | | `V` | returns `v128` (a `Vector128`, or a 16-byte `Vector`) | | `S` | struct return via hidden buffer, `N` is the struct size in bytes | +| `A` | as `S`, but the struct's argument slot requires 16-byte alignment | **This pointer** (if the method has a `this` parameter): @@ -1091,8 +1092,8 @@ The string format is: 2. **Async continuation** (`a`): present for async calls. Note: the hidden return buffer pointer is **not** encoded in the signature string. Its -presence is implied by the return type being `S` — when the caller sees a struct return, -it knows a hidden retbuf pointer argument is present in the Wasm parameter list. +presence is implied by the return type being `S` or `A` — when the caller sees a struct +return, it knows a hidden retbuf pointer argument is present in the Wasm parameter list. **Explicit parameters** (one token per parameter, in declaration order): @@ -1104,6 +1105,7 @@ it knows a hidden retbuf pointer argument is present in the Wasm parameter list. | `d` | `f64` parameter | | `V` | `v128` parameter (a `Vector128`, or a 16-byte `Vector`, passed by value) | | `S` | struct parameter passed by reference, `` is the struct size in bytes | +| `A` | as `S`, but the struct's argument slot requires 16-byte alignment | | `e` | empty struct parameter — elided from Wasm args but present in the string | | `` | multi-slot parameter passed by value, see below | @@ -1127,15 +1129,23 @@ reads it. So `ll2VV4` is `i64`, `Int128`, `Vector128`, `Vector512`. The gr unambiguous because no other token places a digit after a slot character; `S` consumes its own digits. -These types are still *returned* through a hidden buffer, encoded as `S` like any other -aggregate. Limitation: a single digit carries both the slot count and the elevation factor, -so an aggregate whose elevation differs from its slot count has no spelling. That includes -one whose alignment is merely natural for its slot type, which would need count `N` with -elevation 1. No such type exists in the Wasm ABI today. +These types are still *returned* through a hidden buffer, encoded like any other aggregate: +`A`, since each of them clamps to 16-byte alignment. Limitation: a single digit carries +both the slot count and the elevation factor, so an aggregate whose elevation differs from +its slot count has no spelling. That includes one whose alignment is merely natural for its +slot type, which would need count `N` with elevation 1. No such type exists in the Wasm ABI +today. WasmAppBuilder does not emit or consume multi-slot tokens: they do not appear in `InternalCall` or `PInvoke` signatures. +A struct's argument slot in the transition block is aligned to the struct's own alignment, +clamped to `[8, 16]`. Because thunks are keyed by (and shared across modules by) the signature +string alone, that alignment must be part of the encoding: a 32-byte struct of `long`s and a +32-byte struct of `Int128`s have different argument layouts and cannot share a thunk. Structs +whose alignment clamps to 16 (those containing `Int128`/`UInt128` or a 128-bit vector) use the +`A` token; all others use `S`. + **Suffix**: | Encoding | Meaning | diff --git a/src/coreclr/inc/jiteeversionguid.h b/src/coreclr/inc/jiteeversionguid.h index d922f09ff4025d..dec00fdd70916a 100644 --- a/src/coreclr/inc/jiteeversionguid.h +++ b/src/coreclr/inc/jiteeversionguid.h @@ -37,11 +37,11 @@ #include -constexpr GUID JITEEVersionIdentifier = { /* aa3cece7-a5f9-4b4e-9309-852c8bd4bdb1 */ - 0xaa3cece7, - 0xa5f9, - 0x4b4e, - {0x93, 0x09, 0x85, 0x2c, 0x8b, 0xd4, 0xbd, 0xb1} +constexpr GUID JITEEVersionIdentifier = { /* 5203056d-9af9-4dc8-a134-56a53f27e274 */ + 0x5203056d, + 0x9af9, + 0x4dc8, + {0xa1, 0x34, 0x56, 0xa5, 0x3f, 0x27, 0xe2, 0x74} }; #endif // JIT_EE_VERSIONING_GUID_H diff --git a/src/coreclr/tools/Common/Compiler/CompilerTypeSystemContext.Wasm.cs b/src/coreclr/tools/Common/Compiler/CompilerTypeSystemContext.Wasm.cs index 1117c09066d94d..f59ff804d16a32 100644 --- a/src/coreclr/tools/Common/Compiler/CompilerTypeSystemContext.Wasm.cs +++ b/src/coreclr/tools/Common/Compiler/CompilerTypeSystemContext.Wasm.cs @@ -13,8 +13,7 @@ namespace ILCompiler public partial class CompilerTypeSystemContext { private readonly object _structCacheLock = new object(); - private readonly Dictionary _structsBySize = new Dictionary(); - private readonly Dictionary _returnStructsBySize = new Dictionary(); + private readonly Dictionary<(int Size, int Alignment), TypeDesc> _structsBySizeAndAlignment = new Dictionary<(int, int), TypeDesc>(); private volatile TypeDesc _cachedEmptyStruct; private volatile TypeDesc _wasmV128Type; private volatile TypeDesc _wasmInt128Type; @@ -90,10 +89,11 @@ public void CacheEmptyStruct(TypeDesc type) } /// - /// Caches a struct type by its element size, so RaiseSignature can retrieve a real - /// type of that size. Only the first struct encountered for a given size is retained. + /// Caches a struct type by its element size and argument alignment, so RaiseSignature can + /// retrieve a real type with the same Wasm argument layout. Only the first struct + /// encountered for a given (size, alignment) pair is retained. /// - public void CacheStructBySize(TypeDesc type) + public void CacheStructBySize(TypeDesc type, int alignment) { int size = type.GetElementSize().AsInt; if (size <= 0) @@ -101,50 +101,20 @@ public void CacheStructBySize(TypeDesc type) lock (_structCacheLock) { - _structsBySize.TryAdd(size, type); + _structsBySizeAndAlignment.TryAdd((size, alignment), type); } } /// - /// Caches a struct return type by size. Kept apart from the parameter cache because the two - /// classes are not interchangeable: a multi-slot type spells S<N> as a return but - /// re-lowers to its slot form as a parameter, so letting one answer for the other would give - /// an ordinary same-sized struct that type's larger alignment. + /// Gets a previously cached struct type of the specified byte size and argument alignment. + /// Returns null if no such struct has been cached. + /// Used by RaiseSignature to produce a roundtrippable type for the 'S<N>'/'A<N>' encodings. /// - public void CacheReturnStructBySize(TypeDesc type) - { - int size = type.GetElementSize().AsInt; - if (size <= 0) - return; - - lock (_structCacheLock) - { - _returnStructsBySize.TryAdd(size, type); - } - } - - /// - /// Gets a previously cached struct return type of the specified byte size, falling back to a - /// parameter of that size. Returns null if neither has been cached. - /// - public TypeDesc GetCachedReturnStructOfSize(int size) - { - lock (_structCacheLock) - { - if (_returnStructsBySize.TryGetValue(size, out TypeDesc result)) - { - return result; - } - } - - return GetCachedStructOfSize(size); - } - - public TypeDesc GetCachedStructOfSize(int size) + public TypeDesc GetCachedStructOfSize(int size, int alignment) { lock (_structCacheLock) { - if (_structsBySize.TryGetValue(size, out TypeDesc result)) + if (_structsBySizeAndAlignment.TryGetValue((size, alignment), out TypeDesc result)) return result; } diff --git a/src/coreclr/tools/Common/JitInterface/WasmLowering.cs b/src/coreclr/tools/Common/JitInterface/WasmLowering.cs index 40fe08a2265e98..55588481dde319 100644 --- a/src/coreclr/tools/Common/JitInterface/WasmLowering.cs +++ b/src/coreclr/tools/Common/JitInterface/WasmLowering.cs @@ -409,10 +409,11 @@ public static WasmValueType LowerType(TypeDesc type) _ => throw new InvalidOperationException($"Unknown signature char: {c}") }; - private static int ParseStructSize(string sig, ref int pos) + private static int ParseStructSize(string sig, ref int pos, out int align) { - Debug.Assert(sig[pos] == 'S'); - pos++; // skip 'S' + Debug.Assert(sig[pos] is 'S' or 'A'); + align = (sig[pos] == 'A') ? WideStructArgAlignment : DefaultStructArgAlignment; + pos++; // skip 'S'/'A' int start = pos; while (pos < sig.Length && char.IsDigit(sig[pos])) { @@ -421,6 +422,38 @@ private static int ParseStructSize(string sig, ref int pos) return int.Parse(sig.AsSpan(start, pos - start)); } + // Wasm passes struct arguments in transition block slots aligned to the struct's own + // alignment, clamped to [8, 16]. This must match ArgIterator's Wasm32 case. + // + private const int DefaultStructArgAlignment = 8; + private const int WideStructArgAlignment = 16; + + /// + /// Gets the alignment a struct argument or return type requires in the transition block. + /// Mirrors the Wasm32 case of ArgIterator and of the native ArgIteratorTemplate. + /// + public static int GetStructArgAlignment(TypeDesc type) + { + int alignment = ((DefType)type).InstanceFieldAlignment.AsInt; + return Math.Clamp(alignment, DefaultStructArgAlignment, WideStructArgAlignment); + } + + /// + /// Returns true if starts a struct token ('S<N>' or 'A<N>'). + /// + public static bool IsStructToken(char c) => c is 'S' or 'A'; + + // Appends the 'S'/'A' token for a struct passed by reference, and caches the type so + // RaiseSignature can recover a type with the same argument layout. + // + private static void AppendStructToken(StringBuilder sigBuilder, TypeDesc type) + { + int alignment = GetStructArgAlignment(type); + sigBuilder.Append(alignment == WideStructArgAlignment ? 'A' : 'S'); + sigBuilder.Append(type.GetElementSize().AsInt); + ((CompilerTypeSystemContext)type.Context).CacheStructBySize(type, alignment); + } + public static MethodSignature RaiseSignature(WasmSignature wasmSignature, TypeSystemContext context) { string sig = wasmSignature.SignatureString; @@ -433,11 +466,11 @@ public static MethodSignature RaiseSignature(WasmSignature wasmSignature, TypeSy returnType = context.GetWellKnownType(WellKnownType.Void); pos++; } - else if (sig[pos] == 'S') + else if (sig[pos] is 'S' or 'A') { - int structSize = ParseStructSize(sig, ref pos); - returnType = ((CompilerTypeSystemContext)context).GetCachedReturnStructOfSize(structSize); - Debug.Assert(returnType is not null, $"No cached struct of size {structSize} for return type in signature '{sig}'"); + int structSize = ParseStructSize(sig, ref pos, out int structAlign); + returnType = ((CompilerTypeSystemContext)context).GetCachedStructOfSize(structSize, structAlign); + Debug.Assert(returnType is not null, $"No cached struct of size {structSize} and alignment {structAlign} for return type in signature '{sig}'"); } else { @@ -496,11 +529,11 @@ public static MethodSignature RaiseSignature(WasmSignature wasmSignature, TypeSy parameters.Add(((CompilerTypeSystemContext)context).GetWasmElevatedType(c, elevation)); pos += 2; } - else if (c == 'S') + else if (IsStructToken(c)) { - int structSize = ParseStructSize(sig, ref pos); - TypeDesc cachedStruct = ((CompilerTypeSystemContext)context).GetCachedStructOfSize(structSize); - Debug.Assert(cachedStruct is not null, $"No cached struct of size {structSize} for parameter in signature '{sig}'"); + int structSize = ParseStructSize(sig, ref pos, out int structAlign); + TypeDesc cachedStruct = ((CompilerTypeSystemContext)context).GetCachedStructOfSize(structSize, structAlign); + Debug.Assert(cachedStruct is not null, $"No cached struct of size {structSize} and alignment {structAlign} for parameter in signature '{sig}'"); parameters.Add(cachedStruct); } else @@ -611,19 +644,7 @@ public static WasmSignature GetSignature(MethodSignature signature, LoweringFlag { hasReturnBuffer = true; returnIsVoid = true; - int returnSize = returnType.GetElementSize().AsInt; - sigBuilder.Append('S'); - sigBuilder.Append(returnSize); - - // A multi-slot type spells 'S' only as a return; as a parameter it re-lowers - // to its slot form. Keep it in the return cache alone, so an ordinary same-sized - // struct parameter does not raise with this type's larger alignment. - CompilerTypeSystemContext returnContext = (CompilerTypeSystemContext)returnType.Context; - returnContext.CacheReturnStructBySize(returnType); - if (!TryGetMultiSegmentLayout(returnType, out _, out _)) - { - returnContext.CacheStructBySize(returnType); - } + AppendStructToken(sigBuilder, returnType); } } else if (loweredReturnType.IsVoid) @@ -701,7 +722,6 @@ public static WasmSignature GetSignature(MethodSignature signature, LoweringFlag } // Struct that cannot be lowered to a single primitive — passed by reference - int paramSize = paramType.GetElementSize().AsInt; if (TryGetMultiSegmentLayout(paramType, out WasmValueType slotType, out int slotCount)) { // Passed by value across several wasm parameters, matching the wasm C ABI. @@ -719,9 +739,7 @@ public static WasmSignature GetSignature(MethodSignature signature, LoweringFlag } else { - sigBuilder.Append('S'); - sigBuilder.Append(paramSize); - ((CompilerTypeSystemContext)paramType.Context).CacheStructBySize(paramType); + AppendStructToken(sigBuilder, paramType); result.Add(pointerType); } } diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/WasmArgumentLayoutTests.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/WasmArgumentLayoutTests.cs index e15b25117f46d0..fbdcbb5bd84523 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/WasmArgumentLayoutTests.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/WasmArgumentLayoutTests.cs @@ -283,9 +283,10 @@ public void NarrowVectorsAreNotMultiSlot(string vectorType, int expectedSize, st } /// - /// A multi-slot type is returned through a hidden buffer and so spells S<N>, but it - /// must not be remembered as the type that encoding raises to: it re-lowers to its multi-slot - /// form, so an ordinary same-sized struct parameter would raise with the wrong alignment. + /// A multi-slot type is returned through a hidden buffer, so it takes a struct token rather + /// than its slot form. Its argument alignment is part of that token, so an ordinary struct of + /// the same size — which is only 8-byte aligned — spells a different one and raises with its + /// own layout rather than the multi-slot type's. /// [Theory] [InlineData(Int128Type, 2)] @@ -297,10 +298,13 @@ public void MultiSlotReturnDoesNotPoisonTheStructSizeCache(string typeName, int // Lower a method whose return is the multi-slot type; this is what caches by size. DefType multiSlot = InstantiateMultiSlotType(context, typeName); - WasmLowering.GetSignature( + WasmSignature returnSignature = WasmLowering.GetSignature( new MethodSignature(MethodSignatureFlags.Static, 0, multiSlot, Array.Empty()), WasmLowering.LoweringFlags.None); + // Every multi-slot type clamps to 16-byte alignment, so the return takes the wide token. + Assert.Equal($"A{multiSlot.InstanceFieldSize.AsInt}p", returnSignature.SignatureString); + // Now an ordinary struct of the same size, which is only 8-byte aligned. MetadataType tupleType = fieldCount == 2 ? context.SystemModule.GetType("System"u8, "ValueTuple`2"u8) @@ -462,9 +466,10 @@ public void SingleFieldStructIsPassedAsTheTypeItWraps(string typeName, string ex Assert.Equal(expectedSignature, SignatureOf(context, wrapper)); Assert.Equal(OffsetsOf(context, wrapped), OffsetsOf(context, wrapper)); - // A second field makes it an ordinary aggregate, so it goes back to the struct ABI. + // A second field makes it an ordinary aggregate, so it goes back to the struct ABI. Two + // vector fields still leave it over-aligned, so it takes the wide-alignment token. DefType twoFields = MakeValueTuple(context, wrapped, wrapped); - Assert.Equal($"vlS{twoFields.InstanceFieldSize.AsInt}ip", SignatureOf(context, twoFields)); + Assert.Equal($"vlA{twoFields.InstanceFieldSize.AsInt}ip", SignatureOf(context, twoFields)); } /// @@ -482,6 +487,29 @@ public void SingleFieldStructWrappingAMultiSlotScalarKeepsItsSlots() Assert.Equal(OffsetsOf(context, int128), OffsetsOf(context, wrapper)); } + /// + /// A thunk is keyed by its signature string, so two structs of the same size must not spell the + /// same token unless they also agree on argument alignment. The over-aligned one is padded to 16 + /// in the transition block and the other is not, so one thunk cannot describe both. + /// + [Fact] + public void SameSizedStructsOfDifferentAlignmentGetDifferentTokens() + { + ReadyToRunCompilerContext context = CreateWasmContext(); + TypeDesc int64 = context.GetWellKnownType(WellKnownType.Int64); + DefType vector = InstantiateVector(context, Vector128OfT, WellKnownType.Int32); + + DefType wideAligned = MakeValueTuple(context, vector, vector); + DefType slotAligned = MakeValueTuple(context, int64, int64, int64, int64); + + Assert.Equal(wideAligned.InstanceFieldSize.AsInt, slotAligned.InstanceFieldSize.AsInt); + Assert.NotEqual(wideAligned.InstanceFieldAlignment.AsInt, slotAligned.InstanceFieldAlignment.AsInt); + + Assert.Equal("vlA32ip", SignatureOf(context, wideAligned)); + Assert.Equal("vlS32ip", SignatureOf(context, slotAligned)); + Assert.NotEqual(OffsetsOf(context, wideAligned), OffsetsOf(context, slotAligned)); + } + private static DefType MakeValueTuple(ReadyToRunCompilerContext context, params TypeDesc[] fields) => ((MetadataType)context.SystemModule.GetType( "System"u8, System.Text.Encoding.UTF8.GetBytes($"ValueTuple`{fields.Length}"))) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmImportThunk.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmImportThunk.cs index 57b4e39c5f9c75..4a3208ae30a3da 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmImportThunk.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmImportThunk.cs @@ -105,7 +105,7 @@ private bool HasGenericContextBeforeAsync } int pos = 1; - if (_wasmSignature.SignatureString[0] == 'S') + if (WasmLowering.IsStructToken(_wasmSignature.SignatureString[0])) { while ((pos < _wasmSignature.SignatureString.Length) && char.IsDigit(_wasmSignature.SignatureString[pos])) { @@ -161,7 +161,7 @@ protected override void EmitCode(NodeFactory factory, ref Wasm.WasmEmitter instr int[] offsets = new int[methodSignature.Length]; bool[] isIndirectStructArg = new bool[methodSignature.Length]; - bool hasRetBuffArg = _wasmSignature.SignatureString[0] == 'S'; + bool hasRetBuffArg = WasmLowering.IsStructToken(_wasmSignature.SignatureString[0]); int argIndex = 0; int argOffset; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmInterpreterToR2RThunkNode.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmInterpreterToR2RThunkNode.cs index 326430ce3ab4b4..0a5c3b7c681d47 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmInterpreterToR2RThunkNode.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmInterpreterToR2RThunkNode.cs @@ -53,7 +53,7 @@ private bool HasGenericContextBeforeAsync } int pos = 1; - if (_wasmSignature.SignatureString[0] == 'S') + if (WasmLowering.IsStructToken(_wasmSignature.SignatureString[0])) { while ((pos < _wasmSignature.SignatureString.Length) && char.IsDigit(_wasmSignature.SignatureString[pos])) { @@ -118,7 +118,7 @@ protected override void EmitCode(NodeFactory factory, ref Wasm.WasmEmitter instr MethodSignature methodSignature = WasmLowering.RaiseSignature(_wasmSignature, _context); (ArgIterator argit, TransitionBlock transitionBlock) = GCRefMapBuilder.BuildArgIterator(methodSignature, _context, methodIsAsyncCall: hasAsyncContinuation); - bool hasRetBuffArg = _wasmSignature.SignatureString[0] == 'S'; + bool hasRetBuffArg = WasmLowering.IsStructToken(_wasmSignature.SignatureString[0]); bool hasThis = !methodSignature.IsStatic; bool hasGenericContextBeforeAsync = HasGenericContextBeforeAsync; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmR2RToInterpreterThunkNode.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmR2RToInterpreterThunkNode.cs index 73c63ebe2e841d..14ca5c1dbe3801 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmR2RToInterpreterThunkNode.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmR2RToInterpreterThunkNode.cs @@ -62,7 +62,7 @@ private bool HasGenericContextBeforeAsync } int pos = 1; - if (_wasmSignature.SignatureString[0] == 'S') + if (WasmLowering.IsStructToken(_wasmSignature.SignatureString[0])) { while ((pos < _wasmSignature.SignatureString.Length) && char.IsDigit(_wasmSignature.SignatureString[pos])) { @@ -128,7 +128,7 @@ protected override void EmitCode(NodeFactory factory, ref Wasm.WasmEmitter instr bool hasGenericContextBeforeAsync = HasGenericContextBeforeAsync; (ArgIterator argit, TransitionBlock transitionBlock) = GCRefMapBuilder.BuildArgIterator(methodSignature, _context, methodIsAsyncCall: hasAsyncContinuation); - bool hasRetBuffArg = _wasmSignature.SignatureString[0] == 'S'; + bool hasRetBuffArg = WasmLowering.IsStructToken(_wasmSignature.SignatureString[0]); bool hasThis = !methodSignature.IsStatic; int[] offsets = new int[methodSignature.Length]; diff --git a/src/coreclr/vm/wasm/helpers.cpp b/src/coreclr/vm/wasm/helpers.cpp index 6eeec621843304..94947a59fd8649 100644 --- a/src/coreclr/vm/wasm/helpers.cpp +++ b/src/coreclr/vm/wasm/helpers.cpp @@ -1049,14 +1049,15 @@ namespace ToV128, ToSlotsI64, // Passed by value as several i64 slots (Int128/UInt128) ToSlotsV128, // Passed by value as several v128 slots (Vector256, Vector512) - ToStruct, // S — multi-field struct passed by pointer, structSize holds the size + ToStruct, // S/A — multi-field struct passed by pointer, structSize holds the size ToEmpty, // e — empty struct, takes no wasm argument }; struct ConvertResult { ConvertType type; - uint32_t structSize; // only meaningful when type == ToStruct + uint32_t structSize; // only meaningful when type == ToStruct + uint32_t structAlign; // only meaningful when type == ToStruct }; // Lowers a TypeHandle to a ConvertResult, unwrapping single-field structs @@ -1237,7 +1238,13 @@ namespace // One field with padding — treat as multi-field struct } - return { ConvertType::ToStruct, size }; + // The transition block slot for a struct argument is aligned to the struct's own alignment, + // clamped to [INTERP_STACK_SLOT_SIZE, INTERP_STACK_ALIGNMENT]. This must be part of the + // signature key, since it is part of the argument layout the thunk bakes in. + uint32_t align = std::clamp(CEEInfo::getClassAlignmentRequirementStatic(pMT), + INTERP_STACK_SLOT_SIZE, INTERP_STACK_ALIGNMENT); + + return { ConvertType::ToStruct, size, align }; } ConvertResult ConvertibleTo(CorElementType argType, MetaSig& sig, bool isReturn) @@ -1316,9 +1323,10 @@ namespace case ConvertType::ToEmpty: c = 'e'; break; case ConvertType::ToStruct: { - // Encode as S where N is the struct size in decimal + // Encode as S, or A when the argument slot needs 16-byte alignment char sizeBuf[16]; - int len = sprintf_s(sizeBuf, sizeof(sizeBuf), "S%u", cr.structSize); + int len = sprintf_s(sizeBuf, sizeof(sizeBuf), "%c%u", + (cr.structAlign == INTERP_STACK_ALIGNMENT) ? 'A' : 'S', cr.structSize); for (int j = 0; j < len; j++) { if (pos + (uint32_t)j < maxSize) diff --git a/src/tasks/WasmAppBuilder/coreclr/SignatureMapper.cs b/src/tasks/WasmAppBuilder/coreclr/SignatureMapper.cs index e6c3e4d0ee1cec..87bed5ecdeba19 100644 --- a/src/tasks/WasmAppBuilder/coreclr/SignatureMapper.cs +++ b/src/tasks/WasmAppBuilder/coreclr/SignatureMapper.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -15,20 +15,23 @@ namespace Microsoft.WebAssembly.Build.Tasks.CoreClr; // (section "Wasm Signature String Encoding"). internal static class SignatureMapper { - // Hardcoded struct sizes for types that crossgen2 encodes as S. - // The fully general case is handled by crossgen2's type system; these - // cover the small set of multi-field structs that appear in InternalCall - // and PInvoke signatures. - private static readonly Dictionary s_knownStructSizes = new() + // Hardcoded struct sizes and transition-block argument alignments for types that crossgen2 + // encodes as S (alignment 8) or A (alignment 16). The fully general case is handled by + // crossgen2's type system; these cover the small set of multi-field structs that appear in + // InternalCall and PInvoke signatures. The alignment is the struct's own alignment clamped + // to [8, 16], and must match crossgen2's WasmLowering.GetStructArgAlignment. Note that + // TokenToSlotCount does not yet model 16-byte alignment, so adding an entry with alignment 16 + // requires teaching the generated thunk layout about it as well. + private static readonly Dictionary s_knownStructSizes = new() { - ["System.Runtime.CompilerServices.QCallModule"] = 8, - ["System.Runtime.CompilerServices.QCallAssembly"] = 8, - ["System.Runtime.CompilerServices.QCallTypeHandle"] = 8, - ["System.GC+GCHeapHardLimitInfo"] = 64, + ["System.Runtime.CompilerServices.QCallModule"] = (8, 8), + ["System.Runtime.CompilerServices.QCallAssembly"] = (8, 8), + ["System.Runtime.CompilerServices.QCallTypeHandle"] = (8, 8), + ["System.GC+GCHeapHardLimitInfo"] = (64, 8), // Used by WBT tests - ["WasmAppBuilderTestsPairStruct"] = 8, - ["WasmAppBuilderTests.S"] = 8, - ["WasmAppBuilderTests.Test+S"] = 8, + ["WasmAppBuilderTestsPairStruct"] = (8, 8), + ["WasmAppBuilderTests.S"] = (8, 8), + ["WasmAppBuilderTests.Test+S"] = (8, 8), }; internal static char? TypeToChar(Type t, LogAdapter log, out bool isByRefStruct, out int structSize, int depth = 0) @@ -98,9 +101,10 @@ internal static class SignatureMapper else { string fullName = t.FullName ?? t.Name; - if (s_knownStructSizes.TryGetValue(fullName, out int size)) + if (s_knownStructSizes.TryGetValue(fullName, out (int Size, int Alignment) info)) { - structSize = size; + structSize = info.Size; + c = (info.Alignment == 16) ? 'A' : 'S'; } else { @@ -108,8 +112,6 @@ internal static class SignatureMapper $"SignatureMapper: unknown multi-field struct '{fullName}' (fields: {fields.Length}) — add its size to s_knownStructSizes in SignatureMapper.cs"); return null; } - - c = 'S'; } isByRefStruct = true; @@ -125,7 +127,7 @@ internal static class SignatureMapper /// /// Builds the multi-char token for a type in the signature string. - /// For most types this is a single character; for multi-field structs it is "S<N>". + /// For most types this is a single character; for multi-field structs it is "S<N>"/"A<N>". /// private static string? TypeToSignatureToken(Type t, LogAdapter log, out bool isByRefStruct) { @@ -145,8 +147,8 @@ internal static class SignatureMapper if (c is null) return null; - if (c == 'S' && structSize > 0) - return $"S{structSize}"; + if (c is 'S' or 'A' && structSize > 0) + return $"{c}{structSize}"; return c.Value.ToString(); } @@ -253,7 +255,7 @@ private static bool HasNumericElementType(Type t) /// /// Parses a signature string into individual tokens. - /// Single-char types produce one-char tokens; S<N> produces a multi-char token like "S8" or "S64", + /// Single-char types produce one-char tokens; S<N>/A<N> produces a multi-char token like "S8" or "A32", /// and a multi-slot parameter produces a two-char token like "l2" or "V4". /// The 'a' and 'p' suffixes are included as their own tokens. /// @@ -263,10 +265,10 @@ public static List ParseSignatureTokens(string signature) int i = 0; while (i < signature.Length) { - if (signature[i] == 'S') + if (signature[i] is 'S' or 'A') { int start = i; - i++; // skip 'S' + i++; // skip 'S'/'A' while (i < signature.Length && char.IsDigit(signature[i])) i++; tokens.Add(signature.Substring(start, i - start)); @@ -309,7 +311,7 @@ public static string TokenToNativeType(string token) 'l' => "int64_t", 'f' => "float", 'd' => "double", - 'S' => "int32_t", + 'S' or 'A' => "int32_t", 'T' => "int32_t", 'p' => "PCODE", _ => throw new InvalidSignatureCharException(token[0]) @@ -326,7 +328,7 @@ public static string TokenToNameType(string token) 'l' => "I64", 'f' => "F32", 'd' => "F64", - 'S' => token, // e.g. "S8", "S64" — encodes size in the name + 'S' or 'A' => token, // e.g. "S8", "A32" — encodes size and alignment in the name 'T' => "This", 'p' => "PE", _ => throw new InvalidSignatureCharException(token[0]) @@ -342,7 +344,7 @@ public static string TokenToArgType(string token) 'l' => "ARG_I64", 'f' => "ARG_F32", 'd' => "ARG_F64", - 'S' => "ARG_IND", + 'S' or 'A' => "ARG_IND", 'T' => "ARG_I32", _ => throw new InvalidSignatureCharException(token[0]) }; @@ -354,7 +356,7 @@ public static string TokenToArgType(string token) /// public static int TokenToSlotCount(string token) { - if (token[0] != 'S' || token.Length < 2) + if (token[0] is not ('S' or 'A') || token.Length < 2) return 1; int size = int.Parse(token.Substring(1));