Skip to content

Handle Decimal32/64/128 ABI packing in the VM and AOT type system#131345

Open
tannergooding wants to merge 3 commits into
dotnet:mainfrom
tannergooding:tannergooding-decimal-abi-packing
Open

Handle Decimal32/64/128 ABI packing in the VM and AOT type system#131345
tannergooding wants to merge 3 commits into
dotnet:mainfrom
tannergooding:tannergooding-decimal-abi-packing

Conversation

@tannergooding

Copy link
Copy Markdown
Member

System.Numerics.Decimal32/64/128 were added without any of the special ABI handling that Int128/UInt128 get, so:

This mirrors the existing Int128 handling across the VM, the shared crossgen2/ILCompiler type system, and the interop marshallers, using a parallel IsDecimalFloatingPointOrHasDecimalFloatingPointFields flag rather than reusing the Int128 one -- a decimal isn't an Int128, the diagnostic text differs, and reusing it would be confusing next to System.Decimal. Existing Int128 paths are untouched.

Two separable pieces:


The types are now marked [Intrinsic], matching Int128/UInt128, so DecimalFieldLayoutAlgorithm.IsDecimalFloatingPointType can require type.IsIntrinsic exactly like Int128FieldLayoutAlgorithm.IsIntegerType does. I audited every IsIntrinsicType() consumer in the VM and every isIntrinsicType/CORINFO_FLG_INTRINSIC_TYPE site in the JIT; all are exact-name gated except two, and both mattered:

  • classlayoutinfo.cpp GetNativeLayoutInfo copies the managed alignment to the native alignment for Int128/UInt128/Vector*, gated on IsIntrinsicType(). Without [Intrinsic] this was unreachable for the decimals, so a marshaled struct containing a Decimal128 would have gotten 8 byte native alignment against its 16 byte managed alignment. Added CLASS__DECIMAL128.
  • jitinterface.cpp GetTypeLayoutHelper sets simdTypeHnd and stops enumerating fields for any intrinsic type in System.Numerics -- a namespace-only check that held only because every such type was SIMD until now. The decimals would have been reported with numFields = 0, and TryPromoteValueClassAsPrimitive would then bail, silently losing struct promotion for e.g. a Decimal64 field that promotes as TYP_LONG today. Excluded via the new flag, which combined with IsIntrinsicType() identifies exactly the three types (a user struct merely containing a decimal isn't intrinsic).

On the platform split for the 16 byte alignment: ARM32 gets 8, matching Int128, since its PCS has no such primitive. x64/x86 get 16 (_Decimal128 is 16 byte aligned in the x86-64 psABI). Wasm gets 16, but not for the sizeof(v128) reason the Int128 branch cites -- clang has no _Decimal128 at all, so the Wasm Basic C ABI doesn't define one; 16 matches __int128_t, the only other 16 byte scalar it does define, which stays 16 byte aligned even under Emscripten (Emscripten only reduces long double to 8). The comment says so rather than repeating the inherited rationale.

Decimal128's #if BIGENDIAN _upper/_lower swap was already present and matches Int128. Mono has no Int128 special-casing, so nothing is owed there. [StructLayout(LayoutKind.Sequential)] was added to Decimal64/Decimal128, which Decimal32 and Int128 already carried; no ref assembly change, since neither Int128 nor Decimal128 declares it in the ref and Roslyn defaults structs to sequential either way.


New interop tests under src/tests/Interop/PInvoke/Decimal128:

  • TestDecimal128FieldLayout -- 16 byte alignment round-trip, byref/out marshalling, and the by-value MarshalDirectiveException both directly and struct-wrapped.
  • TestDecimal32And64MarshalRestriction -- the marshal restriction for the other two.

The native side treats Decimal128 as alignas(16) struct { uint64_t lower, upper; } and adds the raw halves, since MSVC has no _Decimal128 either. The managed side builds and reads known bit patterns through Unsafe.As<UInt128, Decimal128>, as Decimal128 has no public bits constructor; that's endian-safe because both types use the same #if BIGENDIAN field swap.

Validation: clr and libs.pretest build clean (0 warnings / 0 errors); the new interop tests pass (exit code 100); System.Runtime.Tests Decimal32/Decimal64/Decimal128 -- 7148 total, 0 failed. No src/coreclr/jit files changed, so jit-format doesn't apply.

CC. @dotnet/jit-contrib @dotnet/interop-contrib

Note

This pull request description was drafted by GitHub Copilot.

The IEEE 754 decimal types had no special ABI handling, so Decimal128 defaulted
to 8 byte alignment rather than the 16 byte alignment of the _Decimal128 ABI
primitive, and by-value interop passing hit the same unimplemented JIT path as
Int128/UInt128. Mirror the Int128 handling with a parallel flag, and mark the
types [Intrinsic] so the layout algorithm and native alignment propagation can
key off it the same way.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 4 pipeline(s).
12 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/interop-contrib
See info in area-owners.md if you want to be subscribed.

@tannergooding

tannergooding commented Jul 24, 2026

Copy link
Copy Markdown
Member Author

CC. @lewing, @jkotas, @jkoritzinsky. We added the Decimal32/64/128 types in .NET 11 and they are ABI primitives with special packing and ABI conventions, so as has been standard we're ensuring the managed packing is correct and then blocking them from crossing the P/Invoke boundary until the right classification can be enabled for all platforms (namely classifying as SSE/SSEUP on SysV).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Updates CoreCLR + NativeAOT type system + interop marshalling to treat System.Numerics.Decimal32/64/128 as ABI-special types: enforce 16-byte packing for Decimal128 where appropriate and block by-value P/Invoke marshalling (throwing MarshalDirectiveException) for all three, mirroring existing Int128/UInt128 handling.

Changes:

  • Mark Decimal32/64/128 as [Intrinsic] and ensure layout metadata matches runtime expectations.
  • Add a new “decimal floating-point (or has decimal fields)” flag across VM and shared type system, using it to (a) adjust Decimal128 alignment and (b) reject by-value marshalling.
  • Add new interop tests and native test asset for Decimal128 alignment + by-value marshalling restriction coverage.
Show a summary per file
File Description
src/tests/Interop/PInvoke/Decimal128/Decimal128Test.cs Adds managed interop tests for Decimal128 layout + by-value restriction, plus Decimal32/64 restriction checks
src/tests/Interop/PInvoke/Decimal128/Decimal128Native.cpp Native side helpers to validate field layout/alignment and host dummy exports for restriction tests
src/tests/Interop/PInvoke/Decimal128/CMakeLists.txt Builds the new native interop test library
src/tests/Interop/Interop.csproj Includes new Decimal128 test sources in the merged wrapper build
src/tests/Interop/CMakeLists.txt Adds the Decimal128 native test subdirectory to the interop native build
src/libraries/System.Private.CoreLib/src/System/Numerics/Decimal32.cs Marks Decimal32 as intrinsic (and confirms sequential layout attribute)
src/libraries/System.Private.CoreLib/src/System/Numerics/Decimal64.cs Marks Decimal64 as intrinsic and adds explicit sequential layout
src/libraries/System.Private.CoreLib/src/System/Numerics/Decimal128.cs Marks Decimal128 as intrinsic and adds explicit sequential layout
src/coreclr/vm/mlinfo.cpp Blocks by-value marshalling for decimal floating-point types/containers with a dedicated resource ID
src/coreclr/vm/methodtablebuilder.cpp Tracks decimal-floating-point containment and sets Decimal128 alignment requirement by target
src/coreclr/vm/methodtable.inl Adds MethodTable::IsDecimalFloatingPointOrHasDecimalFloatingPointFields() helper
src/coreclr/vm/methodtable.h Declares the new MethodTable query helper
src/coreclr/vm/jitinterface.cpp Ensures intrinsic decimal types in System.Numerics still report fields to the JIT layout API
src/coreclr/vm/dllimport.cpp Forces marshaling-required for decimal floating-point types/containers (so restriction triggers)
src/coreclr/vm/corelib.h Adds CoreLib binder entry for Decimal128 for native layout handling
src/coreclr/vm/classnames.h Adds Decimal32/64/128 name constants for VM type recognition
src/coreclr/vm/classlayoutinfo.cpp Propagates decimal containment via nested-field flags and aligns Decimal128 native layout with managed
src/coreclr/vm/class.h Adds EEClassLayoutInfo and EEClass flag plumbing for decimal containment
src/coreclr/tools/Common/TypeSystem/Interop/IL/MarshalHelpers.cs Rejects by-value marshalling in ILCompiler type system for decimal-floating-point types/containers
src/coreclr/tools/Common/TypeSystem/Common/MetadataFieldLayoutAlgorithm.cs Propagates the decimal containment bit through computed layouts
src/coreclr/tools/Common/TypeSystem/Common/FieldLayoutAlgorithm.cs Extends computed layout struct with decimal containment flag
src/coreclr/tools/Common/TypeSystem/Common/DefType.FieldLayout.cs Adds a persistent layout flag + public query property for decimal containment
src/coreclr/tools/Common/Compiler/DecimalFieldLayoutAlgorithm.cs New layout algorithm for Decimal32/64/128; applies 16-byte alignment for Decimal128 where needed
src/coreclr/tools/aot/ILCompiler.TypeSystem.Tests/TestTypeSystemContext.cs Wires DecimalFieldLayoutAlgorithm into the test type system context
src/coreclr/tools/aot/ILCompiler.TypeSystem.Tests/ILCompiler.TypeSystem.Tests.csproj Includes DecimalFieldLayoutAlgorithm in type system tests compilation
src/coreclr/tools/aot/ILCompiler.ReadyToRun/ILCompiler.ReadyToRun.csproj Includes DecimalFieldLayoutAlgorithm in R2R build
src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCompilerContext.cs Selects decimal layout algorithm for decimal floating-point intrinsic types
src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj Includes DecimalFieldLayoutAlgorithm in AOT compiler build
src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/CompilerTypeSystemContext.Aot.cs Selects decimal layout algorithm for decimal floating-point intrinsic types
src/coreclr/dlls/mscorrc/resource.h Adds new resource ID for the decimal by-value marshalling restriction
src/coreclr/dlls/mscorrc/mscorrc.rc Adds the user-facing error string for the restriction

Copilot's findings

  • Files reviewed: 31/31 changed files
  • Comments generated: 1

Comment thread src/tests/Interop/PInvoke/Decimal128/Decimal128Native.cpp
The alignas(16) was guarded on MSVC-only _M_* macros, so on Linux/macOS the
native Decimal128 kept 8 byte alignment while the managed side used 16, moving
the field offset in StructWithDecimal128 and breaking the byref layout check.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 24, 2026 20:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 31/31 changed files
  • Comments generated: 1

Comment thread src/tests/Interop/PInvoke/Decimal128/Decimal128Test.cs
Mono implements neither the 16-byte packing for ABI primitive types nor the
by-value marshaling restriction, so both tests fail there. Covered by the same
issue used for the equivalent Int128 test.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 25, 2026 00:15
@tannergooding

Copy link
Copy Markdown
Member Author

Had to apply the same "skip on mono" label we have for the other interop tests and updated #69399 to include it in the list

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 31/31 changed files
  • Comments generated: 1

Comment thread src/tests/Interop/PInvoke/Decimal128/Decimal128Test.cs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants