Skip to content

Commit 382fbd3

Browse files
Use ArrayExpansion for [InlineArray] structs (#81254)
# Overview Currently [inline array structs](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-12.0/inline-arrays) use the default `MemberExpansion`, which only shows the first element / the single field: <img width="643" height="57" alt="image" src="https://github.com/user-attachments/assets/8388dda8-5b9c-4115-8a49-745fa3385e56" /> # Details - Add some helpers to detect structs with `[InlineArray]` attributes and decode the Length and element type - Adds a case in the ResultProvider to use `ArrayExpansion` for inline arrays # Testing The real complexity of this change came when trying to write a test that exercised this case. Since all the ResultProvider expansion tests rely on instantiating objects into the test host, then decoding them via reflection in the debugger engine mocks, some additional work was necessary to support the Net8+ inline array type. - Added a `$(NetRoslyn)` target to `ResultProvider.UnitTests` and `ResultProvider.Utilities` - Added an InlineArrayExpansion test that is only compiled under `$(NetRoslyn)` - Enabled `DkmClrValue` to handle `GetArrayElement` for inline arrays (ish, see comments about reflection and inline arrays) Fixes: #68983
1 parent b2585e5 commit 382fbd3

File tree

12 files changed

+214
-22
lines changed

12 files changed

+214
-22
lines changed

src/ExpressionEvaluator/CSharp/Test/ResultProvider/Microsoft.CodeAnalysis.CSharp.ResultProvider.UnitTests.csproj

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<OutputType>Library</OutputType>
66
<RootNamespace>Microsoft.CodeAnalysis.CSharp.ExpressionEvaluator</RootNamespace>
77
<AssemblyName>Microsoft.CodeAnalysis.CSharp.ExpressionEvaluator.ResultProvider.UnitTests</AssemblyName>
8-
<TargetFramework>net472</TargetFramework>
8+
<TargetFrameworks>net472;$(NetRoslyn)</TargetFrameworks>
99
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
1010
</PropertyGroup>
1111
<ItemGroup Label="Project References">
@@ -17,10 +17,25 @@
1717
<ProjectReference Include="..\..\..\Core\Test\ResultProvider\Microsoft.CodeAnalysis.ResultProvider.Utilities.csproj" />
1818
<ProjectReference Include="..\..\..\..\Test\PdbUtilities\Roslyn.Test.PdbUtilities.csproj" />
1919
</ItemGroup>
20-
<ItemGroup>
20+
<!--
21+
ResultProvider tests rely on dynamically instantiating objects into the test process via reflection.
22+
As some types cannot exist in .NET Framework (e.g. an [InlineArray] struct) we have to split our tests based on the
23+
target framework of the test project.
24+
-->
25+
<ItemGroup Condition="'$(TargetFramework)' == 'net472'">
2126
<Reference Include="Microsoft.CSharp" />
2227
<Reference Include="System" />
2328
<Reference Include="System.Xml" />
29+
<!--Don't compile NetCore tests-->
30+
<Compile Remove="NetCoreTests\**\*.cs"/>
31+
<!--Keep the folder visible in the IDE-->
32+
<None Include="NetCoreTests\**\*.cs"/>
33+
</ItemGroup>
34+
<ItemGroup Condition="'$(TargetFramework)' == '$(NetRoslyn)'">
35+
<!--Only compile the tests under NetCoreTests-->
36+
<Compile Remove="**\*.cs" />
37+
<Compile Include="CSharpResultProviderTestBase.cs"/>
38+
<Compile Include="NetCoreTests\**\*.cs" />
2439
</ItemGroup>
2540
<ItemGroup>
2641
<Compile Include="..\..\..\..\Compilers\CSharp\Portable\SymbolDisplay\ObjectDisplay.cs">
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using Microsoft.CodeAnalysis.ExpressionEvaluator;
6+
using Microsoft.VisualStudio.Debugger.Evaluation;
7+
using Xunit;
8+
9+
namespace Microsoft.CodeAnalysis.CSharp.ExpressionEvaluator.UnitTests.NetCoreTests;
10+
11+
public class InlineArrayExpansionTests : CSharpResultProviderTestBase
12+
{
13+
[Fact]
14+
public void InlineArrayExpansion()
15+
{
16+
var hostObject = new SampleInlineArray<int>();
17+
for (int i = 0; i < SampleInlineArray<int>.Length; i++)
18+
{
19+
hostObject[i] = i;
20+
}
21+
22+
var value = CreateDkmClrValue(hostObject, typeof(SampleInlineArray<int>), evalFlags: DkmEvaluationResultFlags.None);
23+
24+
const string rootExpr = "new SampleInlineArray<int>()";
25+
var evalResult = (DkmSuccessEvaluationResult)FormatResult(rootExpr, value);
26+
Verify(evalResult,
27+
EvalResult(rootExpr, "0,1,2,3", "Microsoft.CodeAnalysis.ExpressionEvaluator.SampleInlineArray<int>", rootExpr, DkmEvaluationResultFlags.Expandable));
28+
29+
Verify(GetChildren(evalResult),
30+
EvalResult("[0]", "0", "int", "(new SampleInlineArray<int>())[0]", DkmEvaluationResultFlags.None),
31+
EvalResult("[1]", "1", "int", "(new SampleInlineArray<int>())[1]", DkmEvaluationResultFlags.None),
32+
EvalResult("[2]", "2", "int", "(new SampleInlineArray<int>())[2]", DkmEvaluationResultFlags.None),
33+
EvalResult("[3]", "3", "int", "(new SampleInlineArray<int>())[3]", DkmEvaluationResultFlags.None));
34+
}
35+
}

src/ExpressionEvaluator/Core/Source/ExpressionCompiler/ExpressionEvaluatorFatalError.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ internal static class RegistryHelpers
2727
var hKeyCurrentUserField = registryType.GetTypeInfo().GetDeclaredField("CurrentUser");
2828
if (hKeyCurrentUserField != null && hKeyCurrentUserField.IsStatic)
2929
{
30-
using var currentUserKey = (IDisposable)hKeyCurrentUserField.GetValue(null);
30+
using var currentUserKey = (IDisposable?)hKeyCurrentUserField.GetValue(null);
31+
RoslynDebug.AssertNotNull(currentUserKey);
3132
var openSubKeyMethod = currentUserKey.GetType().GetTypeInfo().GetDeclaredMethod("OpenSubKey", [typeof(string), typeof(bool)]);
3233

3334
using var eeKey = (IDisposable?)openSubKeyMethod?.Invoke(currentUserKey, new object[] { RegistryKey, /*writable*/ false });
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Collections.Generic;
6+
using System.Diagnostics.CodeAnalysis;
7+
using BindingFlags = Microsoft.VisualStudio.Debugger.Metadata.BindingFlags;
8+
using CustomAttributeData = Microsoft.VisualStudio.Debugger.Metadata.CustomAttributeData;
9+
using FieldInfo = Microsoft.VisualStudio.Debugger.Metadata.FieldInfo;
10+
using Type = Microsoft.VisualStudio.Debugger.Metadata.Type;
11+
12+
namespace Microsoft.CodeAnalysis.ExpressionEvaluator;
13+
14+
internal static class InlineArrayHelpers
15+
{
16+
private const string InlineArrayAttributeName = "System.Runtime.CompilerServices.InlineArrayAttribute";
17+
18+
public static bool TryGetInlineArrayInfo(Type t, out int arrayLength, [NotNullWhen(true)] out Type? tElementType)
19+
{
20+
arrayLength = -1;
21+
tElementType = null;
22+
23+
if (!t.IsValueType)
24+
{
25+
return false;
26+
}
27+
28+
IList<CustomAttributeData> customAttributes = t.GetCustomAttributesData();
29+
foreach (var attribute in customAttributes)
30+
{
31+
if (InlineArrayAttributeName.Equals(attribute.Constructor?.DeclaringType?.FullName))
32+
{
33+
var ctorParams = attribute.Constructor.GetParameters();
34+
if (ctorParams.Length == 1 && ctorParams[0].ParameterType.IsInt32() &&
35+
attribute.ConstructorArguments.Count == 1 && attribute.ConstructorArguments[0].Value is int length)
36+
{
37+
arrayLength = length;
38+
}
39+
}
40+
}
41+
42+
// Inline arrays must have length > 0
43+
if (arrayLength <= 0)
44+
{
45+
return false;
46+
}
47+
48+
FieldInfo[] fields = t.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly);
49+
if (fields.Length == 1)
50+
{
51+
tElementType = fields[0].FieldType;
52+
}
53+
else
54+
{
55+
// Inline arrays must have exactly one field
56+
return false;
57+
}
58+
59+
return true;
60+
}
61+
}

src/ExpressionEvaluator/Core/Source/ResultProvider/Helpers/TypeHelpers.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,11 @@ private static MethodInfo GetNonIndexerGetMethod(PropertyInfo property)
278278
: null;
279279
}
280280

