diff --git a/src/Compilers/CSharp/Portable/Symbols/Synthesized/GeneratedNames.cs b/src/Compilers/CSharp/Portable/Symbols/Synthesized/GeneratedNames.cs
index a0aa922337d63..9dfc1cd2a1a25 100644
--- a/src/Compilers/CSharp/Portable/Symbols/Synthesized/GeneratedNames.cs
+++ b/src/Compilers/CSharp/Portable/Symbols/Synthesized/GeneratedNames.cs
@@ -289,7 +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');
- 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/CSharp/Test/ResultProvider/ArrayExpansionTests.cs b/src/ExpressionEvaluator/CSharp/Test/ResultProvider/ArrayExpansionTests.cs
index d0c4ef0452412..6fe3e22b598af 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]
+ [UnsafeValueType]
+ [StructLayout(LayoutKind.Sequential, Size = 256)]
+ public struct e__FixedBuffer
+ {
+ public byte FixedElementField;
+ }
+
+ public e__FixedBuffer 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..d58b2fdddad5f 100644
--- a/src/ExpressionEvaluator/Core/Source/ResultProvider/Helpers/InlineArrayHelpers.cs
+++ b/src/ExpressionEvaluator/Core/Source/ResultProvider/Helpers/InlineArrayHelpers.cs
@@ -2,18 +2,23 @@
// 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;
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 CompilerGeneratedAttributeName = "System.Runtime.CompilerServices.CompilerGeneratedAttribute";
+ private const string UnsafeValueTypeAttributeName = "System.Runtime.CompilerServices.UnsafeValueTypeAttribute";
public static bool TryGetInlineArrayInfo(Type t, out int arrayLength, [NotNullWhen(true)] out Type? tElementType)
{
@@ -58,4 +63,125 @@ public static bool TryGetInlineArrayInfo(Type t, out int arrayLength, [NotNullWh
return true;
}
+
+ public static bool TryGetFixedBufferInfo(Type type, out int arrayLength, [NotNullWhen(true)] out Type? elementType)
+ {
+ arrayLength = -1;
+ 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.
+ //
+ // 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;
+ // }
+
+ if (!type.IsValueType || GetStructLayoutAttribute(type) is not { Value: LayoutKind.Sequential, Size: int explicitStructSize })
+ {
+ return false;
+ }
+
+ if (!type.Name.EndsWith(CommonGeneratedNames.FixedBufferFieldSuffix))
+ {
+ return false;
+ }
+
+ FieldInfo[] fields = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly);
+ if (fields.Length != 1)
+ {
+ return false;
+ }
+
+ bool isCompilerGenerated = false;
+ bool isUnsafeValueType = false;
+ foreach (var attribute in type.GetCustomAttributesData())
+ {
+ switch (attribute.Constructor.DeclaringType?.FullName)
+ {
+ case CompilerGeneratedAttributeName:
+ if (attribute.Constructor.GetParameters().Length == 0)
+ {
+ isCompilerGenerated = true;
+ }
+
+ break;
+ case UnsafeValueTypeAttributeName:
+ if (attribute.Constructor.GetParameters().Length == 0)
+ {
+ 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/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/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;
+ }
+}