From bf84e5ac9e9ed2c7929aa72bbe1b5c32925fc299 Mon Sep 17 00:00:00 2001 From: Anders Sundheim Date: Wed, 3 Dec 2025 13:23:12 -0800 Subject: [PATCH 1/8] use ArrayExpansion for fixed buffer fields --- .../ResultProvider/ArrayExpansionTests.cs | 54 ++++++++++++++ .../Helpers/InlineArrayHelpers.cs | 73 +++++++++++++++++++ .../ResultProvider/Helpers/TypeHelpers.cs | 5 ++ .../Source/ResultProvider/ResultProvider.cs | 5 +- .../MemberInfo/CustomAttributeDataImpl.cs | 18 +++-- .../Debugger/MemberInfo/TypeImpl.cs | 19 +---- .../Test/ResultProvider/SampleFixedBuffer.cs | 21 ++++++ 7 files changed, 171 insertions(+), 24 deletions(-) create mode 100644 src/ExpressionEvaluator/Core/Test/ResultProvider/SampleFixedBuffer.cs diff --git a/src/ExpressionEvaluator/CSharp/Test/ResultProvider/ArrayExpansionTests.cs b/src/ExpressionEvaluator/CSharp/Test/ResultProvider/ArrayExpansionTests.cs index d0c4ef0452412..fe0aada0783d5 100644 --- a/src/ExpressionEvaluator/CSharp/Test/ResultProvider/ArrayExpansionTests.cs +++ b/src/ExpressionEvaluator/CSharp/Test/ResultProvider/ArrayExpansionTests.cs @@ -5,6 +5,7 @@ #nullable disable using System; +using System.Reflection; using Microsoft.CodeAnalysis.ExpressionEvaluator; using Microsoft.VisualStudio.Debugger.Evaluation; using Roslyn.Test.Utilities; @@ -343,5 +344,58 @@ public void LazyExpansion() EvalResult(string.Format("[{0}]", indices2), "0", "byte", string.Format("{0}[{1}]", parenthesizedExpr, indices2))); } } + + /// + /// Validate that our helper is able to identify the compiler-generated fixed buffer types. + /// + [Fact] + public void IdentifyFixedBuffer() + { + // The mock DkmClrValue.GetArrayElement relies on casting RawValue `object` to `Array` but fixed buffers are not `Array`. + // We can get the first element of the generated type via the single defined field, + // but everything gets boxed coming out of reflection so we can't do unsafe reads to get at the rest of the elements. + // We can't cast `object` to our known SampleFixedBuffer type because that is the enclosing type that defines the field; + // the actual type that shows up in the `GetArrayElement` mock is the generated field type, something like SampleFixedBuffer+e__Buffer. + // All we can do with these testing limitations is to validate that our helper returns accurate information when it encounters a fixed buffer. + var instance = SampleFixedBuffer.Create(); + var fixedBuffer = CreateDkmClrValue(instance) + .GetMemberValue(nameof(SampleFixedBuffer.Buffer), (int)MemberTypes.Field, null, DefaultInspectionContext); + + // Validate the actual ResultProvider gives back an ArrayExpansion for our fixed buffer field + var dataItem = FormatResult("instance.Buffer", fixedBuffer).GetDataItem(); + Assert.IsAssignableFrom(dataItem.Expansion); + + // Directly validate the values are computed correctly + Assert.True(InlineArrayHelpers.TryGetFixedBufferInfo(fixedBuffer.Type.GetLmrType(), out var length, out var elementType)); + Assert.Equal(4, length); + Assert.Equal(typeof(byte).FullName, elementType.FullName); + + // Validate fixed buffer identification / expansion does not kick in for a nearly identical shape + var source = +@" +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +public unsafe struct Enclosing +{ + [CompilerGenerated] + [StructLayout(LayoutKind.Sequential, Size = 256)] + public struct AlmostFixedBuffer + { + public byte FixedElementField; + } + + // Missing the generated [FixedBuffer(Type, int)] attribute + public AlmostFixedBuffer Buffer; +}"; + + var assembly = GetUnsafeAssembly(source); + var type = assembly.GetType("Enclosing"); + var fakeValue = CreateDkmClrValue(Activator.CreateInstance(type)); + var fakeBuffer = fakeValue.GetMemberValue("Buffer", (int)MemberTypes.Field, null, DefaultInspectionContext); + var fakeDataItem = FormatResult("fake.Buffer", fakeBuffer).GetDataItem(); + Assert.IsNotAssignableFrom(fakeDataItem.Expansion); + Assert.False(InlineArrayHelpers.TryGetFixedBufferInfo(fakeBuffer.Type.GetLmrType(), out _, out _)); + } } } diff --git a/src/ExpressionEvaluator/Core/Source/ResultProvider/Helpers/InlineArrayHelpers.cs b/src/ExpressionEvaluator/Core/Source/ResultProvider/Helpers/InlineArrayHelpers.cs index 01f6a5d16d661..31d6c0bbb4d26 100644 --- a/src/ExpressionEvaluator/Core/Source/ResultProvider/Helpers/InlineArrayHelpers.cs +++ b/src/ExpressionEvaluator/Core/Source/ResultProvider/Helpers/InlineArrayHelpers.cs @@ -14,6 +14,7 @@ namespace Microsoft.CodeAnalysis.ExpressionEvaluator; internal static class InlineArrayHelpers { private const string InlineArrayAttributeName = "System.Runtime.CompilerServices.InlineArrayAttribute"; + private const string FixedBufferAttributeName = "System.Runtime.CompilerServices.FixedBufferAttribute"; public static bool TryGetInlineArrayInfo(Type t, out int arrayLength, [NotNullWhen(true)] out Type? tElementType) { @@ -58,4 +59,76 @@ public static bool TryGetInlineArrayInfo(Type t, out int arrayLength, [NotNullWh return true; } + + public static bool TryGetFixedBufferInfo(Type t, out int arrayLength, [NotNullWhen(true)] out Type? tElementType) + { + arrayLength = -1; + tElementType = null; + + // Fixed buffer types are compiler-generated and are nested within the struct that contains the fixed buffer field. + // They are structurally identical to [InlineArray] structs in that they have 1 field defined in metadata which is repeated `arrayLength` times. + // The main difference is that the attribute is applied to the generated field and not the type itself, so we have to look a little harder to find it. + // + // Example: + // internal unsafe struct Buffer + // { + // public fixed char fixedBuffer[128]; + // } + // + // Compiles into: + // + // internal struct Buffer + // { + // [StructLayout(LayoutKind.Sequential, Size = 256)] + // [CompilerGenerated] + // [UnsafeValueType] + // public struct e__FixedBuffer + // { + // public char FixedElementField; + // } + + // [FixedBuffer(typeof(char), 128)] + // public e__FixedBuffer fixedBuffer; + // } + + // Filter out shapes we know can't be fixed buffer types + if (!t.IsValueType || !t.IsLayoutSequential || t.IsGenericType) + { + return false; + } + + if (!t.IsNested || t.DeclaringType is not Type enclosingType) + { + return false; + } + + FieldInfo[] fields = enclosingType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly); + foreach (FieldInfo field in fields) + { + // Match the field whose type is the fixed buffer type and is decorated with [FixedBuffer(Type, int)] + if (field.FieldType.Equals(t)) + { + IList customAttributes = field.GetCustomAttributesData(); + foreach (var attribute in customAttributes) + { + if (FixedBufferAttributeName.Equals(attribute.Constructor?.DeclaringType?.FullName)) + { + var ctorParams = attribute.Constructor.GetParameters(); + if (ctorParams.Length == 2 && + ctorParams[0].ParameterType.IsReflectionType() && + ctorParams[1].ParameterType.IsInt32() && + attribute.ConstructorArguments.Count == 2 && + attribute.ConstructorArguments[0].Value is Type type && + attribute.ConstructorArguments[1].Value is int length) + { + tElementType = type; + arrayLength = length; + } + } + } + } + } + + return arrayLength > 0 && tElementType is not null; + } } diff --git a/src/ExpressionEvaluator/Core/Source/ResultProvider/Helpers/TypeHelpers.cs b/src/ExpressionEvaluator/Core/Source/ResultProvider/Helpers/TypeHelpers.cs index 35fa353196e0e..4f8f65c7b871e 100644 --- a/src/ExpressionEvaluator/Core/Source/ResultProvider/Helpers/TypeHelpers.cs +++ b/src/ExpressionEvaluator/Core/Source/ResultProvider/Helpers/TypeHelpers.cs @@ -373,6 +373,11 @@ internal static bool IsIDynamicMetaObjectProvider(this Type type) return false; } + internal static bool IsReflectionType(this Type type) + { + return type.IsMscorlibType("System", "Type"); + } + /// /// Returns type argument if the type is /// Nullable<T>, otherwise null. diff --git a/src/ExpressionEvaluator/Core/Source/ResultProvider/ResultProvider.cs b/src/ExpressionEvaluator/Core/Source/ResultProvider/ResultProvider.cs index 770923d603f10..545c1ee100b6d 100644 --- a/src/ExpressionEvaluator/Core/Source/ResultProvider/ResultProvider.cs +++ b/src/ExpressionEvaluator/Core/Source/ResultProvider/ResultProvider.cs @@ -1012,7 +1012,10 @@ internal Expansion GetTypeExpansion( return TupleExpansion.CreateExpansion(inspectionContext, declaredTypeAndInfo, value, cardinality); } - if (InlineArrayHelpers.TryGetInlineArrayInfo(runtimeType, out int inlineArrayLength, out Type inlineArrayElementType)) + int inlineArrayLength; + Type inlineArrayElementType; + if (InlineArrayHelpers.TryGetInlineArrayInfo(runtimeType, out inlineArrayLength, out inlineArrayElementType) || + InlineArrayHelpers.TryGetFixedBufferInfo(runtimeType, out inlineArrayLength, out inlineArrayElementType)) { // Inline arrays are always 1D, zero-based arrays. return ArrayExpansion.CreateExpansion( diff --git a/src/ExpressionEvaluator/Core/Test/ResultProvider/Debugger/MemberInfo/CustomAttributeDataImpl.cs b/src/ExpressionEvaluator/Core/Test/ResultProvider/Debugger/MemberInfo/CustomAttributeDataImpl.cs index 17299285e9443..3e7ec4528ec2a 100644 --- a/src/ExpressionEvaluator/Core/Test/ResultProvider/Debugger/MemberInfo/CustomAttributeDataImpl.cs +++ b/src/ExpressionEvaluator/Core/Test/ResultProvider/Debugger/MemberInfo/CustomAttributeDataImpl.cs @@ -41,14 +41,22 @@ public override IList ConstructorArguments private static CustomAttributeTypedArgument MakeTypedArgument(System.Reflection.CustomAttributeTypedArgument a) { var argumentType = (TypeImpl)a.ArgumentType; - if (!argumentType.IsArray) + if (argumentType.IsArray) + { + var reflectionValue = (ReadOnlyCollection)a.Value; + var lmrValue = new ReadOnlyCollection(reflectionValue.Select(MakeTypedArgument).ToList()); + + return new CustomAttributeTypedArgument(argumentType, lmrValue); + } + else if (argumentType.IsReflectionType()) + { + // LMR converts all Type attribute values to LMR type values - this mock should too + return new CustomAttributeTypedArgument(argumentType, new TypeImpl((System.Type)a.Value)); + } + else { return new CustomAttributeTypedArgument(argumentType, a.Value); } - - var reflectionValue = (ReadOnlyCollection)a.Value; - var lmrValue = new ReadOnlyCollection(reflectionValue.Select(MakeTypedArgument).ToList()); - return new CustomAttributeTypedArgument(argumentType, lmrValue); } } } diff --git a/src/ExpressionEvaluator/Core/Test/ResultProvider/Debugger/MemberInfo/TypeImpl.cs b/src/ExpressionEvaluator/Core/Test/ResultProvider/Debugger/MemberInfo/TypeImpl.cs index 3e93142b011ea..04c036e1bcd1b 100644 --- a/src/ExpressionEvaluator/Core/Test/ResultProvider/Debugger/MemberInfo/TypeImpl.cs +++ b/src/ExpressionEvaluator/Core/Test/ResultProvider/Debugger/MemberInfo/TypeImpl.cs @@ -350,24 +350,7 @@ public override Type MakePointerType() protected override System.Reflection.TypeAttributes GetAttributeFlagsImpl() { - System.Reflection.TypeAttributes result = 0; - if (this.Type.IsClass) - { - result |= System.Reflection.TypeAttributes.Class; - } - if (this.Type.IsInterface) - { - result |= System.Reflection.TypeAttributes.Interface; - } - if (this.Type.IsAbstract) - { - result |= System.Reflection.TypeAttributes.Abstract; - } - if (this.Type.IsSealed) - { - result |= System.Reflection.TypeAttributes.Sealed; - } - return result; + return this.Type.Attributes; } protected override bool IsValueTypeImpl() diff --git a/src/ExpressionEvaluator/Core/Test/ResultProvider/SampleFixedBuffer.cs b/src/ExpressionEvaluator/Core/Test/ResultProvider/SampleFixedBuffer.cs new file mode 100644 index 0000000000000..7475292ca2707 --- /dev/null +++ b/src/ExpressionEvaluator/Core/Test/ResultProvider/SampleFixedBuffer.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.CodeAnalysis.ExpressionEvaluator; + +internal unsafe struct SampleFixedBuffer +{ + public fixed byte Buffer[4]; + + public static SampleFixedBuffer Create() + { + SampleFixedBuffer val = default; + val.Buffer[0] = 0; + val.Buffer[1] = 1; + val.Buffer[2] = 2; + val.Buffer[3] = 3; + + return val; + } +} From 90dcfb472ab9aecdcb2e7afbe60ec30ebc1bee8b Mon Sep 17 00:00:00 2001 From: Anders Sundheim Date: Thu, 4 Dec 2025 13:41:57 -0800 Subject: [PATCH 2/8] break early --- .../Core/Source/ResultProvider/Helpers/InlineArrayHelpers.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/ExpressionEvaluator/Core/Source/ResultProvider/Helpers/InlineArrayHelpers.cs b/src/ExpressionEvaluator/Core/Source/ResultProvider/Helpers/InlineArrayHelpers.cs index 31d6c0bbb4d26..1b9a076de5f69 100644 --- a/src/ExpressionEvaluator/Core/Source/ResultProvider/Helpers/InlineArrayHelpers.cs +++ b/src/ExpressionEvaluator/Core/Source/ResultProvider/Helpers/InlineArrayHelpers.cs @@ -124,8 +124,13 @@ attribute.ConstructorArguments[0].Value is Type type && tElementType = type; arrayLength = length; } + + break; } } + + // There should only be one field matching this type if it is indeed a fixed buffer - in any case, don't bother checking more fields + break; } } From c660278b3be09ff704ce2d8eb29174beaec5fb2d Mon Sep 17 00:00:00 2001 From: Anders Sundheim Date: Fri, 5 Dec 2025 11:25:57 -0800 Subject: [PATCH 3/8] PR comments --- .../Helpers/InlineArrayHelpers.cs | 42 +++++++++---------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/src/ExpressionEvaluator/Core/Source/ResultProvider/Helpers/InlineArrayHelpers.cs b/src/ExpressionEvaluator/Core/Source/ResultProvider/Helpers/InlineArrayHelpers.cs index 1b9a076de5f69..147d991e8643f 100644 --- a/src/ExpressionEvaluator/Core/Source/ResultProvider/Helpers/InlineArrayHelpers.cs +++ b/src/ExpressionEvaluator/Core/Source/ResultProvider/Helpers/InlineArrayHelpers.cs @@ -2,10 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using BindingFlags = Microsoft.VisualStudio.Debugger.Metadata.BindingFlags; -using CustomAttributeData = Microsoft.VisualStudio.Debugger.Metadata.CustomAttributeData; using FieldInfo = Microsoft.VisualStudio.Debugger.Metadata.FieldInfo; using Type = Microsoft.VisualStudio.Debugger.Metadata.Type; @@ -16,26 +14,25 @@ internal static class InlineArrayHelpers private const string InlineArrayAttributeName = "System.Runtime.CompilerServices.InlineArrayAttribute"; private const string FixedBufferAttributeName = "System.Runtime.CompilerServices.FixedBufferAttribute"; - public static bool TryGetInlineArrayInfo(Type t, out int arrayLength, [NotNullWhen(true)] out Type? tElementType) + public static bool TryGetInlineArrayInfo(Type type, out int arrayLength, [NotNullWhen(true)] out Type? elementType) { arrayLength = -1; - tElementType = null; + elementType = null; - if (!t.IsValueType) + if (!type.IsValueType) { return false; } - IList customAttributes = t.GetCustomAttributesData(); - foreach (var attribute in customAttributes) + foreach (var attribute in type.GetCustomAttributesData()) { if (InlineArrayAttributeName.Equals(attribute.Constructor?.DeclaringType?.FullName)) { var ctorParams = attribute.Constructor.GetParameters(); if (ctorParams.Length == 1 && ctorParams[0].ParameterType.IsInt32() && - attribute.ConstructorArguments.Count == 1 && attribute.ConstructorArguments[0].Value is int length) + attribute.ConstructorArguments.Count == 1 && attribute.ConstructorArguments[0].Value is int ctorLengthArg) { - arrayLength = length; + arrayLength = ctorLengthArg; } } } @@ -46,10 +43,10 @@ public static bool TryGetInlineArrayInfo(Type t, out int arrayLength, [NotNullWh return false; } - FieldInfo[] fields = t.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly); + FieldInfo[] fields = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly); if (fields.Length == 1) { - tElementType = fields[0].FieldType; + elementType = fields[0].FieldType; } else { @@ -60,10 +57,10 @@ public static bool TryGetInlineArrayInfo(Type t, out int arrayLength, [NotNullWh return true; } - public static bool TryGetFixedBufferInfo(Type t, out int arrayLength, [NotNullWhen(true)] out Type? tElementType) + public static bool TryGetFixedBufferInfo(Type type, out int arrayLength, [NotNullWhen(true)] out Type? elementType) { arrayLength = -1; - tElementType = null; + elementType = null; // Fixed buffer types are compiler-generated and are nested within the struct that contains the fixed buffer field. // They are structurally identical to [InlineArray] structs in that they have 1 field defined in metadata which is repeated `arrayLength` times. @@ -92,12 +89,12 @@ public static bool TryGetFixedBufferInfo(Type t, out int arrayLength, [NotNullWh // } // Filter out shapes we know can't be fixed buffer types - if (!t.IsValueType || !t.IsLayoutSequential || t.IsGenericType) + if (!type.IsValueType || !type.IsLayoutSequential || type.IsGenericType) { return false; } - if (!t.IsNested || t.DeclaringType is not Type enclosingType) + if (!type.IsNested || type.DeclaringType is not Type enclosingType) { return false; } @@ -106,10 +103,9 @@ public static bool TryGetFixedBufferInfo(Type t, out int arrayLength, [NotNullWh foreach (FieldInfo field in fields) { // Match the field whose type is the fixed buffer type and is decorated with [FixedBuffer(Type, int)] - if (field.FieldType.Equals(t)) + if (field.FieldType.Equals(type)) { - IList customAttributes = field.GetCustomAttributesData(); - foreach (var attribute in customAttributes) + foreach (var attribute in field.GetCustomAttributesData()) { if (FixedBufferAttributeName.Equals(attribute.Constructor?.DeclaringType?.FullName)) { @@ -118,11 +114,11 @@ public static bool TryGetFixedBufferInfo(Type t, out int arrayLength, [NotNullWh ctorParams[0].ParameterType.IsReflectionType() && ctorParams[1].ParameterType.IsInt32() && attribute.ConstructorArguments.Count == 2 && - attribute.ConstructorArguments[0].Value is Type type && - attribute.ConstructorArguments[1].Value is int length) + attribute.ConstructorArguments[0].Value is Type ctorElementTypeArg && + attribute.ConstructorArguments[1].Value is int ctorLengthArg) { - tElementType = type; - arrayLength = length; + elementType = ctorElementTypeArg; + arrayLength = ctorLengthArg; } break; @@ -134,6 +130,6 @@ attribute.ConstructorArguments[0].Value is Type type && } } - return arrayLength > 0 && tElementType is not null; + return arrayLength > 0 && elementType is not null; } } From 1f438dc468596554140e7652b293f3d13da41ce5 Mon Sep 17 00:00:00 2001 From: Anders Sundheim Date: Fri, 5 Dec 2025 17:46:28 -0800 Subject: [PATCH 4/8] pattern matching, PR comments --- .../ResultProvider/Helpers/InlineArrayHelpers.cs | 13 +++++-------- .../Source/ResultProvider/Helpers/TypeHelpers.cs | 2 +- .../Debugger/MemberInfo/CustomAttributeDataImpl.cs | 2 +- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/ExpressionEvaluator/Core/Source/ResultProvider/Helpers/InlineArrayHelpers.cs b/src/ExpressionEvaluator/Core/Source/ResultProvider/Helpers/InlineArrayHelpers.cs index 147d991e8643f..9a91582ce1253 100644 --- a/src/ExpressionEvaluator/Core/Source/ResultProvider/Helpers/InlineArrayHelpers.cs +++ b/src/ExpressionEvaluator/Core/Source/ResultProvider/Helpers/InlineArrayHelpers.cs @@ -29,8 +29,8 @@ public static bool TryGetInlineArrayInfo(Type type, out int arrayLength, [NotNul if (InlineArrayAttributeName.Equals(attribute.Constructor?.DeclaringType?.FullName)) { var ctorParams = attribute.Constructor.GetParameters(); - if (ctorParams.Length == 1 && ctorParams[0].ParameterType.IsInt32() && - attribute.ConstructorArguments.Count == 1 && attribute.ConstructorArguments[0].Value is int ctorLengthArg) + if (ctorParams is [{ ParameterType: Type ctorParam1Type }] && ctorParam1Type.IsInt32() && + attribute.ConstructorArguments is [{ Value: int ctorLengthArg }]) { arrayLength = ctorLengthArg; } @@ -110,12 +110,9 @@ public static bool TryGetFixedBufferInfo(Type type, out int arrayLength, [NotNul if (FixedBufferAttributeName.Equals(attribute.Constructor?.DeclaringType?.FullName)) { var ctorParams = attribute.Constructor.GetParameters(); - if (ctorParams.Length == 2 && - ctorParams[0].ParameterType.IsReflectionType() && - ctorParams[1].ParameterType.IsInt32() && - attribute.ConstructorArguments.Count == 2 && - attribute.ConstructorArguments[0].Value is Type ctorElementTypeArg && - attribute.ConstructorArguments[1].Value is int ctorLengthArg) + if (ctorParams is [{ ParameterType: Type ctorParam1Type }, { ParameterType: Type ctorParam2Type }] && + ctorParam1Type.IsSystemType() && ctorParam2Type.IsInt32() && + attribute.ConstructorArguments is [{ Value: Type ctorElementTypeArg }, { Value: int ctorLengthArg }]) { elementType = ctorElementTypeArg; arrayLength = ctorLengthArg; diff --git a/src/ExpressionEvaluator/Core/Source/ResultProvider/Helpers/TypeHelpers.cs b/src/ExpressionEvaluator/Core/Source/ResultProvider/Helpers/TypeHelpers.cs index 4f8f65c7b871e..ed98c8df4759e 100644 --- a/src/ExpressionEvaluator/Core/Source/ResultProvider/Helpers/TypeHelpers.cs +++ b/src/ExpressionEvaluator/Core/Source/ResultProvider/Helpers/TypeHelpers.cs @@ -373,7 +373,7 @@ internal static bool IsIDynamicMetaObjectProvider(this Type type) return false; } - internal static bool IsReflectionType(this Type type) + internal static bool IsSystemType(this Type type) { return type.IsMscorlibType("System", "Type"); } diff --git a/src/ExpressionEvaluator/Core/Test/ResultProvider/Debugger/MemberInfo/CustomAttributeDataImpl.cs b/src/ExpressionEvaluator/Core/Test/ResultProvider/Debugger/MemberInfo/CustomAttributeDataImpl.cs index 3e7ec4528ec2a..747d7c9629ba1 100644 --- a/src/ExpressionEvaluator/Core/Test/ResultProvider/Debugger/MemberInfo/CustomAttributeDataImpl.cs +++ b/src/ExpressionEvaluator/Core/Test/ResultProvider/Debugger/MemberInfo/CustomAttributeDataImpl.cs @@ -48,7 +48,7 @@ private static CustomAttributeTypedArgument MakeTypedArgument(System.Reflection. return new CustomAttributeTypedArgument(argumentType, lmrValue); } - else if (argumentType.IsReflectionType()) + else if (argumentType.IsSystemType()) { // LMR converts all Type attribute values to LMR type values - this mock should too return new CustomAttributeTypedArgument(argumentType, new TypeImpl((System.Type)a.Value)); From 7c5400c3d806ae61caf5ad141b6742abb5140942 Mon Sep 17 00:00:00 2001 From: Anders Sundheim Date: Fri, 12 Dec 2025 14:08:53 -0800 Subject: [PATCH 5/8] PR feedback: match only on type, revert unecessary changes --- .../Symbols/Synthesized/GeneratedNames.cs | 2 + .../ResultProvider/ArrayExpansionTests.cs | 8 +- .../Helpers/InlineArrayHelpers.cs | 120 ++++++++++++------ .../ResultProvider/Helpers/TypeHelpers.cs | 5 - .../MemberInfo/CustomAttributeDataImpl.cs | 18 +-- .../Debugger/MemberInfo/TypeImpl.cs | 19 ++- 6 files changed, 112 insertions(+), 60 deletions(-) diff --git a/src/Compilers/CSharp/Portable/Symbols/Synthesized/GeneratedNames.cs b/src/Compilers/CSharp/Portable/Symbols/Synthesized/GeneratedNames.cs index a0aa922337d63..f22b10bcd4c6b 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Synthesized/GeneratedNames.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Synthesized/GeneratedNames.cs @@ -289,6 +289,8 @@ internal static string MakeFixedFieldImplementationName(string fieldName) { // the native compiler adds numeric digits at the end. Roslyn does not. Debug.Assert((char)GeneratedNameKind.FixedBufferField == 'e'); + + // note - the EE assumes this naming scheme when identifying generated fixed buffer types return "<" + fieldName + ">e__FixedBuffer"; } diff --git a/src/ExpressionEvaluator/CSharp/Test/ResultProvider/ArrayExpansionTests.cs b/src/ExpressionEvaluator/CSharp/Test/ResultProvider/ArrayExpansionTests.cs index fe0aada0783d5..6fe3e22b598af 100644 --- a/src/ExpressionEvaluator/CSharp/Test/ResultProvider/ArrayExpansionTests.cs +++ b/src/ExpressionEvaluator/CSharp/Test/ResultProvider/ArrayExpansionTests.cs @@ -379,14 +379,14 @@ public void IdentifyFixedBuffer() public unsafe struct Enclosing { [CompilerGenerated] + [UnsafeValueType] [StructLayout(LayoutKind.Sequential, Size = 256)] - public struct AlmostFixedBuffer + public struct e__FixedBuffer { public byte FixedElementField; } - - // Missing the generated [FixedBuffer(Type, int)] attribute - public AlmostFixedBuffer Buffer; + + public e__FixedBuffer Buffer; }"; var assembly = GetUnsafeAssembly(source); diff --git a/src/ExpressionEvaluator/Core/Source/ResultProvider/Helpers/InlineArrayHelpers.cs b/src/ExpressionEvaluator/Core/Source/ResultProvider/Helpers/InlineArrayHelpers.cs index 9a91582ce1253..4d81de54ee02c 100644 --- a/src/ExpressionEvaluator/Core/Source/ResultProvider/Helpers/InlineArrayHelpers.cs +++ b/src/ExpressionEvaluator/Core/Source/ResultProvider/Helpers/InlineArrayHelpers.cs @@ -2,37 +2,43 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; using BindingFlags = Microsoft.VisualStudio.Debugger.Metadata.BindingFlags; +using CustomAttributeData = Microsoft.VisualStudio.Debugger.Metadata.CustomAttributeData; using FieldInfo = Microsoft.VisualStudio.Debugger.Metadata.FieldInfo; using Type = Microsoft.VisualStudio.Debugger.Metadata.Type; +using TypeCode = Microsoft.VisualStudio.Debugger.Metadata.TypeCode; namespace Microsoft.CodeAnalysis.ExpressionEvaluator; internal static class InlineArrayHelpers { private const string InlineArrayAttributeName = "System.Runtime.CompilerServices.InlineArrayAttribute"; - private const string FixedBufferAttributeName = "System.Runtime.CompilerServices.FixedBufferAttribute"; + private const string CompilerGeneratedAttributeName = "System.Runtime.CompilerServices.CompilerGeneratedAttribute"; + private const string UnsafeValueTypeAttributeName = "System.Runtime.CompilerServices.UnsafeValueTypeAttribute"; - public static bool TryGetInlineArrayInfo(Type type, out int arrayLength, [NotNullWhen(true)] out Type? elementType) + public static bool TryGetInlineArrayInfo(Type t, out int arrayLength, [NotNullWhen(true)] out Type? tElementType) { arrayLength = -1; - elementType = null; + tElementType = null; - if (!type.IsValueType) + if (!t.IsValueType) { return false; } - foreach (var attribute in type.GetCustomAttributesData()) + IList customAttributes = t.GetCustomAttributesData(); + foreach (var attribute in customAttributes) { if (InlineArrayAttributeName.Equals(attribute.Constructor?.DeclaringType?.FullName)) { var ctorParams = attribute.Constructor.GetParameters(); - if (ctorParams is [{ ParameterType: Type ctorParam1Type }] && ctorParam1Type.IsInt32() && - attribute.ConstructorArguments is [{ Value: int ctorLengthArg }]) + if (ctorParams.Length == 1 && ctorParams[0].ParameterType.IsInt32() && + attribute.ConstructorArguments.Count == 1 && attribute.ConstructorArguments[0].Value is int length) { - arrayLength = ctorLengthArg; + arrayLength = length; } } } @@ -43,10 +49,10 @@ public static bool TryGetInlineArrayInfo(Type type, out int arrayLength, [NotNul return false; } - FieldInfo[] fields = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly); + FieldInfo[] fields = t.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly); if (fields.Length == 1) { - elementType = fields[0].FieldType; + tElementType = fields[0].FieldType; } else { @@ -64,7 +70,6 @@ public static bool TryGetFixedBufferInfo(Type type, out int arrayLength, [NotNul // Fixed buffer types are compiler-generated and are nested within the struct that contains the fixed buffer field. // They are structurally identical to [InlineArray] structs in that they have 1 field defined in metadata which is repeated `arrayLength` times. - // The main difference is that the attribute is applied to the generated field and not the type itself, so we have to look a little harder to find it. // // Example: // internal unsafe struct Buffer @@ -88,45 +93,86 @@ public static bool TryGetFixedBufferInfo(Type type, out int arrayLength, [NotNul // public e__FixedBuffer fixedBuffer; // } - // Filter out shapes we know can't be fixed buffer types - if (!type.IsValueType || !type.IsLayoutSequential || type.IsGenericType) + if (!type.IsValueType || GetStructLayoutAttribute(type) is not { Value: LayoutKind.Sequential, Size: int explicitStructSize }) { return false; } - if (!type.IsNested || type.DeclaringType is not Type enclosingType) + if (!type.Name.EndsWith(">e__FixedBuffer")) { return false; } - FieldInfo[] fields = enclosingType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly); - foreach (FieldInfo field in fields) + FieldInfo[] fields = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly); + if (fields.Length != 1) { - // Match the field whose type is the fixed buffer type and is decorated with [FixedBuffer(Type, int)] - if (field.FieldType.Equals(type)) - { - foreach (var attribute in field.GetCustomAttributesData()) - { - if (FixedBufferAttributeName.Equals(attribute.Constructor?.DeclaringType?.FullName)) - { - var ctorParams = attribute.Constructor.GetParameters(); - if (ctorParams is [{ ParameterType: Type ctorParam1Type }, { ParameterType: Type ctorParam2Type }] && - ctorParam1Type.IsSystemType() && ctorParam2Type.IsInt32() && - attribute.ConstructorArguments is [{ Value: Type ctorElementTypeArg }, { Value: int ctorLengthArg }]) - { - elementType = ctorElementTypeArg; - arrayLength = ctorLengthArg; - } - - break; - } - } + return false; + } - // There should only be one field matching this type if it is indeed a fixed buffer - in any case, don't bother checking more fields - break; + bool isCompilerGenerated = false; + bool isUnsafeValueType = false; + foreach (var attribute in type.GetCustomAttributesData()) + { + switch (attribute.Constructor.DeclaringType?.FullName) + { + case CompilerGeneratedAttributeName: + isCompilerGenerated = true; + break; + case UnsafeValueTypeAttributeName: + isUnsafeValueType = true; + break; + default: + break; } } + if (!isCompilerGenerated || !isUnsafeValueType) + { + return false; + } + + int elementSize = Type.GetTypeCode(fields[0].FieldType) switch + { + TypeCode.Boolean => sizeof(bool), + TypeCode.Byte => sizeof(byte), + TypeCode.SByte => sizeof(sbyte), + TypeCode.UInt16 => sizeof(ushort), + TypeCode.Int16 => sizeof(short), + TypeCode.Char => sizeof(char), + TypeCode.UInt32 => sizeof(uint), + TypeCode.Int32 => sizeof(int), + TypeCode.UInt64 => sizeof(ulong), + TypeCode.Int64 => sizeof(long), + TypeCode.Single => sizeof(float), + TypeCode.Double => sizeof(double), + _ => -1, + }; + + if (elementSize <= 0 || explicitStructSize % elementSize != 0) + { + return false; + } + + elementType = fields[0].FieldType; + arrayLength = explicitStructSize / elementSize; + return arrayLength > 0 && elementType is not null; } + + // LMR Type defaults to throwing on access to StructLayoutAttribute and is not virtual. + // This hack is a necessity to be able to test the use of StructLayoutAttribute with mocks derived from LMR Type. + // n.b. [StructLayout] does not appear as an attribute in metadata; it is burned into the class layout. + private static StructLayoutAttribute? GetStructLayoutAttribute(Type type) + { +#if NETSTANDARD + // Retail, cannot see mock TypeImpl + return type.StructLayoutAttribute; +#else + return type switch + { + TypeImpl mockType => mockType.Type.StructLayoutAttribute, + _ => type.StructLayoutAttribute + }; +#endif + } } diff --git a/src/ExpressionEvaluator/Core/Source/ResultProvider/Helpers/TypeHelpers.cs b/src/ExpressionEvaluator/Core/Source/ResultProvider/Helpers/TypeHelpers.cs index ed98c8df4759e..35fa353196e0e 100644 --- a/src/ExpressionEvaluator/Core/Source/ResultProvider/Helpers/TypeHelpers.cs +++ b/src/ExpressionEvaluator/Core/Source/ResultProvider/Helpers/TypeHelpers.cs @@ -373,11 +373,6 @@ internal static bool IsIDynamicMetaObjectProvider(this Type type) return false; } - internal static bool IsSystemType(this Type type) - { - return type.IsMscorlibType("System", "Type"); - } - /// /// Returns type argument if the type is /// Nullable<T>, otherwise null. diff --git a/src/ExpressionEvaluator/Core/Test/ResultProvider/Debugger/MemberInfo/CustomAttributeDataImpl.cs b/src/ExpressionEvaluator/Core/Test/ResultProvider/Debugger/MemberInfo/CustomAttributeDataImpl.cs index 747d7c9629ba1..17299285e9443 100644 --- a/src/ExpressionEvaluator/Core/Test/ResultProvider/Debugger/MemberInfo/CustomAttributeDataImpl.cs +++ b/src/ExpressionEvaluator/Core/Test/ResultProvider/Debugger/MemberInfo/CustomAttributeDataImpl.cs @@ -41,22 +41,14 @@ public override IList ConstructorArguments private static CustomAttributeTypedArgument MakeTypedArgument(System.Reflection.CustomAttributeTypedArgument a) { var argumentType = (TypeImpl)a.ArgumentType; - if (argumentType.IsArray) - { - var reflectionValue = (ReadOnlyCollection)a.Value; - var lmrValue = new ReadOnlyCollection(reflectionValue.Select(MakeTypedArgument).ToList()); - - return new CustomAttributeTypedArgument(argumentType, lmrValue); - } - else if (argumentType.IsSystemType()) - { - // LMR converts all Type attribute values to LMR type values - this mock should too - return new CustomAttributeTypedArgument(argumentType, new TypeImpl((System.Type)a.Value)); - } - else + if (!argumentType.IsArray) { return new CustomAttributeTypedArgument(argumentType, a.Value); } + + var reflectionValue = (ReadOnlyCollection)a.Value; + var lmrValue = new ReadOnlyCollection(reflectionValue.Select(MakeTypedArgument).ToList()); + return new CustomAttributeTypedArgument(argumentType, lmrValue); } } } diff --git a/src/ExpressionEvaluator/Core/Test/ResultProvider/Debugger/MemberInfo/TypeImpl.cs b/src/ExpressionEvaluator/Core/Test/ResultProvider/Debugger/MemberInfo/TypeImpl.cs index 04c036e1bcd1b..3e93142b011ea 100644 --- a/src/ExpressionEvaluator/Core/Test/ResultProvider/Debugger/MemberInfo/TypeImpl.cs +++ b/src/ExpressionEvaluator/Core/Test/ResultProvider/Debugger/MemberInfo/TypeImpl.cs @@ -350,7 +350,24 @@ public override Type MakePointerType() protected override System.Reflection.TypeAttributes GetAttributeFlagsImpl() { - return this.Type.Attributes; + System.Reflection.TypeAttributes result = 0; + if (this.Type.IsClass) + { + result |= System.Reflection.TypeAttributes.Class; + } + if (this.Type.IsInterface) + { + result |= System.Reflection.TypeAttributes.Interface; + } + if (this.Type.IsAbstract) + { + result |= System.Reflection.TypeAttributes.Abstract; + } + if (this.Type.IsSealed) + { + result |= System.Reflection.TypeAttributes.Sealed; + } + return result; } protected override bool IsValueTypeImpl() From e95ae44b508e4a51e0119c772a06560f2cf96dbd Mon Sep 17 00:00:00 2001 From: Anders Sundheim Date: Mon, 15 Dec 2025 14:09:47 -0800 Subject: [PATCH 6/8] check param count --- .../ResultProvider/Helpers/InlineArrayHelpers.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/ExpressionEvaluator/Core/Source/ResultProvider/Helpers/InlineArrayHelpers.cs b/src/ExpressionEvaluator/Core/Source/ResultProvider/Helpers/InlineArrayHelpers.cs index 4d81de54ee02c..86be0f45899cc 100644 --- a/src/ExpressionEvaluator/Core/Source/ResultProvider/Helpers/InlineArrayHelpers.cs +++ b/src/ExpressionEvaluator/Core/Source/ResultProvider/Helpers/InlineArrayHelpers.cs @@ -116,10 +116,18 @@ public static bool TryGetFixedBufferInfo(Type type, out int arrayLength, [NotNul switch (attribute.Constructor.DeclaringType?.FullName) { case CompilerGeneratedAttributeName: - isCompilerGenerated = true; + if (attribute.Constructor.GetParameters().Length == 0) + { + isCompilerGenerated = true; + } + break; case UnsafeValueTypeAttributeName: - isUnsafeValueType = true; + if (attribute.Constructor.GetParameters().Length == 0) + { + isUnsafeValueType = true; + } + break; default: break; From 3cdfd9db50402360bc9f94dc5ddd71ec4a7d34d1 Mon Sep 17 00:00:00 2001 From: Anders Sundheim Date: Mon, 15 Dec 2025 16:07:48 -0800 Subject: [PATCH 7/8] kicking build policies after flaky failure --- .../Core/Source/ResultProvider/Helpers/InlineArrayHelpers.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ExpressionEvaluator/Core/Source/ResultProvider/Helpers/InlineArrayHelpers.cs b/src/ExpressionEvaluator/Core/Source/ResultProvider/Helpers/InlineArrayHelpers.cs index 86be0f45899cc..c186228726a32 100644 --- a/src/ExpressionEvaluator/Core/Source/ResultProvider/Helpers/InlineArrayHelpers.cs +++ b/src/ExpressionEvaluator/Core/Source/ResultProvider/Helpers/InlineArrayHelpers.cs @@ -71,7 +71,7 @@ public static bool TryGetFixedBufferInfo(Type type, out int arrayLength, [NotNul // Fixed buffer types are compiler-generated and are nested within the struct that contains the fixed buffer field. // They are structurally identical to [InlineArray] structs in that they have 1 field defined in metadata which is repeated `arrayLength` times. // - // Example: + // Example: // internal unsafe struct Buffer // { // public fixed char fixedBuffer[128]; From f8ae7add8da5aef131a3bf7e8dd8d5a2a198ccc3 Mon Sep 17 00:00:00 2001 From: Anders Sundheim Date: Fri, 19 Dec 2025 17:29:48 -0800 Subject: [PATCH 8/8] move suffix to common const string --- .../CSharp/Portable/Symbols/Synthesized/GeneratedNames.cs | 4 +--- src/Compilers/Core/Portable/Symbols/CommonGeneratedNames.cs | 2 ++ .../Core/Source/ResultProvider/Helpers/InlineArrayHelpers.cs | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Compilers/CSharp/Portable/Symbols/Synthesized/GeneratedNames.cs b/src/Compilers/CSharp/Portable/Symbols/Synthesized/GeneratedNames.cs index f22b10bcd4c6b..9dfc1cd2a1a25 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Synthesized/GeneratedNames.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Synthesized/GeneratedNames.cs @@ -289,9 +289,7 @@ internal static string MakeFixedFieldImplementationName(string fieldName) { // the native compiler adds numeric digits at the end. Roslyn does not. Debug.Assert((char)GeneratedNameKind.FixedBufferField == 'e'); - - // note - the EE assumes this naming scheme when identifying generated fixed buffer types - return "<" + fieldName + ">e__FixedBuffer"; + return "<" + fieldName + CommonGeneratedNames.FixedBufferFieldSuffix; } internal static string MakeStateMachineStateFieldName() diff --git a/src/Compilers/Core/Portable/Symbols/CommonGeneratedNames.cs b/src/Compilers/Core/Portable/Symbols/CommonGeneratedNames.cs index 70468fc996bd2..5cd00de870530 100644 --- a/src/Compilers/Core/Portable/Symbols/CommonGeneratedNames.cs +++ b/src/Compilers/Core/Portable/Symbols/CommonGeneratedNames.cs @@ -7,4 +7,6 @@ namespace Microsoft.CodeAnalysis.Symbols; internal static partial class CommonGeneratedNames { public const char GenerationSeparator = '#'; + + public const string FixedBufferFieldSuffix = ">e__FixedBuffer"; } diff --git a/src/ExpressionEvaluator/Core/Source/ResultProvider/Helpers/InlineArrayHelpers.cs b/src/ExpressionEvaluator/Core/Source/ResultProvider/Helpers/InlineArrayHelpers.cs index c186228726a32..d58b2fdddad5f 100644 --- a/src/ExpressionEvaluator/Core/Source/ResultProvider/Helpers/InlineArrayHelpers.cs +++ b/src/ExpressionEvaluator/Core/Source/ResultProvider/Helpers/InlineArrayHelpers.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using Microsoft.CodeAnalysis.Symbols; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; @@ -98,7 +99,7 @@ public static bool TryGetFixedBufferInfo(Type type, out int arrayLength, [NotNul return false; } - if (!type.Name.EndsWith(">e__FixedBuffer")) + if (!type.Name.EndsWith(CommonGeneratedNames.FixedBufferFieldSuffix)) { return false; }