Skip to content

Commit

Permalink
[wasm][debugger] Implement support to DebuggerDisplay attribute (#55524)
Browse files Browse the repository at this point in the history
* Support DebuggerDisplay attribute on type.

* Implementing DebuggerDisplay calling methods.

* Apply suggestions from code review

Co-authored-by: Larry Ewing <lewing@microsoft.com>

* Fixing snake case.

* Changing snake case.

Co-authored-by: Larry Ewing <lewing@microsoft.com>
  • Loading branch information
thaystg and lewing authored Jul 14, 2021
1 parent 8bf8030 commit 44f7103
Show file tree
Hide file tree
Showing 8 changed files with 271 additions and 6 deletions.
33 changes: 33 additions & 0 deletions src/mono/sample/wasm/browser/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,40 @@

using System;
using System.Runtime.CompilerServices;
using System.Diagnostics;

namespace Sample
{
public class Test
{
[DebuggerDisplay ("Some {Val1} Value {Val2} End")]
class WithDisplayString
{
internal string Val1 = "one";

public int Val2 { get { return 2; } }
}

class WithToString
{
public override string ToString ()
{
return "SomeString";
}
}

[DebuggerDisplay ("{GetDebuggerDisplay(), nq}")]
class DebuggerDisplayMethodTest
{
int someInt = 32;
int someInt2 = 43;

string GetDebuggerDisplay ()
{
return "First Int:" + someInt + " Second Int:" + someInt2;
}
}

public static void Main(string[] args)
{
Console.WriteLine ("Hello, World!");
Expand All @@ -16,6 +45,10 @@ public static void Main(string[] args)
[MethodImpl(MethodImplOptions.NoInlining)]
public static int TestMeaning()
{
var a = new WithDisplayString();
var c = new DebuggerDisplayMethodTest();
Console.WriteLine(a);
Console.WriteLine(c);
return 42;
}
}
Expand Down
11 changes: 11 additions & 0 deletions src/mono/wasm/debugger/BrowserDebugProxy/DevToolsHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -322,5 +322,16 @@ internal class PerScopeCache
{
public Dictionary<string, JObject> Locals { get; } = new Dictionary<string, JObject>();
public Dictionary<string, JObject> MemberReferences { get; } = new Dictionary<string, JObject>();
public Dictionary<string, JObject> ObjectFields { get; } = new Dictionary<string, JObject>();
public PerScopeCache(JArray objectValues)
{
foreach (var objectValue in objectValues)
{
ObjectFields[objectValue["name"].Value<string>()] = objectValue.Value<JObject>();
}
}
public PerScopeCache()
{
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ public override void Visit(SyntaxNode node)

if (node is IdentifierNameSyntax identifier
&& !(identifier.Parent is MemberAccessExpressionSyntax)
&& !(identifier.Parent is InvocationExpressionSyntax)
&& !identifiers.Any(x => x.Identifier.Text == identifier.Identifier.Text))
{
identifiers.Add(identifier);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ internal class MemberReferenceResolver
private ExecutionContext ctx;
private PerScopeCache scopeCache;
private ILogger logger;
private bool locals_fetched;
private bool localsFetched;

public MemberReferenceResolver(MonoProxy proxy, ExecutionContext ctx, SessionId sessionId, int scopeId, ILogger logger)
{
Expand All @@ -33,6 +33,18 @@ public MemberReferenceResolver(MonoProxy proxy, ExecutionContext ctx, SessionId
this.logger = logger;
scopeCache = ctx.GetCacheForScope(scopeId);
}

public MemberReferenceResolver(MonoProxy proxy, ExecutionContext ctx, SessionId sessionId, JArray objectValues, ILogger logger)
{
this.sessionId = sessionId;
scopeId = -1;
this.proxy = proxy;
this.ctx = ctx;
this.logger = logger;
scopeCache = new PerScopeCache(objectValues);
localsFetched = true;
}

public async Task<JObject> GetValueFromObject(JToken objRet, CancellationToken token)
{
if (objRet["value"]?["className"]?.Value<string>() == "System.Exception")
Expand Down Expand Up @@ -76,6 +88,11 @@ public async Task<JObject> Resolve(string varName, CancellationToken token)
if (scopeCache.MemberReferences.TryGetValue(varName, out JObject ret)) {
return ret;
}

if (scopeCache.ObjectFields.TryGetValue(varName, out JObject valueRet)) {
return await GetValueFromObject(valueRet, token);
}

foreach (string part in parts)
{
string partTrimmed = part.Trim();
Expand All @@ -96,12 +113,12 @@ public async Task<JObject> Resolve(string varName, CancellationToken token)
}
continue;
}
if (scopeCache.Locals.Count == 0 && !locals_fetched)
if (scopeCache.Locals.Count == 0 && !localsFetched)
{
Result scope_res = await proxy.GetScopeProperties(sessionId, scopeId, token);
if (scope_res.IsErr)
throw new Exception($"BUG: Unable to get properties for scope: {scopeId}. {scope_res}");
locals_fetched = true;
localsFetched = true;
}
if (scopeCache.Locals.TryGetValue(partTrimmed, out JObject obj))
{
Expand Down Expand Up @@ -145,6 +162,12 @@ public async Task<JObject> Resolve(InvocationExpressionSyntax method, Dictionary
rootObject = await Resolve(memberAccessExpressionSyntax.Expression.ToString(), token);
methodName = memberAccessExpressionSyntax.Name.ToString();
}
else if (expr is IdentifierNameSyntax)
if (scopeCache.ObjectFields.TryGetValue("this", out JObject valueRet)) {
rootObject = await GetValueFromObject(valueRet, token);
methodName = expr.ToString();
}

if (rootObject != null)
{
DotnetObjectId.TryParse(rootObject?["objectId"]?.Value<string>(), out DotnetObjectId objectId);
Expand Down
2 changes: 1 addition & 1 deletion src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ internal class MonoProxy : DevToolsProxy
public MonoProxy(ILoggerFactory loggerFactory, IList<string> urlSymbolServerList) : base(loggerFactory)
{
this.urlSymbolServerList = urlSymbolServerList ?? new List<string>();
SdbHelper = new MonoSDBHelper(this);
SdbHelper = new MonoSDBHelper(this, logger);
}

internal ExecutionContext GetContext(SessionId sessionId)
Expand Down
95 changes: 93 additions & 2 deletions src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -615,9 +615,12 @@ internal class MonoSDBHelper
private MonoProxy proxy;
private static int MINOR_VERSION = 61;
private static int MAJOR_VERSION = 2;
public MonoSDBHelper(MonoProxy proxy)
private readonly ILogger logger;

public MonoSDBHelper(MonoProxy proxy, ILogger logger)
{
this.proxy = proxy;
this.logger = logger;
}

public void ClearCache()
Expand Down Expand Up @@ -946,6 +949,7 @@ public async Task<List<FieldTypeClass>> GetTypeFields(SessionId sessionId, int t
}
return ret;
}

public string ReplaceCommonClassNames(string className)
{
className = className.Replace("System.String", "string");
Expand All @@ -957,6 +961,88 @@ public string ReplaceCommonClassNames(string className)
className = className.Replace("System.Byte", "byte");
return className;
}

public async Task<string> GetDebuggerDisplayAttribute(SessionId sessionId, int objectId, int typeId, CancellationToken token)
{
string expr = "";
var invokeParams = new MemoryStream();
var invokeParamsWriter = new MonoBinaryWriter(invokeParams);
var commandParams = new MemoryStream();
var commandParamsWriter = new MonoBinaryWriter(commandParams);
commandParamsWriter.Write(typeId);
commandParamsWriter.Write(0);
var retDebuggerCmdReader = await SendDebuggerAgentCommand<CmdType>(sessionId, CmdType.GetCattrs, commandParams, token);
var count = retDebuggerCmdReader.ReadInt32();
if (count == 0)
return null;
for (int i = 0 ; i < count; i++)
{
var methodId = retDebuggerCmdReader.ReadInt32();
if (methodId == 0)
continue;
commandParams = new MemoryStream();
commandParamsWriter = new MonoBinaryWriter(commandParams);
commandParamsWriter.Write(methodId);
var retDebuggerCmdReader2 = await SendDebuggerAgentCommand<CmdMethod>(sessionId, CmdMethod.GetDeclaringType, commandParams, token);
var customAttributeTypeId = retDebuggerCmdReader2.ReadInt32();
var customAttributeName = await GetTypeName(sessionId, customAttributeTypeId, token);
if (customAttributeName == "System.Diagnostics.DebuggerDisplayAttribute")
{
invokeParamsWriter.Write((byte)ValueTypeId.Null);
invokeParamsWriter.Write((byte)0); //not used
invokeParamsWriter.Write(0); //not used
var parmCount = retDebuggerCmdReader.ReadInt32();
invokeParamsWriter.Write((int)1);
for (int j = 0; j < parmCount; j++)
{
invokeParamsWriter.Write((byte)retDebuggerCmdReader.ReadByte());
invokeParamsWriter.Write(retDebuggerCmdReader.ReadInt32());
}
var retMethod = await InvokeMethod(sessionId, invokeParams.ToArray(), methodId, "methodRet", token);
DotnetObjectId.TryParse(retMethod?["value"]?["objectId"]?.Value<string>(), out DotnetObjectId dotnetObjectId);
var displayAttrs = await GetObjectValues(sessionId, int.Parse(dotnetObjectId.Value), true, false, false, false, token);
var displayAttrValue = displayAttrs.FirstOrDefault(attr => attr["name"].Value<string>().Equals("Value"));
try {
ExecutionContext context = proxy.GetContext(sessionId);
var objectValues = await GetObjectValues(sessionId, objectId, true, false, false, false, token);

var thisObj = CreateJObject<string>("", "object", "", false, objectId:$"dotnet:object:{objectId}");
thisObj["name"] = "this";
objectValues.Add(thisObj);

var resolver = new MemberReferenceResolver(proxy, context, sessionId, objectValues, logger);
var dispAttrStr = displayAttrValue["value"]?["value"]?.Value<string>();
//bool noQuotes = false;
if (dispAttrStr.Contains(", nq"))
{
//noQuotes = true;
dispAttrStr = dispAttrStr.Replace(", nq", "");
}
expr = "$\"" + dispAttrStr + "\"";
JObject retValue = await resolver.Resolve(expr, token);
if (retValue == null)
retValue = await EvaluateExpression.CompileAndRunTheExpression(expr, resolver, token);
return retValue?["value"]?.Value<string>();
}
catch (Exception)
{
logger.LogDebug($"Could not evaluate DebuggerDisplayAttribute - {expr}");
return null;
}
}
else
{
var parmCount = retDebuggerCmdReader.ReadInt32();
for (int j = 0; j < parmCount; j++)
{
//to read parameters
await CreateJObjectForVariableValue(sessionId, retDebuggerCmdReader, "varName", false, -1, token);
}
}
}
return null;
}

public async Task<string> GetTypeName(SessionId sessionId, int type_id, CancellationToken token)
{
var commandParams = new MemoryStream();
Expand Down Expand Up @@ -1043,7 +1129,7 @@ public async Task<int> GetMethodIdByName(SessionId sessionId, int type_id, strin
var commandParamsWriter = new MonoBinaryWriter(commandParams);
commandParamsWriter.Write((int)type_id);
commandParamsWriter.WriteString(method_name);
commandParamsWriter.Write((int)(0x10 | 4)); //instance methods
commandParamsWriter.Write((int)(0x10 | 4 | 0x20)); //instance methods
commandParamsWriter.Write((int)1); //case sensitive
var retDebuggerCmdReader = await SendDebuggerAgentCommand<CmdType>(sessionId, CmdType.GetMethodsByNameFlags, commandParams, token);
var nMethods = retDebuggerCmdReader.ReadInt32();
Expand Down Expand Up @@ -1299,7 +1385,12 @@ public async Task<JObject> CreateJObjectForObject(SessionId sessionId, MonoBinar
var className = "";
var type_id = await GetTypeIdFromObject(sessionId, objectId, false, token);
className = await GetTypeName(sessionId, type_id[0], token);
var debuggerDisplayAttribute = await GetDebuggerDisplayAttribute(sessionId, objectId, type_id[0], token);
var description = className.ToString();

if (debuggerDisplayAttribute != null)
description = debuggerDisplayAttribute;

if (await IsDelegate(sessionId, objectId, token))
{
if (typeIdFromAttribute != -1)
Expand Down
33 changes: 33 additions & 0 deletions src/mono/wasm/debugger/DebuggerTestSuite/CustomViewTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.WebAssembly.Diagnostics;
using Newtonsoft.Json.Linq;
using System.Threading;
using Xunit;

namespace DebuggerTests
{

public class CustomViewTests : DebuggerTestBase
{
[Fact]
public async Task CustomView()
{
var bp = await SetBreakpointInMethod("debugger-test.dll", "DebuggerTests.DebuggerCustomViewTest", "run", 5);
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",
bp.Value["locations"][0]["lineNumber"].Value<int>(),
bp.Value["locations"][0]["columnNumber"].Value<int>(),
"run");

var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
CheckObject(locals, "a", "DebuggerTests.WithDisplayString", description:"Some one Value 2 End");
CheckObject(locals, "c", "DebuggerTests.DebuggerDisplayMethodTest", description: "First Int:32 Second Int:43");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Threading.Tasks;
using System.Diagnostics;

namespace DebuggerTests
{
[DebuggerDisplay("Some {Val1} Value {Val2} End")]
class WithDisplayString
{
internal string Val1 = "one";

public int Val2 { get { return 2; } }
}

class WithToString
{
public override string ToString ()
{
return "SomeString";
}
}

[DebuggerTypeProxy(typeof(TheProxy))]
class WithProxy
{
public string Val1 {
get { return "one"; }
}
}

class TheProxy
{
WithProxy wp;

public TheProxy (WithProxy wp)
{
this.wp = wp;
}

public string Val2 {
get { return wp.Val1; }
}
}

[DebuggerDisplay("{GetDebuggerDisplay(), nq}")]
class DebuggerDisplayMethodTest
{
int someInt = 32;
int someInt2 = 43;

string GetDebuggerDisplay ()
{
return "First Int:" + someInt + " Second Int:" + someInt2;
}
}

class DebuggerCustomViewTest
{
public static void run()
{
var a = new WithDisplayString();
var b = new WithProxy();
var c = new DebuggerDisplayMethodTest();
Console.WriteLine(a);
Console.WriteLine(b);
Console.WriteLine(c);
}
}
}

0 comments on commit 44f7103

Please sign in to comment.