Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#nullable disable

using System;
using System.Reflection;
using Microsoft.CodeAnalysis.ExpressionEvaluator;
using Microsoft.VisualStudio.Debugger.Evaluation;
using Roslyn.Test.Utilities;
Expand Down Expand Up @@ -343,5 +344,58 @@ public void LazyExpansion()
EvalResult(string.Format("[{0}]", indices2), "0", "byte", string.Format("{0}[{1}]", parenthesizedExpr, indices2)));
}
}

/// <summary>
/// Validate that our helper is able to identify the compiler-generated fixed buffer types.
/// </summary>
[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+<Buffer>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<EvalResultDataItem>();
Assert.IsAssignableFrom<ArrayExpansion>(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<EvalResultDataItem>();
Assert.IsNotAssignableFrom<ArrayExpansion>(fakeDataItem.Expansion);
Assert.False(InlineArrayHelpers.TryGetFixedBufferInfo(fakeBuffer.Type.GetLmrType(), out _, out _));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,20 @@

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)
{
Expand Down Expand Up @@ -58,4 +62,117 @@ 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 <fixedBuffer>e__FixedBuffer
// {
// public char FixedElementField;
// }

// [FixedBuffer(typeof(char), 128)]
// public <fixedBuffer>e__FixedBuffer fixedBuffer;
// }

if (!type.IsValueType || GetStructLayoutAttribute(type) is not { Value: LayoutKind.Sequential, Size: int explicitStructSize })
{
return false;
}

if (!type.Name.EndsWith(">e__FixedBuffer"))
{
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:
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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}