281+
internal static bool IsInt32(this Type type)
282+
{
283+
return Type.GetTypeCode(type) == TypeCode.Int32;
284+
}
285+
281286
internal static bool IsBoolean(this Type type)
282287
{
283288
return Type.GetTypeCode(type) == TypeCode.Boolean;

src/ExpressionEvaluator/Core/Source/ResultProvider/ResultProvider.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1012,6 +1012,15 @@ internal Expansion GetTypeExpansion(
10121012
return TupleExpansion.CreateExpansion(inspectionContext, declaredTypeAndInfo, value, cardinality);
10131013
}
10141014

1015+
if (InlineArrayHelpers.TryGetInlineArrayInfo(runtimeType, out int inlineArrayLength, out Type inlineArrayElementType))
1016+
{
1017+
// Inline arrays are always 1D, zero-based arrays.
1018+
return ArrayExpansion.CreateExpansion(
1019+
elementTypeAndInfo: new TypeAndCustomInfo(DkmClrType.Create(declaredTypeAndInfo.ClrType.AppDomain, inlineArrayElementType), null),
1020+
sizes: new ReadOnlyCollection<int>([inlineArrayLength]),
1021+
lowerBounds: new ReadOnlyCollection<int>([0]));
1022+
}
1023+
10151024
return MemberExpansion.CreateExpansion(inspectionContext, declaredTypeAndInfo, value, flags, TypeHelpers.IsVisibleMember, this, isProxyType: false, supportsFavorites);
10161025
}
10171026

