Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Changing Delegate signature in library to omit an argument does not break applications using it

Tags:

c#

Consider the following code in a class library:

public class Service
{
    public delegate string Formatter(string s1, string s2);

    public void Print(Formatter f)
    {
        Console.WriteLine(f("a", "b"));
    }
}

And here's a console application that uses it:

static void Main(string[] args)
{
    s = new Service();
    s.Print(Concat);
}

static string Concat(string s1, string s2)
{
    return string.Format("{0}-{1}", s1, s2);
}

So far it prints "a-b", just as one would expect.

Now, I change the class library as follows:

public class Service
{
    public delegate string Formatter(string s1);

    public void Print(Formatter f)
    {
        Console.WriteLine(f("a"));
    }
}

I.e. I removed one parameter from the delegate. I compile the class library only and overwrite the dll sitting next to the console app (console app is not recompiled). I'd expect that this is a breaking change in the library and if I execute the app, it finds the mismatch, resulting in some runtime exception.

In contrast, when I run the app there's no exception at all, and I get the stunning output "-a". When I debug, I can see that the Concat method (with 2 parameters) is called, the call stack level below shows Print calling to f("a") (one parameter), no error indication anywhere. Most interestingly, in Concat s1 is null, s2 is "a".

I also played around with different changes to the signature (adding parameters, changing parameter type) mostly with the same result. When I changed the type of s2 from string to int I got an exception, but not when the Concat method was called, but when it tried to call string.Format.

I tried it with .NET target framework 4.5.1 and 3.5, x86 and x64.

Can anyone answer whether this is the expected behaviour or a bug? It seems pretty dangerous to me.

like image 933
dzs Avatar asked Apr 07 '17 15:04

dzs


1 Answers

Here's a simpler repro - basically, I'm using the "under the hood" constructor on the delegate type (the one that the IL uses) to pass a method target with the wrong signature, and... it works fine (by which I mean it doesn't throw an exception - it behaves just like your code):

using System;

static class P
{
    static void Main()
    {
        // resolve the (object, IntPtr) ctor
        var ctor = typeof(Func<string, string>).GetConstructors()[0];

        // resolve the target method
        var mHandle = typeof(P).GetMethod(nameof(Concat))
            .MethodHandle.GetFunctionPointer();
        object target = null; // because: static

        // create delegate instance
        var del = (Func<string, string>)ctor.Invoke(new object[] { target, mHandle });
        var result = del("abc");
        Console.WriteLine(result); // "-abc"
    }
    public static string Concat(string s1, string s2)
    {
        return string.Format("{0}-{1}", s1, s2);
    }
}

This is not really an explanation. But it might be helpful if you want to ask someone more CLR-expert! I would have expected the delegate constructor to have complained loudly about the target being incorrect.

At a guess (pure speculation), it is a case of: if you're passing an IntPtr (native int), then you're entirely on your own - the code does the fastest thing possible. It does seem like a nasty trap for the unwary, though!

As for why s2 has the value and s1 is empty: I guess that is because the stack builds down (not up), hence in a two parameter method, arg1 is the parameter immediately adjacent to the previous position on the stack. When we pass a single value instead of two, we only put one value underneath, so s2 has a value, and s1 is undefined (could be garbage from previous code).

like image 52
Marc Gravell Avatar answered Nov 17 '22 11:11

Marc Gravell