Making a type visualizer for a custom array class
In this tutorial we will create a type visualizer for our own vector-like array class. Before you begin, please follow the getting started tutorial to make a very basic “dummy” visualizer and ensure that it works and can be debugged.
- Open the MyTypeVisualizer project (C# visualizer from the previous tutorial) and run it. A new instance of Visual Studio will start (and will be debugged by the first instance of Visual Studio).
- Open the C++ project from the previous tutorial and replace the main file with the following:
#include <stdio.h> #include <assert.h> template <class _Type> class VeryBasicArray { private: size_t m_Count; _Type *m_pData; public: VeryBasicArray(size_t count) : m_Count(count) , m_pData(new _Type[count]) { } ~VeryBasicArray() { delete[] m_pData; } _Type &operator[](size_t index) { assert(index <= m_Count); return m_pData[index]; } size_t GetCount() { return m_Count; } }; int main() { VeryBasicArray<int> test(3); for (int i = 0; i < test.GetCount(); i++) test[i] = i * 100; return 0; }
- Build the project. Place a breakpoint at the “return 0” line and start debugging. When the breakpoint is hit, hover the mouse over “test” to see its value:Notice how the value of “test” has been changed to “Hello, Visual Studio” by the basic visualizer from the previous tutorial.
- Carefully look at the values displayed by Visual Studio when you hover your mouse over “test” (or add it to the Watch window). Each tree node corresponds to the VisualGDB IExpression interface. The annotated screenshot below shows which element of IExpression corresponds to the main displayed elements:
- The type visualizers are based on the Expression Filter mechanism. Expression Filters can replace arbitrary IExpression objects with their own implementations. In most of the cases creating an instance of StaticExpressionFilter and specifying which fields to override should be sufficient. We will now make a visualizer that will use the m_Count value to display the collection elements.
- The first step will be to query the m_Count value as an integer to know how many elements to query. Replace the body of the DoAttach() method with the following:
protected override IExpression DoAttach(IExpression expr, IExpressionEvaluator evaluator) { string countExpression = string.Format("({0}).m_Count", expr.FullNameForEvaluator); var value = evaluator.EvaluateExpression(countExpression, "int") as ExpressionValue.Integral; if (value == null) return null; ulong count = value.Value; return new StaticExpressionFilter(expr) { ValueOverride = new ExpressionValue.Custom(string.Format("An array with {0} items", count)) }; }
Note that you can use “Break All” command in the outer Visual Studio to stop the inner Visual Studio, and then edit the visualizer code directly. Edit-and-continue will patch the visualizer on-the-fly, so you won’t need to restart Visual Studio or even restart the debugged C++ program!
- Hover the mouse over “test” again. You will see the :
- Note how we have queried the element count: first, we derived the GDB expression for it using IExpression.FullNameForEvaluator (would be ‘test.m_Count’ in this case). Then we evaluated it using the IExpressionEvaluator interface. Finally we ensured that the returned value is a valid integral number (VisualGDB parses it automatically and returns ExpressionValue.Integral) and extracted the integral value (of type ulong) from it.
- Now when we know the count and know that i-th element of the array can be accessed as test.m_pData[i] it’s time to create IExpression objects for all those values. Replace the return statement in the DoAttach() method with the following code:
List<IExpression> children = new List<IExpression>(); for (ulong i = 0; i < count; i++) { string itemExpression = string.Format("({0}).m_pData[{1}]", expr.FullNameForEvaluator, i); var item = evaluator.CreateExpression(itemExpression); if (item != null) children.Add(item); } var result = new StaticExpressionFilter(expr) { ValueOverride = new ExpressionValue.Custom(string.Format("An array with {0} items", count)) }; result.ChildrenOverride = new StaticChildProvider(children.ToArray()); return result;
- Hover the mouse over “test” again:
- The problem now is that the user won’t be able to access/modify the original m_Count and m_Data values. To provide this possibility we will add an “Actual members” node. Instead of making our own implementation of IExpression we will simply reuse the VirtualExpressionNode class provided by VisualGDB:
children.Insert(0, new VirtualExpressionNode("[Actual members]", "", expr.Children));
Note that we have simply “moved” the IExpressionChildProvider from the original expression node to the newly created node. We did not have to query any actual members at this point.
- Add the “test” to the Watch window and expand the results:
- Note that the element names still have the format “(test).m_pData[i]” that corresponds to the raw GDB expressions, but is not very user-friendly. To fix this, we could wrap the child expressions returned by IExpressionEvaluator.CreateExpression() using StaticExpressionFilter and override the ShortName.
- Furthermore, when dealing with a 1000-element array, our implementation would call CreateExpression() 1000 times even if the user just hovered the mouse over the array or had the “Autos” window open. To fix that we could make our own implementation of IExpressionChildProvider creating child nodes on demand. To facilitate that VisualGDB provides the ArrayChildProvider class that does everything automatically:
protected override IExpression DoAttach(IExpression expr, IExpressionEvaluator evaluator) { string countExpression = string.Format("({0}).m_Count", expr.FullNameForEvaluator); var value = evaluator.EvaluateExpression(countExpression, "int") as ExpressionValue.Integral; if (value == null) return null; ulong count = value.Value; var actualMembers = new VirtualExpressionNode("[Actual members]", "", expr.Children); var result = new StaticExpressionFilter(expr); result.ValueOverride = new ExpressionValue.Custom(string.Format("An array with {0} items", count)); string format = "(" + expr.FullNameForEvaluator + ").m_pData[{0}]"; result.ChildrenOverride = new ArrayChildProvider(evaluator, format, 0, (int)count, actualMembers); return result; }
- Delete “test” from the Watch window and add it once again:
- You can get an overview of the raw commands that are being sent to GDB when your expressions are evaluated by selecting “All GDB interaction” in the GDB Session window:
Source code
You can download the source code for the example in this tutorial here. The code in the archive is provided under the BSD license (no restrictions, no warranty).