11

A few days ago I asked why delegates are reference types, based on my misguided notion that all you need for a delegate are two references: one to an object, and one to a function. What I completely overlooked (not because I wasn't aware, simply because I forgot) is that in .NET, delegates are at least partially in place to support events as a built-in implementation of the Observer pattern, which means that every delegate supports multiple subscribers by way of an invocation list.

This got me thinking, delegates really play two different roles in the .NET world. One is that of a humble function pointer, such as:

Action<string> writeLine = Console.WriteLine;

The other is that of an observable:

textBox.TextChanged += HandleTextChanged;

The existence of an invocation list seems to be exclusively for the second role, as in cases like the simple writeLine example above you generally don't even think about subscribers.

So really, it seems to me there could be two different "kinds" of delegates: the "function pointer" kind, and the "observable" kind. The former, it seems to me, could be a value type.

Now, I'm not arguing that this should be the case, if it's even possible. I am sure there would be a lot of downsides to making this distinction between regular and multicast delegates, such as the likely high frequency of boxing if delegates were value types, the possible need to introduce a new keyword (multicast?), the inevitable developer confusion, etc. What I'm really curious to know is simply if it would be possible, from a CLR perspective, to have a value type that could act as a function pointer.

I guess another way of asking this would be: is System.Delegate, with its invocation list and all, basically a fundamental CLR type; or is it a wrapper around a simpler "function reference" type that simply isn't exposed by any CLR languages?

I apologize for all of the informal terms I've used that may have confused some of the more educated developers out there.

Community
  • 1
  • 1
Dan Tao
  • 125,917
  • 54
  • 300
  • 447

4 Answers4

11

In the very early days of the CLR there used to be a distinction between System.Delegate (function pointer like) and System.MulticastDelegate (event like). That was scrapped before .NET 1.0 shipped, there is no way to create an instance of a delegate type that derives from Delegate. It just wasn't necessary. MulticastDelegate was optimized to only create its invocation list when there's more than one subscriber. System.Delegate was preserved for some reason, probably too much work to remove it.

Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • So does this mean that the original `System.Delegate` type *could* have been a value type, or rather, could have been *subclassed* by value types, much like `System.Enum`? (Presumably it still *wouldn't* have, though, for probably more reasons than I can think of?) – Dan Tao Oct 28 '11 at 16:51
  • 3
    Erm, difficult, it is the base class for MulticastDelegate. Hard to reason about something that never truly existed. – Hans Passant Oct 28 '11 at 17:02
4

In my opinion, in the vast majority of cases, .NET developers find the 'unified' support for unicast / multicast and closed / open-instance well worth the minor overheads involved. It's unfortunate if you fall into the minority case, but there are ways around it if you don't mind breaking idioms and reinventing a lot of wheels. More to come on that later.

Actually, the intent of the question isn't all that clear, but I will try to tackle the individual points.

What I'm really curious to know is simply if it would be possible, from a CLR perspective, to have a value type that could act as a function pointer.

Of course. In fact, delegates are built using native int sized value-type function-pointers (IntPtr in the managed world). All of the type-safe bells and whistles are built on top of that.

The IL for your Action<string> writeLine = Console.WriteLine; example looks something like this:

// Push null-reference onto stack.
// (Console.WriteLine is a static method)
ldnull

// Push unmanaged pointer to desired function onto stack.
ldftn void [mscorlib]System.Console::WriteLine(string)

// Create delegate and push reference to it onto stack.
instance void [mscorlib]System.Action`1<string>::.ctor(object, native int)

// Pop delegate-reference from top of the stack and store in local.
stloc.0 

where the Action<T> constructor is very conveniently declared as:

// First arg is 'this' for closed-instance delegates.
// Second arg is pointer to function.
public Action(object @object, IntPtr method);

I guess another way of asking this would be: is System.Delegate, with its invocation list and all, basically a fundamental CLR type; or is it a wrapper around a simpler "function reference" type that simply isn't exposed by any CLR languages?

Well, it's both a fundamental CLR type (in the sense that the execution-engine knows about it and treats it specially) and a "wrapper" around a "function reference" - System.Delegate stores a pointer to the function as a field (and an object reference for closed-instance delegates). Multicast-support, through its subclass System.MulticastDelegate, is obviously a lot more complicated because of the need to store the multicast invocation-list. Unification is achieved through the fact that all delegate-types in the wild must inherit from System.MulticastDelegate.

And I would argue that the "function reference" is exposed to CLR languages - it's possible to get a MethodInfo representing the method through the delegate's Target property, and from there the associated method-handle and function-pointer.


But you probably know all of this already. It seems to me like your real question is:

How would I build a lightweight, type-safe, delegate-like value-type that stores nothing but a pointer to a managed function in .NET?

Given everything I've mentioned so far, it's really quite easy:

// Cool but mostly useless lightweight (in space, not time)
// type-safe delegate-like value-type. Doesn't support closed-instance scenarios 
// in the interests of space, but trivial to put in if desired.
public struct LeanDelegate<TDelegate>
{
    // The only storage required.
    private readonly IntPtr _functionPointer;

    public LeanDelegate(TDelegate source)
    {
        if (source == null)
            throw new ArgumentNullException("source");

        var del = source as Delegate;

        if (del == null)
            throw new ArgumentException("Argument is not a delegate", "source");

        if (del.Target != null)
            throw new ArgumentException("Delegate is a closed-instance delegate.", "source");

        if (del.GetInvocationList().Length > 1)
            throw new ArgumentException("Delegate is a multicast delegate.", "source");

         // Retrieve and store pointer to the delegate's target function.
        _functionPointer = del.Method.MethodHandle.GetFunctionPointer();
    }

    // Creates delegate-instance on demand.
    public TDelegate Delegate
    {
        get
        {
            if (_functionPointer == IntPtr.Zero)
                throw new InvalidOperationException("Uninitialized LeanDelegate instance.");

            // Use the aforementioned compiler-generated constructor 
            // to generate the delegate instance.
            return (TDelegate)Activator.CreateInstance
                              (typeof(TDelegate), null, _functionPointer);
        }
    }
}

And then you can do:

 var del = new LeanDelegate<Action<string>>(Console.WriteLine);
 del.Delegate("Hello world");

What have you gained? The ability to store a pointer to an arbitrary static-method (or instance-method if the delegate is an open-instance one) and execute it in a type-safe manner, all in machine-pointer size space (excluding temporary allocations).

What have you lost? Closed-instance capability. Multicast. Direct compatibility with many other APIs. Speed (almost certainly), although workarounds are conceivable. The love of developers who will use your API.

Ani
  • 111,048
  • 26
  • 262
  • 307
  • Note that it would be possible for the struct to just store a `MethodInfo`. Since method-infos are cached, this should offer a similar space-profile. But you want a real, honest-to-goodness function-pointer. – Ani Dec 10 '11 at 07:45
  • I wish Delegate had been a family of generic interface types. That would have allowed any class which only had to create a "delegate" for one method having a particular signature to use itself as a delegate to call that method. This would be especially useful for things like closures. Otherwise, Delegate must be a class rather than a struct, to allow for atomic assignments, but when the majority of delegates have a single target, I see no reason for delegates to have an extra field for an invocation list. – supercat Dec 12 '11 at 17:36
  • Perhaps I'm biased, though, since I really dislike the use of MulticastDelegates for the Observer pattern. A lot of event-subscription and unsubscription code is not thread-safe, which means that subscriptions to events published by outside code cannot safely be unsubscribed in asynchronous contexts. – supercat Dec 12 '11 at 17:42
3

One problem here is atomicity of assignment. If you think of a struct that contains a target and a pointer to a method. This struct has the size of two pointers. The CLR only guarantees atomic writes for pointer sized variables, so this would require some special mechanism to keep target and method in sync. But if an attacker manages to invoke a function on an object whose type doesn't match he might be able to circumvent the sandbox.

CodesInChaos
  • 106,488
  • 23
  • 218
  • 262
  • Wow, very good point! I hadn't thought about that aspect of the issue. – Dan Tao Oct 28 '11 at 18:44
  • That is the *only* reason Delegate needs to be a value type, but it's sufficient. There's no reason a type with just a function and a target wouldn't be sufficient to support multicast delegates, but the non-atomic updates would be a killer. – supercat Dec 12 '11 at 17:31
  • @supercat "That is the only reason Delegate needs to be a value type" Did you mean "needs to be a reference type"? – CodesInChaos Dec 12 '11 at 18:58
  • Indeed. I may have been debating between "...needs to be a reference type", and "...cannot be a value type", and munged the two notions together, badly. Thanks. The main point is that support for multicast delegates wouldn't require that Delegate itself have any extra fields. One could join multiple delegates by defining a MultiDelegateInvoker class which held the methods and targets of multiple delegates, and then create a new delegate whose target is the MultiDelegateInvoker object and whose method runs all the delegates in the MultiDelegateInvoker. – supercat Dec 12 '11 at 19:09
  • That probable wouldn't be the most efficient way of handling event subscriptions, but since multicast delegates aren't really the right approach anyhow, I don't see that as a loss. Other methods for handling multicast delegates would be more efficient anyway. – supercat Dec 12 '11 at 19:15
0

is System.Delegate, with its invocation list and all, basically a fundamental CLR type; or is it a wrapper around a simpler "function reference" type that simply isn't exposed by any CLR languages?

I think neither. First, although System.Delegate has some methods implemented by the CLR, instances of it are not handled specially, only in C# and other languages (it's just a class with Invoke method and some others). Second, there is a really special CLR type called fnptr representing a function pointer (see this question). However, this type is accessible, although not in C#, but in CIL (method void *()) or in C++/CLI (void (*foo)()).

Community
  • 1
  • 1
IS4
  • 11,945
  • 2
  • 47
  • 86