VSEDebug - VS.NET Debugging Enhancement






4.92/5 (34 votes)
Aug 7, 2003
9 min read

175429

2187
VSEDebug is a VS.NET debugger add-in that adds the ability to debug complex types in simpler form.
Introduction
Ever since I started using the STL heavily, I was constantly annoyed at the
inability of the Visual Studio debuggers, or any debugger, for that matter, to
properly parse them. Sure, I could see how many elements were in a vector, but I
couldn't actually view them outside of code. And I could look at all the
elements of a std::list
, if I didn't mind having a treelistview
that was larger than a doublewide. Other types were even more ridiculous,
particularly maps, stacks, and queues. So, fed up with this, I decided to do
some research and attempt to find a solution. As it turns out, I did, and a
Visual Studio Add-In of never before seen functionality, was born.
The Add-In is an attempt to emulate the native Visual Studio.NET debugger windows, augmenting them with custom type functionality. It is composed of four windows, VSE Locals, VSE Autos, VSE Watches, and VSE This. They act almost exactly the same as the native windows, minus some functionality in the autos window for viewing the return values of functions.
Visual Studio, since version 6, has included a simple method by which to modify and customize the debugger. By editing autoexp.dat and writing a simple DLL, users can alter the way Visual Studio displays types in the hover-tooltips and in the debugger windows. However, this method is insufficient for our needs. First of all, it operates on raw memory rather than on typed data. One must know everything about a type before attempting to parse it. This means that any use of templates may become a problem. Using a special syntax, one can generalize over a set of types (<*>). However it is impossible to concretely determine the specific type you are generalizing for in a given call. This prohibits effective parsing of the STL containers. Furthermore, altering the string does not alter the way items are expanded in the debug windows, so you are limited to small types if you wish to use it effectively. Finally, the current method is designed only to work with C++, and would be unpredictable in managed languages, since the .NET Runtime has control over memory layout. This leaves out other major languages such as C#. All in all, the current customization method is insufficient for serious use. For more information, see MSDN.
Building the Add-In - A Quick Look at the Automation SDK
The Automation SDK that ships with Visual Studio.NET is the easiest way to do most extensions of the IDE. It's fairly simple, well designed and well documented. The SDK is divided into two major parts, the Common Environment Object Model and the Visual Studio Debugger debugger Object Model. There is also an extensibility model for VB.NET and C# for working with projects.
Each object model is a set of interfaces, most of them managed. Through both inheriting these interfaces as well as using them directly, you can extend the environment by intercepting and handling message handlers, adding toolbar items, and manipulating the debugger in various ways. There are two types of modifications to the IDE: Macros and Add-Ins. Macros are written in VB.NET and can use the Automation SDK as well as normal .NET framework functionality. Use Macros to automate quick and dirty tasks. For a more permanent solution, use an Add-In. Add-Ins are persistent and can be started automatically when the IDE loads. They also have access to some run time methods and properties of the Automation SDK that Macros don't.
Building the Add-In - The Debugger Object Model
This Add-In makes extensive use of the Debugger SDK and, in particular, the
GetExpression
method. GetExpression
evaluates a
language construct, such as a variable name or a basic expression such as X + Y,
in the context of the current stack frame. A stack frame refers to the state of
a program in a certain function. So the function main()
would be a
stack frame, and all the variables which main introduces or that are accessible
to main()
can be used in GetExpression
.
GetExpression
returns a reference to an object of the Expression
interface, which provides information about the expression or variable just
evaluated. It provides the type, value, name, and sub members of the expression.
So, an instance of a class will have the class's sub members.
There are a couple things to keep in mind when using the Debugger Object
Model. First, GetExpression
isn't that fast. A call to
GetExpression
can take up to a couple of milliseconds, and the
evaluation of an Expression's sub members even more than that. Use sparingly.
Secondly, the debugger runs in a separate process from your Add-In. While this
may seem rather harmless, this means that the debugger can start execution while
your Add-In is still using it to gather information, resulting in many of the
Debugger Object Model's methods and properties throwing when you try to access
or use them. As far as I know, there isn't an easy workaround for this issue. If
your Add-In takes a lot of time to execute, such a thread "misalignment" is
bound to happen. The most obvious solution is to create a separate thread that
listens to the debugger messages and throw the debugger out of run mode if your
thread is still executing it.
Building the Add-In - Basic Framework
The Add-In is composed of several parts. First, there is a main module which performs menu and toolbar setup, as well as inherits the IExtensibility2 interface, which is necessary for Visual Studio to call VSEDebug an Add-In. Then there are four window classes that emulate the functionality of each of the four Visual Studio.NET debugger windows. Next there is the evaluation framework, which provides variable evaluation either automatically or through a script. Finally, there is the script module which wraps the .NET Framework scripting functionality for my purposes.
The four debugger windows are built using a modified version of the
TreeListView
control. The original TreeListView
can be
found on this site here.
Building the Add-In - The Evaluation Algorithm
The VSEDebug debugger windows work in a similar manner to the native debugger
windows. The window requests several "base" variables to be evaluated based on
the current stack frame, and the evaluation framework fills in the necessary
information and builds the tree. Each window retrieves its "base" contents from
various locations. The locals window gets the local variables in the current
stack frame from the Debugger SDK, the This window simply evaluates the
expression "this" in GetExpression
, the Watch window maintains a
list of watched expressions, and the Autos window does some rough parsing and
testing on the actual lines of code, extracting relevant variable names.
A basic evaluation is performed as follows. Let's take the variable
ThisIsAVarable
for instance. Let's say ThisIsAVariable
is a local variable in the main()
function. When the debugger is
the main() function, it will report that ThisIsAVariable
is a
member of the current stack frame. The Locals window will pick that up and start
the evaluation of this variable by calling Evaluate(), sending various
information, such as it's type (as a string), it's name, and the tree node which
will represent this variable.
The Evaluate()
function begins by determining whether a given
script is available to handle this type of variable. Scripts are loaded from the
/scripts directory at run time. Each script contains a function to test whether
it supports the type of variable being handed to it, usually through a straight
string comparison, and a function to handle the variable itself, should it
support it. More information is presented in the next section.
If the variable is supported by a script, the Evaluate function passes off
control of the evaluation to this script, otherwise, it uses a default evaluator
(EvaluateUnsupportedType
) that performs no special processing on
the variable in question. The evaluation function's task is to set up the
current node with information about the expression in question, and evaluate sub
members, if any. SetupBasic()
provides a convenient way to evaluate
the expression of the current node, which is passed as a string, set the tree
node's Text
and SubItem
properties, and determine
whether the tree has been expanded deep enough to warrant further evaluation of
sub members. After the basic setup, the script or default evaluator passes
control of the evaluation off to Evaluate for any sub items, where the process
is repeated. The evaluation function can also pass control off to the
Divide()
function, which servers a proxy for Evaluate()
,
partitioning an expression's sub members into groups. This is an optimization
tool and a convenient way to view variables with many sub members, as only sub
items you need to see have to be expanded. Evaluation continues until the
expression being evaluated cannot be seen because of lack of tree expansion or
until there are no more sub members to evaluate.
Building the Add-In - Scripting
Scripting is an essential part of the VSEDebug Add-In. It provides a simple way to add support for new types and maintain old ones without recompilation of the main program. The scripts are JIT'd at startup, so they are fairly fast, and .NET has a built in script engine for JScript.NET and VBScript.NET with bindings to all the .NET Framework functionality, so it's very convenient.
To maintain simplicity, I used the source code from Script
Happen.NET as a wrapper for the main .NET Framework script engine
functionality, then wrapped it myself with simple functions to call the type
comparison (IsSupportedTypeFunc
) and
evaluation(EvaluateType
) functions and gather their return values.
As mentioned above, a script must contain a class named parser
and inside it two static functions, IsSupportedTypeFunc()
and
EvaluateType()
. IsSupportedTypeFunc()
takes a string
and returns whether the script supports the type in question. This is usually a
simple string comparison. EvaluateType()
takes various arguments,
including the expression name, the expression type, and the current node that
this expression will occupy and then evaluates the expression as explained in
the previous section.
Below is a simple script that parses the STL std::list
type.
Note that when using GetExpression
for these kinds of things, you
must get very low level in order for it to return valid values. So, this script
is implementation specific for the STL, but the Visual Studio.NET version of the
STL should work.
import vsedebug;
import SynapticEffect.Forms;
import System;
class parser
{
/*
* The purpose of this function is to return whether the given
* script supports the type indicated
* by typename. typename is a .NET framework String object.
* Return true if the type is supported, or false
* otherwise
*/
static function IsSupportedTypeFunc(typename: String) : boolean
{
if(typename.StartsWith("std::list"))
{
return true;
}
return false;
}
/*
*
*/
static function EvaluateType(currentexpression,
/*The current expression as a string that
represents this current item*/
currenttype, /*The type of the current
expression as a string*/
currentname, /*This is the aesthetically pleasing name*/
parentnode, /*The parent TreeListNode of the current node*/
currentnode, /*The current TreeListNode*/
action, /*The current action being performed on this node*/
vsdebugger) : Object
/*The visual studio debugger object type EnvDTE.Debugger.*/
{
// two new variables of the TreeListNode variety, to be used in
// the calculations and information retrieval
var newnode : SynapticEffect.Forms.TreeListNode;
var curnode : SynapticEffect.Forms.TreeListNode;
var exp = null;
var currentlistitem : String;
var childexp : String = null;
var childtype : String = null;
var child = null;
var maxsize = 0;
newnode = vsedebug.VSEDebugEvaluator.SetupBasic(currentexpression,
currenttype, currentname,
TreeListNode.TreeListNodeType.supported,
parentnode, currentnode, action);
if(newnode == null)
{
return null;
}
maxsize = 0;
try
{
exp = vsedebug.VSEUtil.VSDebugger.GetExpression(
newnode.PathToVariable + "_Mysize", false, -1);
}
catch(e)
{
}
try
{
eval("maxsize = " + exp.Value);
}
catch(e)
{
return null;
}
if(!newnode.IsExpanded && maxsize > 0)
{
vsedebug.VSEDebugEvaluator.Divide("DUMMY", "DUMMY",
newnode.Text+"[0]", newnode,
newnode,
vsedebug.VSEDebugEvaluator.EvaluateAction.evaluate,
0, true);
return newnode;
}
if(maxsize > 0)
{
try
{
childtype = vsedebug.VSEUtil.VSDebugger.GetExpression(
newnode.PathToVariable +
"_Myhead->_Next->_Myval", false, -1).Type;
}
catch(e)
{
}
}
currentlistitem = newnode.PathToVariable + "_Myhead->_Next->";
for(var i = 0; i < maxsize; i++)
{
childexp = currentlistitem + "_Myval";
if(i < newnode.Nodes.Count)
{
//this node already exists, so we're going to ovverwrite it
curnode = newnode.Nodes[i];
}
else
{
curnode = null;
}
if(i == maxsize - 1)
{
//The last item to be evaluated.
//Ensures that the window is updated.
vsedebug.VSEDebugEvaluator.Divide(childexp, childtype,
newnode.Text+"["+i.ToString()+"]", newnode, curnode,
vsedebug.VSEDebugEvaluator.EvaluateAction.evaluate,
i, true);
}
else
{
vsedebug.VSEDebugEvaluator.Divide(childexp, childtype,
newnode.Text+"["+i.ToString()+"]",
newnode, curnode,
vsedebug.VSEDebugEvaluator.EvaluateAction.evaluate,
i, false);
}
currentlistitem = currentlistitem + "_Next->";
}
vsedebug.VSEDebugEvaluator.CleanDivide(newnode, maxsize);
//return the node created
return newnode;
}
}
Points of Interest
Writing Add-Ins and Macros for VS.NET can be very rewarding. It can speed up tasks that were originally very tedious and annoying. All it takes is a little bit of ingenuity and patience. Happy Coding.
History
- April 13th 2004 - Update to VSEDebug adding font selection tool and "un-beta'ing" it
- July 22nd 2003 - First Release of VSEDebug