Using IL exception filters for a better debugger experience

Visual Studio 2005 brings with it a new technology -- Just My Code. One nice feature that comes with Just My Code is the ability to stop on exceptions that you care about, without stopping on all exceptions. Usually this just works for you because something in a library that you are using (say WinForms), is eating all the exceptions that you want to stop on. Since your code didn't handle the exception, the debugger will stop.

However, sometimes your program might want to be able to tell the debugger when to stop. It turns out that it is possible to do this by using IL exception filters. With exception filters, you can divide the world into critical exceptions (which you want to stop on), and expected exceptions (which you don't want to stop on).

I think its easiest to show this with an example. Suppose you have some C# code, and you always want to stop on NullReferenceExceptions, but you don't actually care about anything else. Here is how you could do this.

    enum DebuggerExceptionDisposition

    {

        DebuggerStop,

        DebuggerIgnore

    };

    delegate void UserRoutine();

    delegate DebuggerExceptionDisposition DebuggerExceptionFilter(Exception e);

    [DebuggerNonUserCode]

  static class ExceptionBoundry

    {

        [System.Runtime.CompilerServices.MethodImplAttribute(System.Runtime.CompilerServices.MethodImplOptions.ForwardRef)]

        static public extern Exception Invoke(UserRoutine routine, DebuggerExceptionFilter filter);

    };

    class Program

    {

        static void Main(string[] args)

        {

            UserRoutine userRoutine = delegate()

            {

                //throw new ArgumentException();

                throw new System.IO.FileNotFoundException();

            };

            DebuggerExceptionFilter filter = delegate(Exception e)

            {

                if (e is NullReferenceException)

                {

                    return DebuggerExceptionDisposition.DebuggerStop;

                }

                return DebuggerExceptionDisposition.DebuggerIgnore;

            };

            try

            {

                ExceptionBoundry.Invoke(userRoutine, filter);

            }

            catch

            {

            }

        }

    }

The idea behind ExceptionBoundry.Invoke is that non-user code that will catch any exception that 'filter' tells it to catch (by returning DebuggerStop).

Here is the implementation for ExceptionBoundry.Invoke:

.class private abstract auto ansi sealed beforefieldinit cs.ExceptionBoundry
extends [mscorlib]System.Object
{
.custom instance void [mscorlib]System.Diagnostics.DebuggerNonUserCodeAttribute::.ctor() = ( 01 00 00 00 )
.method public hidebysig static class [mscorlib]System.Exception
Invoke(class cs.UserRoutine routine,
class cs.DebuggerExceptionFilter 'filter') cil managed
{
.maxstack 2
.locals init ([0] class [mscorlib]System.Exception e)

    .try
{
ldarg.0
callvirt instance void cs.UserRoutine::Invoke()
leave.s EOF
} // end .try
filter
{
castclass class [mscorlib]System.Exception
stloc.0
ldarg.1
ldloc.0
callvirt instance valuetype cs.DebuggerExceptionDisposition cs.DebuggerExceptionFilter::Invoke(class [mscorlib]System.Exception)
ldc.i4.0
ceq
endfilter
}
{
leave.s EOF
} // end handler

EOF: ldloc.0 // return the first local
ret
} // end of method ExceptionBoundry::Invoke

} // end of class cs.ExceptionBoundry

To add it to your program, you could:

  1. Put it in another assembly
    -or-
  2. Add a post build step that will disassembly your program, add the new IL, and reassemble your program

To disassemble your program:
"<Visual Studio Directory>\SDK\v2.0\Bin\ildasm.exe" ..\cs\bin\debug\cs.exe /out=program.il /linenum

To assemble your program:
C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\ilasm.exe /quiet /exe /debug=impl /OUTPUT=..\ILExceptionFilter.exe program.il