src/ExpressionEvaluator/Core/Source/ResultProvider/ResultProvider.projitems

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
<Compile Include="$(MSBuildThisFileDirectory)Formatter.TypeNames.cs" />
1717
<Compile Include="$(MSBuildThisFileDirectory)Formatter.Values.cs" />
1818
<Compile Include="$(MSBuildThisFileDirectory)Helpers\FavoritesDataItem.cs" />
19+
<Compile Include="$(MSBuildThisFileDirectory)Helpers\InlineArrayHelpers.cs" />
1920
<Compile Include="$(MSBuildThisFileDirectory)Helpers\TypeAndCustomInfo.cs" />
2021
<Compile Include="$(MSBuildThisFileDirectory)Helpers\TypeWalker.cs" />
2122
<Compile Include="$(MSBuildThisFileDirectory)Helpers\AttributeHelpers.cs" />

src/ExpressionEvaluator/Core/Test/ResultProvider/Debugger/Engine/DkmClrValue.cs

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -565,9 +565,45 @@ public DkmClrValue GetArrayElement(int[] indices, DkmInspectionContext inspectio
565565
throw new ArgumentNullException(nameof(inspectionContext));
566566
}
567567

568-
var array = (System.Array)RawValue;
569-
var element = array.GetValue(indices);
570-
var type = DkmClrType.Create(this.Type.AppDomain, (TypeImpl)((element == null) ? array.GetType().GetElementType() : element.GetType()));
568+
object element;
569+
System.Type elementType;
570+
if (RawValue is Array array)
571+
{
572+
element = array.GetValue(indices);
573+
elementType = (element == null) ? array.GetType().GetElementType() : element.GetType();
574+
}
575+
else
576+
{
577+
#if NET8_0_OR_GREATER
578+
// Might be an inline array struct
579+
if (indices.Length == 1 && InlineArrayHelpers.TryGetInlineArrayInfo(Type.GetLmrType(), out _, out _))
580+
{
581+
// Since reflection is inadequate to dynamically access inline array elements,
582+
// we have to assume it's the special SampleInlineArray type we define for testing and
583+
// cast appropriately.
584+
element = RawValue switch
585+
{
586+
SampleInlineArray<int> intInlineArray => intInlineArray[indices[0]],
587+
// Add more cases here for other types as needed for testing
588+
_ => throw new InvalidOperationException($"Missing cast case for SampleInlineArray"),
589+
};
590+
591+
var fields = RawValue.GetType().GetFields(System.Reflection.BindingFlags.Public |
592+
System.Reflection.BindingFlags.NonPublic |
593+
System.Reflection.BindingFlags.Instance |
594+
System.Reflection.BindingFlags.DeclaredOnly);
595+
elementType = fields[0].FieldType;
596+
}
597+
else
598+
{
599+
throw new InvalidOperationException("Not an array");
600+
}
601+
#else
602+
throw new InvalidOperationException("Not an array");
603+
#endif
604+
}
605+
606+
var type = DkmClrType.Create(this.Type.AppDomain, (TypeImpl)(elementType));
571607
return new DkmClrValue(
572608
element,
573609
element,

src/ExpressionEvaluator/Core/Test/ResultProvider/Debugger/MemberInfo/ConstructorInfoImpl.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System.Collections.Generic;
99
using System.Diagnostics;
1010
using System.Globalization;
11+
using System.Linq;
1112
using Microsoft.VisualStudio.Debugger.Metadata;
1213
using Type = Microsoft.VisualStudio.Debugger.Metadata.Type;
1314

@@ -135,7 +136,7 @@ public override System.Reflection.MethodImplAttributes GetMethodImplementationFl
135136

136137
public override Microsoft.VisualStudio.Debugger.Metadata.ParameterInfo[] GetParameters()
137138
{
138-
throw new NotImplementedException();
139+
return [.. Constructor.GetParameters().Select(p => new ParameterInfoImpl(p))];
139140
}
140141

141142
public override object Invoke(BindingFlags invokeAttr, Binder binder, object[] parameters, CultureInfo culture)

src/ExpressionEvaluator/Core/Test/ResultProvider/Debugger/MemberInfo/ParameterInfoImpl.cs

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,20 +37,11 @@ public override MemberInfo Member
3737
get { throw new NotImplementedException(); }
3838
}
3939

40-
public override string Name
41-
{
42-
get { throw new NotImplementedException(); }
43-
}
40+
public override string Name => Parameter.Name;
4441

45-
public override Type ParameterType
46-
{
47-
get { throw new NotImplementedException(); }
48-
}
42+
public override Type ParameterType => new TypeImpl(Parameter.ParameterType);
4943

50-
public override int Position
51-
{
52-
get { throw new NotImplementedException(); }
53-
}
44+
public override int Position => Parameter.Position;
5445

5546
public override object RawDefaultValue
5647
{

0 commit comments

Comments
 (0)