Improving object view in D0

D0 makes it possible to view of all objects live in the current scope of the view() script call you placed in your C++ code:

This is nice, but it currently lacks a key feature: custom view definitions. This is a problem because types like std::vector are not shown as a list (m_drawing_contexts in the screenshot), but instead some internal structure that happens to be used by the MSVC STL implementation.

How Visual Studio shows your objects

Before understanding what we're dealing with, let's look at how Visual Studio's debugger handles this case.

Visual Studio utilizes XML files called "natvis", which contain definitions for how e.g STL types should be shown in the debugger, but they're quite limited in what they can do, because it supports only a subset of operations (it's not real code). For example, an std::vector in the natvis file is defined as:

<Type Name="std::vector<*>">
  <Intrinsic Name="size" Expression="(size_t)(_Mypair._Myval2._Mylast - _Mypair._Myval2._Myfirst)" />
  <Intrinsic Name="capacity" Expression="(size_t)(_Mypair._Myval2._Myend - _Mypair._Myval2._Myfirst)" />
  <DisplayString>{{ size={size()} }}</DisplayString>
  <Expand>
    <Item Name="[capacity]" ExcludeView="simple">capacity()</Item>
    <Item Name="[allocator]" ExcludeView="simple">_Mypair</Item>
    <ArrayItems>
      <Size>size()</Size>
      <ValuePointer>_Mypair._Myval2._Myfirst</ValuePointer>
      </ArrayItems>
  </Expand>
</Type>

The snippet effectively communicates that a type of std::vector<*> should be displayed as a list of items with size() items which begin at pointer _Mypair._Myval2._Myfirst (the natvis definition cannot depend on functions like data() being present, as they may not be linked in the executable, hence this is done via member access).

It is important to note that expressions like _Mypair._Myval2._Myfirst are not actually full C++, but are parsed by an another parser in VS that understands them.

This is okay, but isn't great when it comes to your own code - you have to write XML, load it up in VS etc.

How D0 aims do it

D0 utilizes the Angelscript scripting language to allow you to interface with your C/C++ codebase at runtime, so the natural way of implementing custom views is making them just a function in your scripting code. That would mean you can employ any logic you want to view your objects.

This is tricky, because it means D0 has to compile code in order to be able to show the object according to your definition. This would not be a problem, if all types were known and the underlying PDB was relatively small. The PDB file generated by MSVC is used to discover all types, functions and classes available in your codebase. This information is then interpreted and used to register those symbols in Angelscript.

In practice, D0 doesn't actually register any types present in your codebase until you start referencing them in your script code. Then, when the compiler encounters a type it doesn't know about, it asks D0 to find it in the PDB which then resolves it.

This is essential as your C/C++ codebase may consist of thousands/millions of symbols, so D0 has to minimize the number of symbols being registered at runtime.

Let's say we want to define how a std::vector should be displayed in the object view. We need to be able to define some kind of pattern that matches all possible vector instantiations. The XML defines this as std::vector<*>, but because Angelscript does not support templates like that, we will have to add some kind of language/preprocessor feature that allows for defining templates:

void myView(R"std::vector<*>"&inout vector)
{
    // view the list here
}

This imaginary syntax defines a myView function that accepts any type that matches a regex type of std::vector<*>. Here, the compiler would see that the function takes a template argument and would skip compiling the function, until e.g a type std::vector<int> got referenced somewhere by the script. The compiler would then instantiate the function and make D0 aware that myView() should be called for std::vector<int>, instead of displaying the default view.

The benefit of all this is that you wouldn't need to write any XML, and you can write any code you want (e.g. in the future you could display images, colors etc).

The downside is that it is quite complex to implement:

  • the Angelscript compiler has to instantiate functions at runtime
  • a type displayed in the object view may not be necessarily registered by the compiler (because the script code might not use it at all), so the object view logic would also need to tell the compiler to instantiate those functions manually
  • D0 currently uses the DIA SDK provided by Visual Studio to fetch information from PDB files, but it can be very slow depending on the PDB. A faster solution would need to be implemented for a good experience.

That said, this is not an actual feature in D0 yet, as I'm still figuring out all the implementation details.

Thanks for reading!