3

I have a type which represents "Battleship" coordinates:

struct BattleshipCoordinates
{
    int row; // zero-based row offset
    int col; // zero-based column offset
}

Note that the coordinates are stored natively as zero-based index offsets. I would like to display these in the debugger in a more 'natural' view for Battleship coordinates (i.e. when the structure contains {0, 0} I would like the debugger to display "A1" for the upper left corner). I would like to accomplish this custom formatting with a .natvis file.

I am able to translate the values to their respective chars ('A' and '1'), but the debugger displays them in an offputting format with extra formatting:

<Type Name="BattleshipCoordinates">
  <DisplayString>{(char)(row + 'A')}{(char)(col + '1')}</DisplayString>
</Type>

There are a number of issues with this approach; the current result for {0,0} is displayed in the debugger as 65'A'49'1'. I would like to remove the extra formatting (numbers and quotation marks) and just display simply "A1". Additionally, that syntax would break as soon as the column reaches double-digit values.

What secret sauce am I missing? Is there some method through which I can stream together multiple values?

If I could access stringstreams, I could just use: ostr << static_cast<char>(row + 'A') << (col + 1). If I could call one of the available to_string functions in my code, that would also work; but to my knowledge, none of that is available in natvis syntax...

2
  • I don't think this is possible. You can define format specifiers for a code snippet, but you can't change how those specifiers are displayed. In your example the character format specifier is used because of the cast. It's the same as {(row + 'A'),c}. It's just another way of displaying a number. {(row + 'A'),d}{(col + '0'),d} would display 6548. But there is no format specifier for displaying the character only. Commented Jul 5, 2023 at 0:56
  • I'm somewhat convinced that this cannot be done. The debuggers' expression evaluators try very hard to remain free of side effects. Creating a string certainly does have side effects. I also evaluated the following alternatives: 1 Working with a statically sized char array (arrays aren't considered primitive types). 2 Encode the string into a DWORD (which works) and then take the address cast to a char* for display (taking the address won't work). 3 Moving any of this into a <Variable> with the expectation that debugger-controlled memory is more flexible (it isn't). Commented Jul 29, 2024 at 10:05

1 Answer 1

1

It turns out this sort of thing is possible, provided that you can include some character tables in your program. This is because the natvis format [1]nasb can be used to print any byte in program memory as a single character. In fact it's the only way to print single characters. This cryptic format code means:

  • [1] interpret pointer as an array with 1 element

  • na don't print the address of the pointer, regardless of debugger settings

  • sb interpret pointer as a C-string that can be null-terminated

If you need to generate arbitrary ASCII, you must put a table representing all 128 ASCII characters into your program — and take measures to ensure it's not removed by the linker's 'unused data' optimization. Here I've substituted ? for various unprintable characters:

constexpr char ALPH_ASCII[129] = // 128 ASCII characters + null terminator
    "\0???????????????????????????????"
    " !\"#$%&'()*+,-./0123456789:;<=>?@"
    "ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`"
    "abcdefghijklmnopqrstuvwxyz{|}~?";

Then we can offset our table pointer by any integer or char we like in debugger expressions. Here's an example expression for printing a number represented by (bool) sign and (unsigned) magnitude:

struct my_ones_complement {bool my_sign_bit; unsigned my_magnitude;};
{ALPH_ASCII+((my_sign_bit?'-':'+'),[1]nasb}{my_magnitude,d}

For a more involved example, suppose you want to display an integer value as base64. Start by putting a table of all 64 digits into your program.

constexpr char ALPH_B64U[65] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

struct ID_60Bit {uint64_t value;}

Then, in your natvis file (inside AutoVisualizer but outside any Type) you can add an intrinsic to convert the bottom six bits of any number to a pointer to one of these base64 digits. We'll call it D64, short for for "Digit in Base 64".

<Intrinsic Name="D64" Expression="ALPH_B64U+(val&amp;63)">
    <Parameter Name="val" Type="uint64_t"/></Intrinsic>

<Intrinsic Name="CD64" Expression="ALPH_B64U+(val?(val&amp;63):64)">
    <Parameter Name="val" Type="uint64_t"/></Intrinsic>

The second intrinsic CD64, where C stands for "conditional", will offset our alphabet pointer by 64 if the argument is zero. The 64th character in our alphabet is a null terminator, which won't print anything. This trick is useful for making some characters print conditionally without making a new tag.

Now, we can visualize our 60-bit ID into its base64 representation, e.g. #Gu4+8z. If the value is relatively small, the first few calls to CD64 will get an argument of zero and won't print anything. Base64 uses A as its 'zero' character, so this trims the leading As from an ID like #AAAAGu4+8z.

<Type Name="ID_60Bit">
    <!-- Print as 1 to 10 digits of Base64 -->
    <DisplayString>
        #{CD64((value)>>54),[1]nasb}{CD64((value)>>48),[1]nasb
        }{CD64((value)>>42),[1]nasb}{CD64((value)>>36),[1]nasb
        }{CD64((value)>>30),[1]nasb}{CD64((value)>>24),[1]nasb
        }{CD64((value)>>18),[1]nasb}{CD64((value)>>12),[1]nasb
        }{CD64((value)>>6 ),[1]nasb}{ D64((value)    ),[1]nasb}</DisplayString>
</Type>
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.