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; + } +}