Curiosity is bliss    Archive    Feed    About    Search

Julien Couvreur's programming blog and more

The dark side of C# Delegates

 

This article assumes basic programming experience with delegates.

Delegates are a .NET framework feature that allows for type-safe function pointers. Actually, they are a bit more than function pointers, because they are object oriented, as their second name -bound method references- expresses. It means that the method pointer stored in the delegate may actually be bound to an instance of a specific object: when the delegate is invoked, the method is called on that target object. In the code of that method, you can notice that the this variable refers to that same target object.
But as we'll also see, delegates aren't always bound to an object. They support storing a static method pointer, which isn't bound to any specific instance of the class, like classic C-style function pointers.

In this article, we'll dig into delegates, their specificities, some of their inner workings and the role of the compiler in their implementation.
Note: We won't consider the singlecast vs. multicast aspect of delegates.

Update: a follow-up on this article can be found on this C# Delegates strike back entry.
Update: I wrote another follow-up on this article, about C# events vs. delegates

Syntax reminder
Here is a little sample of code using delegates:

// Declare
public delegate void StringLogging(string msg);

class Class1
{
  StringLogging stringHandler;

  static void Main(string[] args)
  {
   Class1 logger = new Class1();

   // Instanciate with static binding
   logger.stringHandler = new StringLogging(Class1.StringOutput);
   // Instanciate with instance binding
   logger.stringHandler = new StringLogging(logger.StringDrop);

   // Invoke
   logger.stringHandler("hello");
  }

  // Outputs strings to the console
  public static void StringOutput(string msg)
  {
   Console.WriteLine(msg);
  }

  // Drops strings into a blackhole
  public void StringDrop(string msg)
  {
   return;
  }
}

Delegates vs. Java Listeners
Delegates are a pretty interesting construct, although somewhat "magical". They allow easy binding of event handlers and other callback mechanisms, without the use of a separate class implementing a specific listener interface (which is the Java approach for callbacks).
This page details the comparison between delegates and the classic Command design pattern.
Basically, instead passing a callback function via an object of known interface that both communicating parties agree on, delegates only need the two parties to use the same function signature. This function signature contract is set during the declaration of a delegate.

For example, in the public delegate void MouseClickDelegate(int buttonClicked) delegate declaration, the interface for the callback function is void foo(int).

