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
{
static function IsSupportedTypeFunc(typename: String) : boolean
{
if(typename.StartsWith("std::list"))
{
return true;
}
return false;
}
static function EvaluateType(currentexpression,
currenttype,
currentname,
parentnode,
currentnode,
action,
vsdebugger) : Object
{
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)
{
curnode = newnode.Nodes[i];
}
else
{
curnode = null;
}
if(i == maxsize - 1)
{
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 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