Dynamic in C# II: Basics

Last time, we began to dive into dynamic binding in C# and what happens through the pipeline. This time, we'll take a simple scenario and pick apart the details of what happens under the covers, both during compile time and runtime.

We can break down what the compiler does into three parts: type and member declarations with dynamics (ie methods that return dynamic), binding and lookup, and emitting. We'll deal now with the binding aspects of dynamic.

Dynamic binding

Dynamic binding itself can be broken into two scenarios. Lets consider the following example.

 static void Main(string[] args)
    dynamic d = 10;
    C c = new C();

    // (1) Dynamic receivers.
    d.Foo(); // Call.
    d.PropOrField = 10; // Property.
    d[10] = 10; // Indexer.

    // (2) Statically typed receivers (or static methods)
    //     with dynamic arguments.
    c.Foo(d); // Instance method call.
    C.StaticMethod(d); // Static method call.
    c.PropOrField = d; // Property.
    c[d] = 10; // Indexer.
    d++; // Think of this as op_increment(d).
    var x = d + 10; // Think of this as op_add(d, 10).
    int x = d; // Think of this as op_implicit(d).
    int y = (int)d; // Think of this as op_explicit(d).

Consider the first set of examples under (1). Each of these dynamic invocations happen off of the dynamically typed expression. It is clear where the dynamicity (yes, I like that word, even though it isn't one...) comes from, and where it goes.

The second set of examples under (2) are a little more complex. The use of dynamic is indirect in each of these. Because the argument to each operation is dynamic, they flow into the containing operation and make them dynamic as well. As such, the compiler does sort of a mix of dynamic binding and static binding - it will use the static type of the receiver to determine the set of members to overload on, but will use the runtime types of the arguments to perform overload resolution.

The first set of examples are much more straight forward to understand, so we'll use this set as our foundation for exploring the feature.

Dynamic receivers

When the compiler encounters an expression typed dynamic, it knows to treat the subsequent operation as a dynamic operation. Whether its an index get, index set, method call etc, the result type of the operation will be determined at runtime, and so at compile time, the result of the operation must also be dynamic.

The compiler transforms all dynamic operations into what we'll call a dynamic call site. This consists of creating a compiler generated static field on a generated static class that stores the DLR site instance for the invocation, and initializing it as necessary.

The DLR call site is a generic object that is generic on the delegate type of the call. More on how this delegate gets generated later. The type names may not be final yet, but currently the creation of the DLR call site takes a CallSiteBinder which is an object that knows how to perform the specific binding that is required for the call site. The DLR provides a set of standard actions that can be used to take advantage of the DLR's support for interop with dynamic objects (more on that in a later post).

The call site contains a field of type T that is an instance of the delegate type that the site is instantiated with. This delegate is used to contain the DLR caching mechanism which you can learn about on Jim Hugunin's blog. It stores the results of each bind and is used to invoke the resulting operation.

Once the call site has been created, the compiler then emits the code to invoke the delegate, passing it the arguments that the user passed to the call site.

What happens at runtime?

Once the compiler has created the DLR call site, it then invokes the delegate, which causes the DLR to do its magic with interop types, and its magic with caching. Assuming that we don't have a true IDynamicObject and we don't have a cache hit, the CallSiteBinder that we seeded the DLR site with will be invoked. C# has its own derived CallSiteBinders that will know how to perform the correct binding, and will return an expression tree which will be merged into the DLR call site's target delegate for caching.

The current caching mechanism simply checks exact type matches on the arguments. For example, suppose our call looks like the following:

 arg0.M(arg1, arg2, ...);

And suppose our call has arg0.Type == C, and all the arguments passed to the call are of type int. The cache check would look like the following:

 if (arg0.GetType() == typeof(C) &&
    arg1.GetType() == typeof(int) &&
    arg2.GetType() == typeof(int) && 
    Merge CallSiteBinder's bind result here.
... // More cache checks
    Call the CallSiteBinder to bind, and update cache.

C# CallSiteBinder creation

The last thing we need to paint a full picture of dynamic binding is to understand what the C# CallSiteBinder implementation does.

In our example, we have 3 different types of dynamic operations. We have a call, a property access, and an indexer. Each of these operations have their own unique pieces to them, but still share much of the common functionality. As such, they all are initialized with a common C# runtime binder, and are used by the runtime binder as data objects that describe the action that needs to be bound. We'll call these objects the C# payloads.

A good way to think of the C# runtime binder is a mini compiler - it has many of the concepts you'd expect in a traditional compiler, such as a symbol table, and a type system, and much of the functionality as well, such as overload resolution and type substitution.

Lets use the simple example of d.Foo(1) for our consideration.

Once the runtime binder gets invoked, it is given the payload for the current call site, and the runtime arguments the site is being bound against. It takes the types of the all the arguments (including the receiver) and populates its symbol table with those types. It then unpacks the payload to find out the name of the operation it's trying to perform on the receiver (in this case, "Foo"), and uses reflection to load all members named "Foo" off of the runtime type of d, putting those members into its symbol table.

From there, we have enough information in the binder's internal system to do the binding that the action describes. At this point, we fork off and bind based on the payload's description.

One of the design choices we made was that the runtime binder should have the exact same semantics that the static compiler has. This includes reporting the same set of errors that the compiler would produce, and perform the same set of conversions (user-defined or otherwise).

As such, each payload is bound exactly as the static compiler would have. The result of the bind is an expression tree that represents the action to take if the binding was successful. Otherwise, a runtime binder exception is thrown. The resulting expression tree is then taken and merged into the call site's delegate to become part of the DLR cache mechanism, and is then invoked so that the result of the user's dynamic bind gets executed.

A slight limitation

As I mentioned, we tried to keep the philosophy of matching exactly what the static compiler would do. However, there are several scenarios that will not work in Visual Studio 2010 that we will hopefully get to in a future release.

Several to note are lambdas, extension methods, and method groups.

Because we currently do not have a way of representing the source of a lambda at runtime without a binding, dynamic invocations that contain lambdas produce a compile time error.

Also, because we don't currently have a way of passing in using clauses and scopes during runtime, extension method lookup will also not be available for this next release.

There is currently no way to represent a method group at runtime (ie there is no MethodGroup type), and so without introducing that concept into .NET, there is no good way for us to allow method groups to be represented dynamically. This means that you cannot do the following:

 delegate void D();
public class C
    static void Main(string[] args)
        dynamic d = 10;
        D del = d.Foo; // This would bind to a method group at runtime. 

Because we cannot represent method groups at runtime, a runtime exception will be thrown if the runtime binder binds d.Foo to a method group.

Hopefully you have a better understanding about the details of what happens when C# performs a dynamic bind. Next time we'll take a look at the second set of scenarios we discussed today. We'll also introduce the Phantom Method, and describe what it does and see how it affects overload resolution.

Until then, happy coding!

kick it on DotNetKicks.com