Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
42 changes: 38 additions & 4 deletions src/MIDebugEngine/Engine.Impl/Variables.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Microsoft.VisualStudio.Debugger.Interop;
using Microsoft.VisualStudio.Debugger.Interop.DAP;
using System;
using Microsoft.DebugEngineHost;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
Expand Down Expand Up @@ -92,6 +93,18 @@ internal sealed class VariableInformation : IVariableInformation

static readonly Lazy<Regex> s_addressPattern = new Lazy<Regex>(() => new Regex(@"^(0x[0-9a-fA-F]+)\b"));

static readonly Regex s_naPattern = new Regex(@"^0x[0-9a-fA-F]+\s+(.)");

internal static string StripLeadingAddress(string value)
{
if (string.IsNullOrEmpty(value))
{
return value;
}

return s_naPattern.Replace(value, "$1");
}

public string Address()
{
// ask GDB to evaluate "&expression"
Expand Down Expand Up @@ -238,7 +251,7 @@ internal VariableInformation(string displayName, string expr, ThreadContext ctx,
: this(ctx, engine, thread)
{
// strip off formatting string
_strippedName = ProcessFormatSpecifiers(expr, out _format);
_strippedName = ProcessFormatSpecifiers(expr, out _format, out _formatHasNa);
Name = displayName;
IsParameter = isParameter;
_parent = null;
Expand All @@ -250,7 +263,7 @@ internal VariableInformation(string expr, IVariableInformation parent, AD7Engine
: this(parent.ThreadContext, engine, parent.Client)
{
// strip off formatting string
_strippedName = ProcessFormatSpecifiers(expr, out _format);
_strippedName = ProcessFormatSpecifiers(expr, out _format, out _formatHasNa);
Name = displayName ?? expr;
_parent = parent;
VariableNodeType = NodeType.Synthetic;
Expand All @@ -261,7 +274,7 @@ internal VariableInformation(string expr, VariableInformation parent)
: this(parent._ctx, parent._engine, parent.Client)
{
// strip off formatting string
_strippedName = ProcessFormatSpecifiers(expr, out _format);
_strippedName = ProcessFormatSpecifiers(expr, out _format, out _formatHasNa);
Name = expr;
VariableNodeType = NodeType.Root;
}
Expand All @@ -272,6 +285,12 @@ private VariableInformation(TupleValue results, VariableInformation parent, stri
{
TypeName = results.TryFindString("type");
Value = results.TryFindString("value");
_formatHasNa = parent._formatHasNa;
if (_formatHasNa)
{
Value = StripLeadingAddress(Value);
}

Name = name ?? results.FindString("exp");
if (results.Contains("dynamic"))
{
Expand Down Expand Up @@ -375,6 +394,9 @@ public VariableInformation FindChildByName(string name)
private DeferedFormatExpression _deferedFormatExpression;
private IVariableInformation _parent;
private string _format;
// Indicates the original format specifier included the natvis "na" modifier
// (used to decide whether to strip MI's leading address prefix from string values)
private bool _formatHasNa = false;
private string _strippedName; // "Name" stripped of format specifiers
private string _fullname;

Expand All @@ -401,8 +423,9 @@ public enum NodeType

private static Regex s_isFunction = new Regex(@".+\(.*\).*");

private string ProcessFormatSpecifiers(string exp, out string formatSpecifier)
private string ProcessFormatSpecifiers(string exp, out string formatSpecifier, out bool formatNa)
{
formatNa = false;
formatSpecifier = null; // will be used with -var-set-format

if (EngineUtils.IsConsoleExecCmd(exp, out string _, out string _))
Expand All @@ -416,6 +439,9 @@ private string ProcessFormatSpecifiers(string exp, out string formatSpecifier)

// Find the format specifier expression
string expFS = exp.Substring(lastComma + 1).Trim();
// Detect whether the natvis 'na' modifier is present in the original format specifier.
// We must detect this before we strip modifiers below.
formatNa = expFS.IndexOf("na", StringComparison.Ordinal) >= 0;

// Strip off modifiers that may be included together with another format specifier, e.g. 'nvoXb' is a valid format specifier, but we only care about the 'Xb' part
// This is not quite the right fix -- really the below switch statement should be a series of if statements. But since none of the supported format specifiers
Expand Down Expand Up @@ -698,6 +724,10 @@ internal async Task Eval(uint radix, enum_EVALFLAGS dwFlags = 0, DAPEvalFlags dw
_attribsFetched = true;
}
Value = results.TryFindString("value");
if (_formatHasNa)
{
Value = StripLeadingAddress(Value);
}
if ((string.IsNullOrEmpty(Value) || _format != null) && !string.IsNullOrEmpty(_internalName))
{
if (_format != null)
Expand All @@ -711,6 +741,10 @@ internal async Task Eval(uint radix, enum_EVALFLAGS dwFlags = 0, DAPEvalFlags dw
if (results.ResultClass == ResultClass.done)
{
Value = results.FindString("value");
if (_formatHasNa)
{
Value = StripLeadingAddress(Value);
}
}
else if (results.ResultClass == ResultClass.error)
{
Expand Down
49 changes: 34 additions & 15 deletions src/MIDebugEngine/Natvis.Impl/Natvis.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1283,12 +1283,23 @@ private string FormatValue(string format, IVariableInformation variable, IDictio
if (m.Success)
{
string rawExpr = format.Substring(i + 1, m.Length - 2);
string spec = ExtractFormatSpecifier(rawExpr);
// Substitute template parameter macros ($T1, $T2, ...) inside the whole brace
// expression (this covers both the expression and any trailing format specifier)
if (scopedNames != null)
{
rawExpr = Regex.Replace(rawExpr, @"\$T\d+", (Match mt) =>
{
if (scopedNames.TryGetValue(mt.Value, out string replacement))
return replacement;
return mt.Value;
});
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I am reading the code correctly, this is already at least partially done by ReplaceNamesInExpression, which is called by GetExpressionValue, so I don't think you want to do this over the entire expression.

Can you trace through why ReplaceNamesInExpression isn't doing this already? (lines 1713-1717).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah it seems like ReplaceNamesInExpression should properly handle the substitution, and it seems like it gets called at the right time, so I have no answer for why it isn't doing it already

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should debug through it and see why it doesn't work. Let me know if you need help.

ExtractFormatSpecifier(rawExpr, out bool hasNa);
string exprValue = GetExpressionValue(rawExpr, variable, scopedNames, intrinsics);
if (spec == "sub" || spec == "su")
exprValue = CleanUtf16StringValue(exprValue);
else if (spec == "sb")
exprValue = CleanAsciiStringValue(exprValue);

@gregg-miskelly gregg-miskelly Jul 22, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you mean to loose this sub/su/sb code?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, is su needed? Variabel.cs line 477 already handles su correctly.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This problem seems to still be here

if (hasNa)
{
exprValue = VariableInformation.StripLeadingAddress(exprValue);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Make VariableInformation.StripLeadingAddress public so you can use it here

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

value.Append(exprValue);
i += m.Length - 1;
}
Expand Down Expand Up @@ -1494,30 +1505,40 @@ private static int FindLastTopLevelComma(string expression)
/// <see cref="VariableInformation.ProcessFormatSpecifiers"/>: modifiers "nvo", "na",
/// "nr", "nd" are stripped before returning. Returns null when no specifier is present.
/// </summary>
internal static string ExtractFormatSpecifier(string expression)
internal static string ExtractFormatSpecifier(string expression, out bool hasNa)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ExtractFormatSpecifier

You need to update src/MIDebugEngineUnitTests/NatvisFormatSpecifierTest.cs for this change

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

{
hasNa = false;
int commaPos = FindLastTopLevelComma(expression);
if (commaPos < 0) return null;

string tail = expression.Substring(commaPos + 1).Trim();
hasNa = tail.IndexOf("na", StringComparison.Ordinal) >= 0;

return expression.Substring(commaPos + 1).Trim()
.Replace("nvo", "").Replace("na", "").Replace("nr", "").Replace("nd", "");
}

/// <summary>
/// Returns true if the NatVis expression's trailing format specifier (the part after the
/// last top-level comma) contains the "na" modifier. This intentionally inspects the
/// raw specifier text and does not normalize/remove modifiers so callers can detect
/// whether the original expression asked for the "na" behavior.
/// </summary>
/// <summary>
/// Cleans up the raw value that GDB/LLDB returns for a <c>const char16_t*</c>
/// expression (i.e. one evaluated with the <c>,sub</c> / <c>,su</c> format specifier).
/// GDB and LLDB both prefix the string with the pointer address, e.g.
/// <c>0x00007fff5fbff6c0 u"Hello"</c>
/// This method strips the address and the surrounding <c>u"…"</c> quotes so that
/// the NatVis DisplayString shows just the string content.
/// This method strips the leading address prefix that GDB/LLDB emits ("0x... ").
/// It does NOT remove surrounding quotes or the leading character-width prefix (u/U).
/// </summary>
internal static string CleanUtf16StringValue(string value)
{
if (string.IsNullOrEmpty(value)) return value;
// Strip leading "0x<hex> " address prefix emitted by GDB/LLDB.
value = s_addressPrefix.Replace(value, "");
value = VariableInformation.StripLeadingAddress(value);
// Strip surrounding u"..." or U"..." quotes.
if (value.Length >= 3 &&
(value.StartsWith("u\"", StringComparison.Ordinal) || value.StartsWith("U\"", StringComparison.Ordinal)))
if (value.Length >= 3 && (value.StartsWith("u\"", StringComparison.Ordinal) || value.StartsWith("U\"", StringComparison.Ordinal)))
{
value = value.EndsWith("\"", StringComparison.Ordinal)
? value.Substring(2, value.Length - 3)
Expand All @@ -1531,15 +1552,13 @@ internal static string CleanUtf16StringValue(string value)
/// (i.e. one evaluated with the <c>,sb</c> format specifier).
/// GDB and LLDB prefix the string with the pointer address, e.g.
/// <c>0x00007fff5fbff6c0 "Hello"</c>
/// This method strips the address and the surrounding <c>"…"</c> quotes so that
/// the NatVis DisplayString shows just the string content (matching VS behaviour,
/// where <c>{ptr,sb}</c> evaluates to bare text without quotes).
/// This method strips the leading address prefix that GDB/LLDB emits ("0x... ").
/// </summary>
internal static string CleanAsciiStringValue(string value)
{
if (string.IsNullOrEmpty(value)) return value;
// Strip leading "0x<hex> " address prefix emitted by GDB/LLDB.
value = s_addressPrefix.Replace(value, "");
value = VariableInformation.StripLeadingAddress(value);
// Strip surrounding "..." quotes.
if (value.Length >= 2 && value.StartsWith("\"", StringComparison.Ordinal))
{
Expand Down
11 changes: 6 additions & 5 deletions src/MIDebugEngineUnitTests/NatvisFormatSpecifierTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,33 +15,34 @@ public class NatvisFormatSpecifierTest
[Fact]
public void ExtractFormatSpecifier_Sub_Extracted()
{
Assert.Equal("sub", Natvis.ExtractFormatSpecifier("schemeStr(),sub"));
Assert.Equal("sub", Natvis.ExtractFormatSpecifier("schemeStr(),sub", out _));
}

[Fact]
public void ExtractFormatSpecifier_Decimal_Extracted()
{
Assert.Equal("d", Natvis.ExtractFormatSpecifier("year(),d"));
Assert.Equal("d", Natvis.ExtractFormatSpecifier("year(),d", out _));
}

[Fact]
public void ExtractFormatSpecifier_NoSpecifier_ReturnsNull()
{
Assert.Null(Natvis.ExtractFormatSpecifier("cspec == 1"));
Assert.Null(Natvis.ExtractFormatSpecifier("cspec == 1", out _));
}

[Fact]
public void ExtractFormatSpecifier_NvoModifierStripped()
{
// "nvoXb": strip "nvo" modifier, result is "Xb"
Assert.Equal("Xb", Natvis.ExtractFormatSpecifier("data1,nvoXb"));
Assert.Equal("Xb", Natvis.ExtractFormatSpecifier("data1,nvoXb", out _));
}

[Fact]
public void ExtractFormatSpecifier_NaModifierStripped()
{
// "view(RecZone)na": strip "na", result is "view(RecZone)"
Assert.Equal("view(RecZone)", Natvis.ExtractFormatSpecifier("this,view(RecZone)na"));
Assert.Equal("view(RecZone)", Natvis.ExtractFormatSpecifier("this,view(RecZone)na", out bool hasNa));
Assert.True(hasNa);
}

// -- CleanUtf16StringValue --------------------------------------------
Expand Down
Loading