Skip to content

Commit

Permalink
[wasm][debugger] Fix DebuggerTypeProxy for structs and for classes wi…
Browse files Browse the repository at this point in the history
…th multiple constructors (dotnet#77039)

* Adding any new constructor breaks UsingDebuggerTypeProxy test.

* Fix: now objects won't get identified as valueTypes by a mistake.

* Test logic fix: rely on comparison with fixed entities only.

* Fix: choosing proxy's matching constructor works.

* Support DebuggerProxy in valueTypes.

* Fix test to not rely on `testPropertiesNone`.

* Fix tests.

* Fix tests.

* Edit and disable rootHidden test.
  • Loading branch information
ilonatommy committed Nov 8, 2022
1 parent 134948e commit 16e4b9a
Show file tree
Hide file tree
Showing 7 changed files with 152 additions and 118 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,10 @@ private static async Task<JArray> GetRootHiddenChildren(
{
// a collection - expose elements to be of array scheme
var memberNamedItems = members
.Where(m => m["name"]?.Value<string>() == "Items" || m["name"]?.Value<string>() == "_items")
.Where(m => m["name"]?.Value<string>() == "Items")
.FirstOrDefault();
if (memberNamedItems is not null &&
(DotnetObjectId.TryParse(memberNamedItems["value"]?["objectId"]?.Value<string>(), out DotnetObjectId itemsObjectId)) &&
DotnetObjectId.TryParse(memberNamedItems["value"]?["objectId"]?.Value<string>(), out DotnetObjectId itemsObjectId) &&
itemsObjectId.Scheme == "array")
{
rootObjectId = itemsObjectId;
Expand Down Expand Up @@ -550,7 +550,8 @@ public static async Task<GetMembersResult> GetObjectMemberValues(
// 2
if (!getCommandType.HasFlag(GetObjectCommandOptions.ForDebuggerDisplayAttribute))
{
GetMembersResult debuggerProxy = await sdbHelper.GetValuesFromDebuggerProxyAttribute(objectId, typeIdsIncludingParents[0], token);
GetMembersResult debuggerProxy = await sdbHelper.GetValuesFromDebuggerProxyAttributeForObject(
objectId, typeIdsIncludingParents[0], token);
if (debuggerProxy != null)
return debuggerProxy;
}
Expand Down
162 changes: 100 additions & 62 deletions src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2121,8 +2121,7 @@ public async Task<int> GetTypeByName(string typeToSearch, CancellationToken toke
return retDebuggerCmdReader.ReadInt32();
}

// FIXME: support valuetypes
public async Task<GetMembersResult> GetValuesFromDebuggerProxyAttribute(int objectId, int typeId, CancellationToken token)
public async Task<GetMembersResult> GetValuesFromDebuggerProxyAttributeForObject(int objectId, int typeId, CancellationToken token)
{
try
{
Expand All @@ -2132,25 +2131,11 @@ public async Task<GetMembersResult> GetValuesFromDebuggerProxyAttribute(int obje

using var ctorArgsWriter = new MonoBinaryWriter();
ctorArgsWriter.Write((byte)ValueTypeId.Null);

// FIXME: move method invocation to valueTypeclass?
if (ValueCreator.TryGetValueTypeById(objectId, out var valueType))
{
//FIXME: Issue #68390
//ctorArgsWriter.Write((byte)0); //not used but needed
//ctorArgsWriter.Write(0); //not used but needed
//ctorArgsWriter.Write((int)1); // num args
//ctorArgsWriter.Write(valueType.Buffer);
return null;
}
else
{
ctorArgsWriter.Write((byte)0); //not used
ctorArgsWriter.Write(0); //not used
ctorArgsWriter.Write((int)1); // num args
ctorArgsWriter.Write((byte)ElementType.Object);
ctorArgsWriter.Write(objectId);
}
ctorArgsWriter.Write((byte)0); //not used
ctorArgsWriter.Write(0); //not used
ctorArgsWriter.Write((int)1); // num args
ctorArgsWriter.Write((byte)ElementType.Object);
ctorArgsWriter.Write(objectId);

var retMethod = await InvokeMethod(ctorArgsWriter.GetParameterBuffer(), methodId, token);
if (!DotnetObjectId.TryParse(retMethod?["value"]?["objectId"]?.Value<string>(), out DotnetObjectId dotnetObjectId))
Expand All @@ -2170,6 +2155,39 @@ public async Task<GetMembersResult> GetValuesFromDebuggerProxyAttribute(int obje
return null;
}

public async Task<GetMembersResult> GetValuesFromDebuggerProxyAttributeForValueTypes(int valueTypeId, int typeId, CancellationToken token)
{
try
{
var typeName = await GetTypeName(typeId, token);
int methodId = await FindDebuggerProxyConstructorIdFor(typeId, token);
if (methodId == -1)
return null;

using var ctorArgsWriter = new MonoBinaryWriter();
ctorArgsWriter.Write((byte)ValueTypeId.Null);

if (!ValueCreator.TryGetValueTypeById(valueTypeId, out var valueType))
return null;
ctorArgsWriter.Write((byte)0); //not used but needed
ctorArgsWriter.Write(0); //not used but needed
ctorArgsWriter.Write((int)1); // num args
ctorArgsWriter.Write(valueType.Buffer);
var retMethod = await InvokeMethod(ctorArgsWriter.GetParameterBuffer(), methodId, token);
if (!DotnetObjectId.TryParse(retMethod?["value"]?["objectId"]?.Value<string>(), out DotnetObjectId dotnetObjectId))
throw new Exception($"Invoking .ctor ({methodId}) for DebuggerTypeProxy on type {typeId} returned {retMethod}");
GetMembersResult members = await GetTypeMemberValues(dotnetObjectId,
GetObjectCommandOptions.WithProperties | GetObjectCommandOptions.ForDebuggerProxyAttribute,
token);
return members;
}
catch (Exception e)
{
logger.LogDebug($"Could not evaluate DebuggerTypeProxyAttribute of type {await GetTypeName(typeId, token)} - {e}");
return null;
}
}

private async Task<int> FindDebuggerProxyConstructorIdFor(int typeId, CancellationToken token)
{
try
Expand All @@ -2178,59 +2196,79 @@ private async Task<int> FindDebuggerProxyConstructorIdFor(int typeId, Cancellati
if (getCAttrsRetReader == null)
return -1;

var methodId = -1;
var parmCount = getCAttrsRetReader.ReadInt32();
for (int j = 0; j < parmCount; j++)
if (parmCount != 1)
throw new InternalErrorException($"Expected to find custom attribute with only one argument, but it has {parmCount} parameters.");

byte monoParamTypeId = getCAttrsRetReader.ReadByte();
// FIXME: DebuggerTypeProxyAttribute(string) - not supported
if ((ValueTypeId)monoParamTypeId != ValueTypeId.Type)
{
var monoTypeId = getCAttrsRetReader.ReadByte();
// FIXME: DebuggerTypeProxyAttribute(string) - not supported
if ((ValueTypeId)monoTypeId != ValueTypeId.Type)
continue;
logger.LogDebug($"DebuggerTypeProxy attribute is only supported with a System.Type parameter type. Got {(ValueTypeId)monoParamTypeId}");
return -1;
}

var typeProxyTypeId = getCAttrsRetReader.ReadInt32();

using var commandParamsWriter = new MonoBinaryWriter();
commandParamsWriter.Write(typeProxyTypeId);
var originalClassName = await GetTypeNameOriginal(typeProxyTypeId, token);

var cAttrTypeId = getCAttrsRetReader.ReadInt32();
using var commandParamsWriter = new MonoBinaryWriter();
commandParamsWriter.Write(cAttrTypeId);
var className = await GetTypeNameOriginal(cAttrTypeId, token);
if (className.IndexOf('[') > 0)
if (originalClassName.IndexOf('[') > 0)
{
string className = originalClassName;
className = className.Remove(className.IndexOf('['));
var assemblyId = await GetAssemblyIdFromType(typeProxyTypeId, token);
var assemblyName = await GetFullAssemblyName(assemblyId, token);

StringBuilder typeToSearch = new(className);
typeToSearch.Append('[');
List<int> genericTypeArgs = await GetTypeParamsOrArgsForGenericType(typeId, token);
for (int k = 0; k < genericTypeArgs.Count; k++)
{
className = className.Remove(className.IndexOf('['));
var assemblyId = await GetAssemblyIdFromType(cAttrTypeId, token);
var assemblyName = await GetFullAssemblyName(assemblyId, token);
var typeToSearch = className;
typeToSearch += "[["; //System.Collections.Generic.List`1[[System.Int32,mscorlib,Version=4.0.0.0,Culture=neutral,PublicKeyToken=b77a5c561934e089]],mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
List<int> genericTypeArgs = await GetTypeParamsOrArgsForGenericType(typeId, token);
for (int k = 0; k < genericTypeArgs.Count; k++)
{
var assemblyIdArg = await GetAssemblyIdFromType(genericTypeArgs[k], token);
var assemblyNameArg = await GetFullAssemblyName(assemblyIdArg, token);
var classNameArg = await GetTypeNameOriginal(genericTypeArgs[k], token);
typeToSearch += classNameArg + ", " + assemblyNameArg;
if (k + 1 < genericTypeArgs.Count)
typeToSearch += "], [";
else
typeToSearch += "]";
}
typeToSearch += "]";
typeToSearch += ", " + assemblyName;
var genericTypeId = await GetTypeByName(typeToSearch, token);
if (genericTypeId < 0)
break;
cAttrTypeId = genericTypeId;
// typeToSearch += '[';
var assemblyIdArg = await GetAssemblyIdFromType(genericTypeArgs[k], token);
var assemblyNameArg = await GetFullAssemblyName(assemblyIdArg, token);
var classNameArg = await GetTypeNameOriginal(genericTypeArgs[k], token);
typeToSearch.Append($"{(k == 0 ? "" : ",")}[{classNameArg}, {assemblyNameArg}]");
}
int[] methodIds = await GetMethodIdsByName(cAttrTypeId, ".ctor", BindingFlags.Default, token);
if (methodIds != null)
methodId = methodIds[0];
break;
typeToSearch.Append($"], {assemblyName}");
var genericTypeId = await GetTypeByName(typeToSearch.ToString(), token);
if (genericTypeId < 0)
{
logger.LogDebug($"Could not find instantiated generic type id for {typeToSearch}.");
return -1;
}
typeProxyTypeId = genericTypeId;
}
int[] constructorIds = await GetMethodIdsByName(typeProxyTypeId, ".ctor", BindingFlags.DeclaredOnly, token);
if (constructorIds is null)
throw new InternalErrorException($"Could not find any constructor for DebuggerProxy type: {originalClassName}");

if (constructorIds.Length == 1)
return constructorIds[0];

return methodId;
string expectedConstructorParamType = await GetTypeName(typeId, token);
foreach (var methodId in constructorIds)
{
var methodInfoFromRuntime = await GetMethodInfo(methodId, token);
// avoid calling to runtime if possible
var ps = methodInfoFromRuntime.Info.GetParametersInfo();
if (ps.Length != 1)
continue;
string parameters = await GetParameters(methodId, token);
if (string.IsNullOrEmpty(parameters))
throw new InternalErrorException($"Could not get method's parameter types. MethodId = {methodId}.");
if (parameters == $"({expectedConstructorParamType})")
return methodId;
}
throw new InternalErrorException($"Could not find a matching constructor for DebuggerProxy type: {originalClassName}");
}
catch (Exception e)
{
logger.LogDebug($"Could not evaluate DebuggerTypeProxyAttribute of type {await GetTypeName(typeId, token)} - {e}");
return -1;
}

return -1;
}

public ValueTypeClass GetValueTypeClass(int valueTypeId)
Expand Down
4 changes: 1 addition & 3 deletions src/mono/wasm/debugger/BrowserDebugProxy/ValueTypeClass.cs
Original file line number Diff line number Diff line change
Expand Up @@ -213,9 +213,7 @@ public async Task<GetMembersResult> GetMemberValues(
if (!getObjectOptions.HasFlag(GetObjectCommandOptions.ForDebuggerDisplayAttribute))
{
// FIXME: cache?
result = await sdbHelper.GetValuesFromDebuggerProxyAttribute(Id.Value, TypeId, token);
if (result != null)
Console.WriteLine($"Investigate GetValuesFromDebuggerProxyAttribute\n{result}. There was a change of logic from loop to one iteration");
result = await sdbHelper.GetValuesFromDebuggerProxyAttributeForValueTypes(Id.Value, TypeId, token);
}

if (result == null && getObjectOptions.HasFlag(GetObjectCommandOptions.AccessorPropertiesOnly))
Expand Down
8 changes: 6 additions & 2 deletions src/mono/wasm/debugger/DebuggerTestSuite/CustomViewTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public async Task UsingDebuggerDisplay()
[ConditionalFact(nameof(RunningOnChrome))]
public async Task UsingDebuggerTypeProxy()
{
var bp = await SetBreakpointInMethod("debugger-test.dll", "DebuggerTests.DebuggerCustomViewTest", "run", 15);
var bp = await SetBreakpointInMethod("debugger-test.dll", "DebuggerTests.DebuggerCustomViewTest", "run", 16);
var pause_location = await EvaluateAndCheck(
"window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.DebuggerCustomViewTest:run'); }, 1);",
"dotnet://debugger-test.dll/debugger-custom-view-test.cs",
Expand All @@ -61,6 +61,10 @@ public async Task UsingDebuggerTypeProxy()
props = await GetObjectOnFrame(frame, "b");
await CheckString(props, "Val2", "one");

await CheckValueType(locals, "bs", "DebuggerTests.WithProxyStruct", description:"DebuggerTests.WithProxyStruct");
props = await GetObjectOnFrame(frame, "bs");
await CheckString(props, "Val2", "one struct");

await CheckObject(locals, "openWith", "System.Collections.Generic.Dictionary<string, string>", description: "Count = 3");
props = await GetObjectOnFrame(frame, "openWith");
Assert.Equal(1, props.Count());
Expand Down Expand Up @@ -106,7 +110,7 @@ async Task<bool> CheckProperties(JObject pause_location)
Assert.True(task.Result);
}
}

[ConditionalFact(nameof(RunningOnChrome))]
public async Task InspectObjectOfTypeWithToStringOverriden()
{
Expand Down
12 changes: 10 additions & 2 deletions src/mono/wasm/debugger/DebuggerTestSuite/DebuggerTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -772,9 +772,17 @@ internal async Task CheckProps(JToken actual, object exp_o, string label, int nu
var exp_i = exp_v_arr[i];
var act_i = actual_arr[i];

AssertEqual(i.ToString(), act_i["name"]?.Value<string>(), $"{label}-[{i}].name");
string exp_name = exp_i["name"]?.Value<string>();
if (string.IsNullOrEmpty(exp_name))
exp_name = i.ToString();

AssertEqual(exp_name, act_i["name"]?.Value<string>(), $"{label}-[{i}].name");
if (exp_i != null)
await CheckValue(act_i["value"], exp_i, $"{label}-{i}th value");
{
await CheckValue(act_i["value"],
((JObject)exp_i).GetValue("value")?.HasValues == true ? exp_i["value"] : exp_i,
$"{label}-{i}th value");
}
}
return;
}
Expand Down
Loading

0 comments on commit 16e4b9a

Please sign in to comment.