Puzzle involving unwound stacks on dynamic invoke

2019-08-04 05:47发布

问题:

This is a new attempt to pose a version of a question asked less successfully this morning.

Consider the following program, which we'll run once inside Visual Studio 2010 and once more by double-clicking the executable directly

namespace ConsoleApplication3
{
    delegate void myFoo(int i, string s);

    class Program
    {
        static void Main(string[] args)
        {
            Foo(1, "hello");
            Delegate Food = (myFoo)Foo;
            Food.DynamicInvoke(new object[] { 2, null });
        }

        static void Foo(int i, string s)
        {
            Console.WriteLine("If the next line triggers an exception, the stack will be unwound up to the .Invoke");
            Console.WriteLine("i=" + i + ", s.Length = " + s.Length);
        }
    }
}

When the exception in Foo triggers while running VS, the debugger shows the stack correctly and shows that the problem occured on the second WriteLine in Foo.

But when the exception occurs while running the executable directly, one gets a little popup window from the CLR indicating that the program threw an unhandled exception. Click debug and select the VS debugger. In this case, the stack unwinds up to the point of the most recent .DynamicInvoke and when you attach with the debugger, the stack context that existed at the time of the exception has been partially lost.

It does exist, in a limited form, within the "inner exception" portion of the exception event. You click to expand the associated information and find the line number where the problem occured. But obviously local variables and other context will be gone.

If one tries the same thing but without the .DynamicInvoke (for example, call Foo(1, null) on line 1 of Main), still by double-clicking the .exe file, we DO get the correct line number when the debugger attaches. Similarly if the application is launched by clicking on the .exe, but then the debugger is attached before the exception gets thrown.

Does anyone know how an application using dynamic reflection/invocation could avoid this problem? In my intended use case, in a system the name of which I won't mention here, I cannot predict the type signature of the object that will be used in the .DynamicInvoke, or even the number of arguments that will be employed, hence static typing or even generics aren't a way out of this.

My question is this: does anyone know why we get such different behaviors when running directly from the debugger versus when attaching to the program after the exception is thrown?

回答1:

As per the comments, whether you see the NullReferenceException as unhandled depends on whether it's handled. Here are some ways to call Foo, the first three will leave the exception as unhandled, the last two will handle the NullReferenceException by wrapping it, and throwing a new exception.

using System;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace ConsoleApplication3
{
    delegate void myFoo(int i, string s);

    internal class Program
    {
        private static void Main(string[] args)
        {
            Foo(1, "hello");

            // From a delegate
            try
            {
                Delegate Food = (myFoo)Foo;
                ((dynamic)Food).Invoke(2, null);
            }
            catch (NullReferenceException ex)
            { Console.WriteLine("Caught NullReferenceException at " + ex.StackTrace); }

            MethodInfo Foom = typeof(Program).GetMethod("Foo", BindingFlags.Static | BindingFlags.NonPublic);

            // From a MethodInfo, obtaining a delegate from it
            try
            {
                Delegate Food = Delegate.CreateDelegate(typeof(Action<,>).MakeGenericType(Foom.GetParameters().Select(p => p.ParameterType).ToArray()), Foom);
                ((dynamic)Food).Invoke(2, null);
            }
            catch (NullReferenceException ex)
            { Console.WriteLine("Caught NullReferenceException at " + ex.StackTrace); }

            // From a MethodInfo, creating a plain Action
            try
            {
                Expression.Lambda<Action>(
                    Expression.Call(
                        Foom,
                        Expression.Constant(2),
                        Expression.Constant(null, typeof(string)))).Compile()();
            }
            catch (NullReferenceException ex)
            { Console.WriteLine("Caught NullReferenceException at " + ex.StackTrace); }

            // MethodBase.Invoke, exception gets wrapped
            try
            {
                Foom.Invoke(null, new object[] { 2, null });
            }
            catch (NullReferenceException)
            { Console.WriteLine("Won't catch NullReferenceException"); }
            catch (TargetInvocationException)
            { Console.WriteLine("Bad!"); }

            // DynamicInvoke, exception gets wrapped
            try
            {
                Delegate Food = (myFoo)Foo;
                Food.DynamicInvoke(2, null);
            }
            catch (NullReferenceException)
            { Console.WriteLine("Won't catch NullReferenceException"); }
            catch (TargetInvocationException)
            { Console.WriteLine("Bad!"); }
        }

        private static void Foo(int i, string s)
        {
            Console.WriteLine("i=" + i + ", s.Length = " + s.Length);
        }
    }
}


回答2:

Actually answered by @hvd:

((dynamic)Food).Invoke(2, null);

solves my problem in one line of code. Thanks!