Natvis files
Microsoft STL Visualizers are on GitHub
A couple of days ago, Microsoft announced that it published STL visualizers on Github. While it has always been possible to provide own .natvis
files for structures and classes, it is not something I thought too much about.
As it is now easier to find some examples, I looked at the documentation and decided to write down some.
Support in CMake
First of all, I checked how easy it is to integrate that information into a project. If using CMake, support has been added in version 3.7 🗄️.
It is as simple as adding the relevant files to the library where the class belongs
add_library(foo
<other source files>
datastructure.cpp
datastructure.natvis
)
or to the executable, if the data structure is not part of any library
add_executable(foo
<other source files>
datastructure.cpp
datastructure.natvis
)
Array-like containers
Suppose you have implemented something like std::array
, std::span
or ct_map
.
Without a .natvis
file, Visual Studio shows the "raw" data structure, which is actually good enough for most use-cases.
cmap {data=0x0113dad0 {('a', -2), ('b', -1)} }
m_data 0x0113dad0 {('a', -2), ('b', -1)}
[0] ('a', -2)
[1] ('b', -1)
With the following .natvis
file
<?xml version="1.0" encoding="utf-8"?>
<AutoVisualizer xmlns="http://schemas.microsoft.com/vstudio/debugger/natvis/2010">
<Type Name="ct_map<*>">
<DisplayString>{{ size={$T3} }}</DisplayString>
<Expand>
<Item Name="[size]">$T3</Item>
<ArrayItems>
<Size>$T3</Size>
<ValuePointer>data</ValuePointer>
</ArrayItems>
</Expand>
</Type>
</AutoVisualizer>
the output looks like
cmap { size=2 }
[size] 2
[0] ('a', -2)
[1] ('b', -1)
[Raw View] {...}
"Raw view" gives the possibility to inspect the content as if there were no .natvis
file.
The address of data
is not shown if not in Raw view
, because it is not logically part of the content of the class. The class has value semantic and is fully represented by m_data
, so there is no need to show the address as a separate/member variable.
In this case, the hard work is done by <ArrayItems>
, which has the nodes <Size>
and <ValuePointer>
. Both are self-explanatory, but for ct_map
I would have liked to get a view more similar to std::map
, which shows the key in the name column, and the value in the Value column:
map { size=2 }
['a'] -2
['b'] -1
[Raw View] {...}
std::map
accomplishes this by using TreeItems and a view helper:
...
<TreeItems>
<Size>_Mypair._Myval2._Myval2._Mysize</Size>
<HeadPointer>_Mypair._Myval2._Myval2._Myhead->_Parent</HeadPointer>
<LeftPointer>_Left</LeftPointer>
<RightPointer>_Right</RightPointer>
<ValueNode Condition="_Isnil == 0" Name="[{_Myval.first}]">_Myval,view(MapHelper)</ValueNode>
</TreeItems>
...
<Type Name="std::pair<*, *>" IncludeView="MapHelper">
<DisplayString>{second}</DisplayString>
</Type>
So ValueNode
seems to support a Name
attribute, just like Item
. I could not find anything about it in the documentation 🗄️, and I was not able to get it to work.
<?xml version="1.0" encoding="utf-8"?>
<AutoVisualizer xmlns="http://schemas.microsoft.com/vstudio/debugger/natvis/2010">
<Type Name="ct_map<*>">
<DisplayString>{{ size={$T3} }}</DisplayString>
<Expand>
<Item Name="[size]">$T3</Item>
<IndexListItems>
<Size>$T3</Size>
<ValueNode>data[$i]</ValueNode>
</IndexListItems>
</Expand>
</Type>
</AutoVisualizer>
Replacing <ValueNode>data[$i]</ValueNode>
with <ValueNode Name="[data[$i].first]">data[$i].second</ValueNode>
or even <ValueNode Name="[dummy]">data[$i].second</ValueNode>
makes the debugger ignore the declaration in the .natvis
, which is a good indicator that something is wrong.
Maybe the Name
attribute works only for nodes of TreeItems
. Unfortunately, I did not find out how to map it to an array.
In the meantime, I’m happy enough with the current .natvis
file, it’s already an improvement.
string-like containers
An instance of something like string_views
, std::string_view
or even std::string
would look similar to
ss {m_data=0x00d6da44 "123" }
m_data 0x00d6da44 "123"
[0] 49 '1'
[1] 50 '2'
[2] 51 '3'
[3] 0 '\0'
With a corresponding .natvis
file, it can be changed too
ss "123"
[size] 3
[0] 49 '1'
[1] 50 '2'
[2] 51 '3'
[3] 0 '\0'
[Raw View] {m_data=0x00d6da44 "123" }
In this case, the .natvis
file looks like
<?xml version="1.0" encoding="utf-8"?>
<AutoVisualizer xmlns="http://schemas.microsoft.com/vstudio/debugger/natvis/2010">
<Type Name="string_views::basic_string_views<char,*>">
<DisplayString>{m_data,[m_size] s8}</DisplayString>
<StringView>m_data,[m_size] s8b</StringView>
<Expand>
<Item Name="[size]">m_size</Item>
<Item Name="[data]">m_data</Item>
</Expand>
</Type>
<Type Name="string_views::basic_string_views<wchar_t,*>">
<DisplayString>{m_data,[m_size] su}</DisplayString>
<StringView>m_data,[m_size] sub</StringView>
<Expand>
<Item Name="[size]">m_size</Item>
<Item Name="[data]">m_data</Item>
</Expand>
</Type>
<Type Name="string_views::basic_string_views<*>">
<DisplayString>"{m_data,[m_size]}"</DisplayString>
<StringView>m_data,[m_size]</StringView>
<Expand>
<Item Name="[size]">m_size</Item>
<Item Name="[data]">m_data</Item>
</Expand>
</Type>
</AutoVisualizer>
Note 📝 | Because of <StringView> it is possible to copy the string as-is. Without it, it needs to be done from m_data . |
A first approach used <DisplayString>{m_data}</DisplayString>
instead of <DisplayString>{m_data,[m_size] s8}</DisplayString>
, and did not distinguish between char
and wchar_t
.
The first issue is that if the size is not specified, the debugger cannot know how long the string is. It can be assumed that it is \0
-terminated, but this is not true for std::string_view
and is generally problematic if there can be embedded \0
, like in std::string
.
The issue is simple to fix; just add the size [m_size]
. The resulting <DisplayString>
would work both for char
and wchar_t
.
I later noticed that it is beneficial to distinguish between the two types if the content happens to be non-ASCII. The distinction of the two types is needed because in one case I use the format specifier s8
, and in the other su
. Both are for specifying that the content is Unicode, but one is for the UTF-8 encoding, the other for UTF-16. If the string contains binary data or data in another encoding… then the string displayed in the debugger will be garbled.
PIMPL classes
Another benefit of a .natvis
file is that it gives a better view of a PIMPL class.
PIMPL is a technique for hiding implementation details and reducing compilation times, but it has several drawbacks, and one of them is adding another layer of indirection while debugging.
Suppose you have defined a class like
// header1
#include <memory>
struct foo;
struct bar{
bar();
~bar();
void increment();
int value() const;
private:
std::unique_ptr<foo> impl;
};
and a possible implementation would be
// implementation file
struct foo {
int data = 42; // whatever data is part of the implementation of bar
};
bar::bar() : impl(std::make_unique<foo>()){
}
bar::~bar() = default;
void bar::increment(){
++(impl->data);
}
int bar::value() const {
return impl->data;
}
then one could define the following .natvis
file
<?xml version="1.0" encoding="utf-8"?>
<AutoVisualizer xmlns="http://schemas.microsoft.com/vstudio/debugger/natvis/2010">
<Type Name="bar">
<DisplayString>{*impl}</DisplayString>
</Type>
</AutoVisualizer>
Some classes might avoid wanting to allocate memory from the default constructor. In those cases impl
can be nullptr
(at the expense of a more complex implementation), but it is possible to create an appropriate .natvis
file that shows the intended behavior of the class.
namespace{
constexpr const int default_data = 42;
}
struct foo {
int data = default_data; // whatever data is part of the implementation of bar
};
bar::bar() = default;
bar::~bar() = default;
void bar::increment(){
if (impl == nullptr){
impl = std::make_unique<foo>();
}
++(impl->data);
}
int bar::value() const {
return impl == nullptr ? default_data : impl->data;
}
then something like the following .natvis
might be more appropriate
<?xml version="1.0" encoding="utf-8"?>
<AutoVisualizer xmlns="http://schemas.microsoft.com/vstudio/debugger/natvis/2010">
<Type Name="bar">
<DisplayString Condition="impl==nullptr">data = 42</DisplayString>
<DisplayString>data = {impl->data}</DisplayString>
</Type>
</AutoVisualizer>
Another issue is that the debugger has apparently issues showing the data hold by foo
if this class is only forward-declared. Maybe there is a way to overcome this limitation. At the moment, I could only find the following workaround
<?xml version="1.0" encoding="utf-8"?>
<AutoVisualizer xmlns="http://schemas.microsoft.com/vstudio/debugger/natvis/2010">
<Type Name="bar">
<DisplayString Condition="impl==nullptr">{{data = 42}}</DisplayString>
<DisplayString>{*impl}</DisplayString>
</Type>
<Type Name="foo">
<DisplayString>{*(file.dll!foo*)this,!}</DisplayString>
</Type>
</AutoVisualizer>
It works but is fragile, as one needs to add the library (or executable) name to the natvis file.
It also does not work in the case of anonymous namespaces.
Other classes
Showing all member variables is probably good enough for most use-cases. But with .natvis
files it is possible to enrich the information shown. For example, a class could have a valid/invalid state, this could be shown directly in the debugger.
It is possible to specify conditionals and call functions, I’m looking forward to using those features in the future.
Issues
The documentation is unfortunately scarce, and of course http://schemas.microsoft.com/vstudio/debugger/natvis/2010 returns a 404.
In the example I made above, I relied on the implementation details of the classes, instead of, for example, using the member function size()
for getting the size. This is because one has to enable specific settings in Visual Studio, otherwise, functions are not executed. As functions could have side effects, this is a good thing but makes such .natvis
files less portable and harder to reuse.
Another "issue" is that it is not possible to add those debug information in the C++ source code directly thus one has to manually ensure that the source code and .natvis
file are synchronized.
On the other hand, one can edit the .natvis
file with Visual Studio while debugging in real-time, and the debugger will update the view at every save. If the debug information were stored with the source code, the debugger might have issues keeping the executing code and source file in sync. With the current approach, one does not need to recompile anything if the .natvis
file changes, which makes it more flexible when debugging, even if I doubt this is an operation that happens often.
Do you want to share your opinion? Or is there an error, some parts that are not clear enough?
You can contact me anytime.