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&63)">
<Parameter Name="val" Type="uint64_t"/></Intrinsic>
<Intrinsic Name="CD64" Expression="ALPH_B64U+(val?(val&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>
{(row + 'A'),c}. It's just another way of displaying a number.{(row + 'A'),d}{(col + '0'),d}would display6548. But there is no format specifier for displaying the character only.1Working with a statically sizedchararray (arrays aren't considered primitive types).2Encode the string into aDWORD(which works) and then take the address cast to achar*for display (taking the address won't work).3Moving any of this into a<Variable>with the expectation that debugger-controlled memory is more flexible (it isn't).