From 39f0ffdf616580e51662d1092d32c68012d7bb39 Mon Sep 17 00:00:00 2001 From: Valentino Saitz Date: Wed, 24 Jun 2026 09:27:15 +0200 Subject: [PATCH] Add decimal support to automation ComVariant marshalling (#21634) NumericUpDown.Value is a decimal?, so changing it while a UI Automation client is attached raised an automation property-changed event whose boxed decimal hit no case in ComVariant.Create and threw an unhandled ArgumentException that crashed the process. The event only fires when a UIA client is listening, so it reproduced only on "some machines". Mirror the BCL System.Runtime.InteropServices.Marshalling.ComVariant (which this type is based on): overlay a decimal field on the variant, write it before setting the VT_DECIMAL discriminator in Create (the discriminator overlaps the decimal's unused leading bytes), and clear that discriminator on a copy before reading the value back in AsObject. Fixes #21491 --- .../Marshalling/ComVariant.cs | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/Windows/Avalonia.Win32.Automation/Marshalling/ComVariant.cs b/src/Windows/Avalonia.Win32.Automation/Marshalling/ComVariant.cs index 0fa78f300e..5c851c6e7d 100644 --- a/src/Windows/Avalonia.Win32.Automation/Marshalling/ComVariant.cs +++ b/src/Windows/Avalonia.Win32.Automation/Marshalling/ComVariant.cs @@ -39,9 +39,17 @@ internal struct ComVariant : IDisposable // Most of the data types in the Variant are carried in _typeUnion [FieldOffset(0)] private TypeUnion _typeUnion; + // Decimal is the largest data type and it needs to use the space that is normally unused in + // TypeUnion._wReserved1, etc. Hence, it is declared to completely overlap with TypeUnion. A + // Decimal does not use the first two bytes, and so TypeUnion._vt can still be used to encode the + // type. (Win32 automation is Windows-only, i.e. always little-endian, so TypeUnion's field order + // already lines up with Decimal._flags and no big-endian special-casing is required.) + [FieldOffset(0)] private decimal _decimal; + [StructLayout(LayoutKind.Sequential)] private struct TypeUnion { + // The layout of _wReserved1 and _vt fields needs to match Decimal._flags. public ushort _vt; public ushort _wReserved1; public ushort _wReserved2; @@ -161,6 +169,15 @@ internal struct ComVariant : IDisposable variant.VarType = VarEnum.VT_R8; variant._typeUnion._unionTypes._r8 = (double)value; } + else if (value is decimal) + { + // Set the value first and then the type, as the decimal storage overlaps the whole + // variant (including the type discriminator, which lands in decimal's unused first two + // bytes). NumericUpDown.Value is a decimal?, so its automation property-changed events + // arrive here; without this branch they throw and crash the app (#21491). + variant._decimal = (decimal)value; + variant.VarType = VarEnum.VT_DECIMAL; + } else if (value is DateTime) { variant.VarType = VarEnum.VT_DATE; @@ -260,6 +277,8 @@ internal struct ComVariant : IDisposable // floating VarEnum.VT_R4 => _typeUnion._unionTypes._r4, VarEnum.VT_R8 => _typeUnion._unionTypes._r8, + // decimal + VarEnum.VT_DECIMAL => GetDecimal(), // date VarEnum.VT_DATE => DateTime.FromOADate(_typeUnion._unionTypes._date), // string @@ -296,6 +315,19 @@ internal struct ComVariant : IDisposable }; } + /// + /// Read the stored in a variant. The + /// decimal storage overlaps the entire variant, so the type discriminator currently sits in the + /// decimal's otherwise-unused first two bytes; clear it on a copy (by setting the type to + /// ) so the VT_DECIMAL flag doesn't leak into the returned value. + /// + private readonly decimal GetDecimal() + { + var copy = this; + copy.VarType = VarEnum.VT_EMPTY; + return copy._decimal; + } + /// /// The type of the data stored in this . ///