Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

VSEDebug - VS.NET Debugging Enhancement

0.00/5 (No votes)
25 Apr 2004 1  
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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here