So, we’ve looked at how the C# compiler produces a CallSite
object for every dynamic call that is made, and provides it with everything it needs to resolve the call at runtime. How is this information used when the callsite is invoked?
To create a CallSite
object, you need to pass in an instance of CallSiteBinder
, which performs the member lookup and binding of a dynamic call for the callsite. Each language has its own binder, and C#’s binder lives in the Microsoft.CSharp.dll
assembly. If you have a look at this assembly in a disassembler, you’ll see that within this assembly, in the Microsoft.CSharp.RuntimeBinder.Semantics
namespace, is a copy of the type lookup and member resolution code from the C# compiler! However, rather than being native calls to C++ methods and classes, the logic has been modified to use normal .NET reflection types and methods, just like any other reflection-based code – System.Type
, MethodInfo
, FieldInfo
, Type.GetInterfaces()
, etc.
When a callsite is invoked, the CallSiteBinder
associated with that callsite takes the static information it was initialized with, combines it with information on the specific arguments the callsite was invoked with, and passes that information to a version of the C# compiler’s type lookup logic in the Microsoft.CSharp.dll
assembly. This logic then uses standard .NET reflection calls to figure out what the call should resolve to, and passes that information back to the binder.
But what about speed?
However, there is a big problem with this: speed. Reflection is slooooow. If this lookup had to be performed every time the callsite is invoked, then dynamic calls would simply be too slow to be usable. So, the callsite needs to cache the result obtained from the binder to be re-used the next time the callsite is invoked. But this leads onto another problem. Take the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class ClassA { public static void TestDynamic() { CallDynamic(new ClassA(), 10); CallDynamic(new ClassA(), "foo"); } public static void CallDynamic(dynamic d, object o) { d.Method(o); } public void Method(int i) {} public void Method(string s) {} } |
The first time the callsite in CallDynamic
is invoked, the C# binder will resolve the call to the Method(int i)
method. If this result is then cached and re-used for the second call, the Method(int i)
method will be called with a string
argument, and the dynamic call will fail with an exception. The C# binder’s member resolution algorithm needs to be run again on the second call, to find a new method to call. This would then correctly resolve to the Method(string s)
method.
So, then, how can the callsite tell when a cached result can be re-used, and when it needs to call the binder to find a new method to call? Only the binder knows that, as it depends entirely on the binder’s member resolution rules and the methods available on the invoked type.
Therefore, when returning the result of a lookup, the binder also needs to return the conditions under which that result can be re-used. And before calling a cached result, the callsite needs to check the conditions apply before either calling the resolved method, or going back to the binder to resolve a new method.
Calling and caching
This leads onto the questions over how this is actually represented, and how the checks and caching can be done without sacrificing performance. Ideally, it would be just as fast as calling a method non-dynamically. Well, the fastest way to generate and execute arbitary code at runtime is to produce and compile a method in raw IL using MethodBuilder
and ILGenerator
. Unfortunately, these aren’t very easy to use, especially when you need to link several methods together, which (as we’ll see) is crucial to the behaviour of the callsite.
However, .NET 4 provides a far easier solution – expression trees blocks. These provide a far easier way to generate and link together code at runtime, and it is these that provide the implementation of the checks and method invocations used by every callsite. My next post will focus on how these expression tree blocks are used to implement the caching and invocation within a dynamic callsite.
Load comments