Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Build custom string in natvis file out of separate chars

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...

like image 421
BTownTKD Avatar asked Dec 11 '25 11:12

BTownTKD


1 Answers

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>
like image 182
Evan Balster Avatar answered Dec 13 '25 00:12

Evan Balster