This delegate declaration is actually transformed by the compiler into a class MouseClickDelegate that can only be created by binding it to a void foo(int) method and only be invoked following that same interface void Invoke(int). Both of these constraints are verified by the compiler at compile time.
You can run ildasm on an assembly containing a delegate definition, and check out the generated delegate class and its interface (notice the Invoke method). You can also see that this class inherits from System.MulticastDelegate and therefore indirectly from System.Delegate (we'll look at this some more further down).

So, delegates are actually implemented using objects, albeit special ones. This is nicely introduced on this "Understanding the Nuances of Delegates in C#" page at O'Reilly Network. In particular, delegates are declared in a way similar to other objects (inside a namespace or inside another object) and can be instantiated and passed around like other typed variables.
Unfortunately, reading this article only left me with more curiousity about delegates and their implementations. That's because the article doesn't explain how delegate classes are generated by the compiler (see example above), but also because it doesn't say how those method pointers are bound to the object holding the method.


Object bound
Trying to figure out how delegates where not just function pointers, but actual method calls on an object, I dug up the System.Delegate class reference at MSDN.
The System.Delegate class has two interesting properties: Target (of type object) and Method (of type MethodInfo). Target references the object that the method is actually bound to. So, each delegate object is really a wrapper around a method and an object to be operated on when the method is called.

In the previous MouseClickDelegate delegate example, the dis-assembling of the assembly reveals the generated MouseClickDelegate class, with the detail of its attributes and methods. One of these methods is the following constructor (which is the same for all delegate objects): .ctor : void(object, native int).
This means that when you instanciate this class with new MouseClickDelegate(this.MyMouseClickListeningMethod), the compiler will actually replace this instanciation with a new MouseClickDelegate(this, ...). This is how the the delegate gets bound to the object.

When the delegate is instanciated with a static method, the object parameter in the constructor is null and the created delegate object has a null Target.
The assembly for such a static method delegates shows:

IL_000d: ldnull
IL_000e: ldftn void MyDelegateNamespace.Class1::StaticMethod(int32)
IL_0014: newobj instance void MyDelegateNamespace.MouseClickDelegate::.ctor(object,
native int)

So we can see that in such an instantiation, there is no instance object (it's null), as the method is static.


Reflection!?
We have seen that in the System.Delegate class, the Target references the object to which the method pointer is bound, but about the Method?
The Method property is a MethodInfo (part of the System.Reflection namespace). Also MSDN describes this property with: "Gets the method represented by the delegate". Does that mean every delegate invocation goes through Reflection?!
Although I don't have much knowledge on the Reflection component of the framework, I would expect such an implementation to have performance problems.

Looking at the dis-assembled MSIL code, we see that the invocation of the delegate looks like:

IL_0033: callvirt instance string MyDelegateNamespace.MouseClickDelegate::Invoke(int32)

The compiler recognized the "function call" on the delegate and replaced it with a call to the delegates Invoke compiler-generated method. The compiler also blocks any explicit call Invoke from your code, by issuing an error: "Invoke cannot be called directly on a delegate".
Unfortunately, this doesn't reveal whether the Invoke method relies on reflection to implement the invocation or if it relies on a native CLR method call mechanism.

The MSIL code for the delegate's Invoke method doesn't bring any information either:

.method public hidebysig virtual instance void
Invoke(int32 i) runtime managed
{
} // end of method MouseClickDelegate::Invoke

The "runtime managed" seems to indicate that the runtime is responsible for the implementation of this method. I haven't found how this works yet.

The only clues that I have gathered on the invocation mechanism are the signature of the delegate constructor (what is the second parameter in .ctor void(object, native int) ?) and a rather detailled explanation of the role of the compiler in the delegates' implementation in this delegates article in MSDNmag.
It appears that the second argument of the delegate constructor is a reference to the method, via "a special Int32 value (obtained from a MethodDef or MethodRef metadata token) that identifies the method is passed for the methodPtr parameter".
Unluckily, MethodDef and MethodRef don't seem to be well publicly documented as they don't bring much information on MSDN (they are mentioned hereon MSDN) or Google...

Update: I know understand what these token mean. For more info read my series on Runtime IL modification.

Summary
I am going to investigate the mono:: compiler source code to try and find the missing details about the implementation of the Invoke method and the meaning of the native int that references the method. But we have seen that the compiler plays a major role in making the delegates work.
Encountering a delegate declaration, the compiler actually generates a class with an Invoke method that matches the delegate declaration's signature.
Then, when a delegate instanciation is compiled, it gets verified (type-wise) and replaced by a call to the delegate constructor passing in an object and a method reference (via an int).
And last, the compiler recognizes delegates invocations and compiles them as call on the Invoke method.


Cheers,
Dumky


References
Microsoft on delegates and comparison with Inner classes, is a response to Sun's criticism of delegates.

Mono:: implementation of the Delegate class

Rotor implementation for the Delegate class

Rotor source code online

Compiler-supplied delegate methods at MSDN: Invoke, BeginInvoke and EndInvoke.

Method signature matching isn't complete type-safety

Interface-style vs. delegate-style performance

______________________________________

O'Reilly Network just added an article related to delegates, and how they compare to similar Java patterns.

A Java Programmer Looks at C# Delegates: http://www.onjava.com/pub/a/onjava/2003/05/21/delegates.html

Posted by: Dumky at May 23, 2003 11:18 AM

Hi,

Your article is really great.
There is need of research.

Posted by: Rashid Mahmood at May 28, 2003 11:39 PM

Hi,

I would have a question concerning the method pointer that is used by the Invoke method of a delegate.

If I understood well, a method pointer is the address of the entry point of the native code of the method, right? When I invoke a delegate, i.e. I call Invoke, then I will start executing the piece of native code that starts at the address that represents the method pointer.

My question is: if I have this address (method pointer), is there a way to know (determine) which method is? Am I able to determine the name of this method, the class that defines it etc (i.e. the complete method reference)?

Many thanks!
George

Posted by: George at October 6, 2004 02:51 AM

is there any property to inform to the user in online ,that the product he is going to order is finished ,

if there plz mail me

Posted by: senthil at April 1, 2005 03:53 AM

Very nice. I knew, when MSDN said that delegates are "not advised" and the similiarity of the reflection classes, that they are using reflection. It's cheating! anyone can do the same in java. Thanx 4 the article.

Posted by: Ajith at February 8, 2006 11:23 AM

Ajith, I don't actually think that delegates use reflection, although both technique do rely on MethodInfo. My understanding is that MethodInfo is the representation of a method pointer in the runtime.
Delegates do have lesser performance than direct function calls, but from the benchmark I've seen they where not that bad.

Posted by: Julien Couvreur at February 8, 2006 03:09 PM

Here is an even darker side to delegates. They are sensitive to the block in which they are declared. Changing the block can change delegate implementation! Here is a stupefying sample, if you change the struct to a class the behaviour reverses (because of stack vs heap):

class ScopeTest
{
public delegate void aDelegate();
public struct testStruct //DIFFERENT RESULT with: public class testStruct
{
public bool test;
public testStruct(string nada)
{
test = true;
MakeTestFalse();
Console.WriteLine(test);
test = true;
aDelegate _MakeTestFalse = new aDelegate(MakeTestFalse);
MakeTestFalseWithDelegate(_MakeTestFalse);
System.Console.ReadLine();
}
public void MakeTestFalse()
{
Console.WriteLine("Making test false");
test = false;
}
public void MakeTestFalseWithDelegate(aDelegate adelegate)
{
adelegate();
Console.WriteLine("delegate result: " + test);
}
}

public static void Main(string[] args)
{
testStruct t = new testStruct("nada");
}
}

Posted by: Brian at February 14, 2006 05:18 AM

It's "Instantiate" not "Instanciate"!

Posted by: The Spelling Police at June 23, 2006 02:44 AM
comments powered by Disqus