Wednesday, 18 January 2012

Customize display of delegate in Visual Studio Debugger.

I was working on a custom expression compiler that had a simple interface such as:

Func<TContext, TOutput> Func(String expression);

defined in an appropriately generic class.

The problem, however, is that the resulting Func does not really show us anything of the original expression that "went in".

Now, Microsoft provides hooks for customizing debugger display, which is great.. but limited.

First I thought it may be possible to attach an attribute "at run time", as I am using Linq Expressions trees to achieve the compilation, perhaps I can add a DebuggerDisplayAttribute to the result. It appears, however, that there is no API for this (it seems that this *may* be possible if one uses ILGenerator, so I find it odd that this is not available if that were the case).

So we can't attach an attribute to the Func delegate type, so perhaps a custom delegate type would work. In some ways this is a shame as I don't like deviating from the Func type but this seemed a small sacrifice for a much improved debugging experience.

The benefit of a custom delegate type would have been that we could add a DebuggerTypeProxyAttribute to proxy to our debugger class. Unfortunately this attribute cannot be applied to delegate types.

If we must use DebuggerDisplayAttribute, the problem becomes: how do we access data about our expression given a delegate instance? The class is (damn it) sealed, so we can't create our own. The solution I came up with is to wrap the compiled func in a delegate that is instance bound to a nested (internal) instance as follows:

Define our internal (to our expression parser class) nested type:

      internal class _ExpressionDebugContext
      {
         private Func<TContext, TOutput> _mWrappedFunc;
 
         public String Expression;
 
         public _ExpressionDebugContext(String expr, Func<TContext, TOutput> func)
         {
            _mWrappedFunc = func;
            Expression = expr;
         }
 
         public override string ToString()
         {
            return Expression;
         }
 
         public TOutput RunFunc(TContext input)
         {
            return _mWrappedFunc(input);
         }
      }

now when we have compiled func result, we can return it as follows:
   return new _ExpressionDebugContext(expr, tCompiledResult).RunFunc;

and finally we can use DebuggerDisplay:

[DebuggerDisplay("{Target.ToString()}")]
   public delegate TOutput _ExpressionDelegate<TContext, TOutput>(TContext context);


In other words we return a delegate that has its "this" bound to an instance of _ExpressionDebugContext. We can display properties of this using the debugger string "{Target.ToString()}", where Target refers to "this". The actual delegate returned simply passes through to the func that we compiled.

Downside: there is probably a small performance overhead due to the extra call. In my case, however, this was well worth it as this kind of performance concerns are not relevant, but a good debug experience is! Furthermore let's just assume the JITer kicks ass and inlines all this stuff.

Finally, LambdaExpression does allow us to compile to a MethodBuilder. MethodBuilder inherits from MethodInfo, and MulticastDelegate should be able to build a delegate given a MethodInfo instance. This is a bit more work as creating a MethodBuilder takes some effort, but may be another angle to do this.

If you found easier/better ways to do this, I would be very interested to hear from you!

1 comment:

  1. Very interesting and thought provoking post! Of course, it might have been possible pass an Expression Tree to ExpressionDebugContext, thus avoiding having to write the string that described the expression manually), but that would incur a performance overhead of compiling the expression on the fly.

    My only comment is that for my tastes, it might have been nicer to write an explicit cast from _ExpressionDebugContext to the underlying delegate type, making it easier to pass around to 3rd party code.

    - Omer Raviv, BugAid Software

    ReplyDelete