C H A P T E R  4

images

Late Binding and Interoperability

DLR binders have two key responsibilities. One is the caching mechanism we looked at in the previous chapter. The other is language interoperability, and that's the main topic of this chapter. Binders alone do not language interoperability make. The key elements in the DLR that make language interoperability possible are: a common type system that consists of twelve operations, binders, dynamic objects, and an interoperability protocol between binders and dynamic objects. In the DLR, binders and dynamic objects work together by adhering to a mutual protocol to ensure that objects from different languages interoperate seamlessly. This chapter will cover all of the key elements in the DLR that enable language interoperability. You'll see examples that fetch dynamic objects from Ruby code and pass them to Python code. However, I won't yet show you how to implement dynamic objects from the ground up; that will be the topic of the next chapter. For this chapter, it is enough to just use dynamic objects that come from Python or Ruby code.

Binders, dynamic objects, and the interoperability protocol they participate in are crucial to a solid understanding of how the DLR works. Let's begin the journey by first looking at what language interoperability means.

Language Interoperability

There are different levels of interoperability between languages. You can view web services as one way to enable language interoperability. A web service can be written in one language while a client of the web service can be written in a different language. The two languages interoperate by sending well-defined XML payloads to each other.

For our purposes, language interoperability means the ability to take something like a class or a function written in one language and use it in another language. For example, we may take a C# class, create an instance of it, and pass the instance to IronPython. Or we could take the same C# class, pass it directly to some IronPython code, and let the IronPython code create an instance of that class. The IronPython code can call methods on the C# object, or access its member properties. Not only can we pass C# classes or objects to IronPython, we can do the same in the other direction. We can pass IronPython classes, functions, or objects to C#. Furthermore, not only do we have this interoperability between a static language like C# and a dynamic language like IronPython, we also have it between two dynamic languages. For example, we can pass IronRuby classes or objects to IronPython and vice versa.

As an example, let's define a class in IronRuby and create an instance of it in C#. Listing 4-1 shows the IronRuby class. You can find all of this chapter's code examples in the Chapter4 solution of this book's download. If you open the Chapter4 solution in Visual Studio C# 2010 Express, you'll see that it has a project called InteropBinderExamples. Because we use IronPython and IronRuby in these code examples, the InteropBinderExamples project has references to the IronPython and IronRuby assemblies, such as IronPython.dll, IronPython.Modules.dll, IronRuby.dll, and IronRuby.Libraries.dll, and it also has references to DLR assemblies, such as Microsoft.Dynamic.dll and MicrosoftScripting.dll.

Listing 4-1. The RubyProduct Class in RubyProduct.rb

class RubyProduct
  attr_accessor :name
  attr_accessor :price

  def initialize(name, price)
     @name = name
     @price = price
  end
end

The code in Listing 4-1 defines a Ruby class called RubyProduct that has two member variables—name and price. The method initialize is a constructor method that gets called when new instances of the RubyProduct class are created. In this example, the constructor method takes a name and a price as input and sets them to the member variables. With RubyProduct in place, the next thing we want to do is to create an instance of it in C#. Listing 4-2 shows the code for doing that.

Listing 4-2. The Method for Creating Instances of the RubyProduct Class

 1)  class RubyExampleCode
 2)  {
 3)      private static dynamic productClass;
 4)      private static ScriptEngine rbEngine;
 5)
 6)      static RubyExampleCode()
 7)      {
 8)          rbEngine = IronRuby.Ruby.CreateEngine();
 9)          ScriptScope scope = rbEngine.ExecuteFile("RubyProduct.rb");
10)          productClass = rbEngine.Runtime.Globals.GetVariable("RubyProduct");
11)      }
12)
13)    public static dynamic CreateRubyProduct(String name, int price)
14)      {
15)          return rbEngine.Operations.CreateInstance(productClass, name, price);
16)      }
17)  }

The method CreateRubyProduct in Listing 4-2 creates an instance of the RubyProduct class. It does that by using the DLR's Hosting API. I won't get into the details of the DLR Hosting API for now as it's not the focus of the current discussion. I'll explain what the code does at a high level and will defer the detailed discussion of DLR's Hosting API to Chapter 6.

The static constructor of the RubyExampleCode class first creates a ScriptEngine instance for running Ruby code (line 8). The Ruby code in this example is the code in Listing 4-1 and it's in the RubyProduct.rb file. Once the Ruby code is run, an object representing the RubyProduct class is available (a class is an object in Ruby). The code in Listing 4-2 fetches that class object and assigns it to the variable productClass (line 10). The code in the CreateRubyProduct method passes productClass to some CreateInstance method to create an instance of RubyProduct. Again, don't worry too much yet if you don't feel you have a good understanding of the code. For now, it's enough to know that the CreateRubyProduct method creates an instance of RubyProduct every time it's called.

At this point, we have a Ruby class and a method that creates instances of the Ruby class in C#. Let's put them together in Listing 4-3 and see how it looks.

Listing 4-3. Client Code That Uses RubyExampleCode to Create Ruby Objects

static void Main(string[] args)
{
    RunRubyClassInstantiationExample();
    Console.ReadLine();
}

private static void RunRubyClassInstantiationExample()
{
    dynamic stretchString = RubyExampleCode.CreateRubyProduct("Stretch String", 7);
    Console.WriteLine("Product {0} is {1} dollars.",
              stretchString.name, stretchString.price);
}

The Main method in Listing 4-3 calls the RunRubyClassInstantiationExample method to run the example. This method creates an instance of the RubyProduct class by calling the static CreateRubyProduct method of the RubyExampleCode class. Once an instance of the RubyProduct class is created, the code in Listing 4-3 prints out the name and price properties of the RubyProduct instance to the console. If you run the code, you'll see output that looks like this:

Product Stretch String is 7 dollars.

The example so far shows some interoperability between C# and IronRuby. Let's crank it up a notch by throwing in a C# class and an IronPython function. What the next example will show you is the passing of both a Ruby object and a C# object to an IronPython function. The IronPython function will do some calculation with the two objects and return the result. Listing 4-4 shows the C# class, which is called CSharpProduct. Like the RubyProduct class, the CSharpProduct class has the two properties name and price. Listing 4-5 shows a Python function called addPrice that we'll use to do some calculation on both a Ruby object and a C# object. The function addPrice takes two parameters as input and adds up their price properties. The Python code is stored in the pythonExampleCode.py file.

Listing 4-4. CSharpProduct.cs.

public class CSharpProduct
{
    public String name { get; set; }
    public int price { get; set; }

    public CSharpProduct(String name, int price)
    {
        this.name = name;
        this.price = price;
    }
}

Listing 4-5. The addPrice Function in pythonExampleCode.py

def addPrice(x, y):
        return x.price + y.price

Just as the previous example used a ScriptEngine instance to run the Ruby code, this example needs a ScriptEngine to run the Python code. Listing 4-6 shows the class PythonExampleCode that acts as a helper class for running the Python code in Listing 4-5. Like before, the code in Listing 4-6 uses DLR's Hosting API, which is the topic of Chapter 6. For now, it is enough to know that the code in Listing 4-6 uses a ScriptEngine instance to run the Python code in the pythonExampleCode.py file. After running the Python code, the addPrice method we saw in Listing 4-5 is available as an object. So the code in Listing 4-6 fetches the object that represents the addPrice Python function and assigns it to the static AddPriceDelegate variable.

Listing 4-6. PythonExampleCode.cs

class PythonExampleCode
{
    public static Func<dynamic, dynamic, int> AddPriceDelegate;

    static PythonExampleCode()
    {
        ScriptEngine pyEngine = IronPython.Hosting.Python.CreateEngine();
        ScriptScope scope = pyEngine.ExecuteFile("pythonExampleCode.py");
        AddPriceDelegate = scope.GetVariable("addPrice");
    }
}

What we have so far is a C# class, a Ruby class, and a Python function. Let's put them together and have some fun. The code in Listing 4-7 passes a Ruby object (i.e., an object of a Ruby class) and a C# object (i.e., an object of a C# class) to the addPrice Python function. The example code in Listing 4-7 first creates a Ruby object and assigns it to the stretchString variable. The price of stretch string is 7 dollars. The code then creates a C# object and assigns it to the handClapper variable. The price of a hand clapper is 6 dollars. Finally, the example code passes the Ruby object and the C# object to the IronPython function. The function adds up the two prices and, sure enough, the total of stretch string and hand clapper is 13 dollars.

Listing 4-7. Passing a Ruby Object and a C# Object to a Python Function

static void Main(string[] args)
{
    RunAddPriceExample();
    Console.ReadLine();
}
private static void RunAddPriceExample()
{
    dynamic stretchString = RubyExampleCode.CreateRubyProduct("Stretch String", 7);
    dynamic handClapper = new CSharpProduct("Hand Clapper", 6);
    int total = PythonExampleCode.AddPriceDelegate(stretchString, handClapper);
    Console.WriteLine("Total is {0}.", total);
}

The example does not look very complex. After all, it's just a few lines of code. Underneath the code, however, there's actually a lot happening. The DLR does great work in hiding the complexity and making the integration between languages look almost effortless. The rest of this chapter is going to take you to backstage and show you the secrets behind the curtain.

Static and Dynamic Objects

One important thing to note in the preceding example is how static objects are treated as dynamic objects. By static objects I mean objects that don't have their own late-binding logic. In contrast, dynamic objects are objects that have their own late-binding logic. In the example, instances of the CSharpProduct class are static objects while instances of the RubyProduct class are dynamic objects. A static object's class can be defined in one language while the object itself is used in another language. And the same goes for dynamic objects. A dynamic object's class can be defined in one language while the dynamic object itself is used in another language. When talking about static or dynamic objects, it helps to distinguish the source language in which an object's class is defined and the target language in which the object is being used. In the example, the source language of the RubyProduct instances is always Ruby. When the RubyProduct instances are used in C#, then C# is the target language that hosts them. Similarly when they are passed to the addPrice method of IronPython, the target language is IronPython.

Even though instances of the CSharpProduct are static objects, the example code treats them as dynamic objects by using the C# dynamic keyword. Sometimes in this book, the term  “dynamic object” means a truly dynamic object like one in Python or Ruby. Sometimes, the term refers to a static object that's treated as a dynamic object. Usually the context makes it clear which case it is. When there's a chance of confusion, I'll clarify what I mean.

Because static objects don't have their own late-binding logic, when they are treated as dynamic objects, who is responsible for deciding their late-binding behavior? What actually happens is that, by default, the DLR wraps static objects with instances of the DynamicMetaObject class. This wrapping effectively turns static objects into dynamic objects. However, there is no late-binding behavior implemented in DynamicMetaObject. Instead, DynamicMetaObject delegates the job of late binding to the target language's binders. So long story short, by default, it is the target language's binders that handle the late-binding behavior of static objects.

DynamicMetaObject is the most important class in the whole discussion about dynamic objects in the next chapter. You will also run into this class a few times in this chapter, so it helps to explain at a high level what it is. In DLR, late-binding logic can be in one of two places. It can be in binders as we have seen so far. It can also be in dynamic objects. The late-binding logic of a dynamic object is not in the dynamic object itself. Rather, the logic is in the meta-object associated with the dynamic object. The meta-object has to be an instance of DynamicMetaObject or one of its derived classes. In other words, instances of DynamicMetaObject or its derivatives are objects that contain the late-binding logic of dynamic objects. The next chapter will show you how to implement custom late-binding logic in classes that derive from DynamicMetaObject. For now, it's enough to understand DynamicMetaObject as the base class of all classes that implement late-binding logic of dynamic objects.

Late-Binding Logic in Two Places

One question you might have is why late-binding logic resides in two places (binders and dynamic objects)? What's the difference between the late-binding logic in those places? How do you decide whether to put your custom late-binding logic in binders or in dynamic objects? The next two sections take a detailed look at these two places. After the discussion, you'll know the answers to these questions. First, let's look at the late-binding logic in binders.

Late Binding Logic in Binders

The late-binding logic in binders pertains to a target language. For example, C# implements its own set of binder classes. Those binder classes have late-binding logic that pertains to C#. Similarly, IronPython implements its own set of binder classes that have late-binding logic pertaining to IronPython. Why does each language need its own binders? Here's an example. In IronPython, you can access the __class__ field of any object to get the object's class. It doesn't matter what the object's source language is. It also doesn't matter whether the object is a static object or dynamic object. As long as we use the object in IronPython, we can expect a seamless interoperability that allows us to access the __class__ field of the object just as we can any native Python object. Clearly, if an object's source language is not Python, it won't have any clue about the Python-specific __class__ field. Even if the object happens to have a field with the name __class__, the semantics of that field might not be the same as Python's. So when we access the __class__ field of an object in some Python code, the late-binding logic pertaining to the IronPython language will kick in and perform the binding no matter whether the object is static or dynamic. If it's a static object, the IronPython language binders will be invoked anyway since the object itself does not know how to do its own late binding. If it's a dynamic object, the DLR has a mechanism that falls back to the IronPython language binders when the object does not know how to bind things like __class__. I will describe more about that fallback mechanism in a bit. First, let's look at an example that shows how the late binding logic in IronPython's binders allows us to access the __class__ field on a static .NET object.

The IronPython code snippet below adds a reference to the System.Windows.Forms.dll assembly and then creates an instance of the Form class in the System.Windows.Forms namespace. The Form class is a static .NET class and does not have a property called __class__ defined. However, the IronPython code snippet is able to access the __class__ property on the form object because, in this example, IronPython is the target language and IronPython's binders will perform the late binding when the example code tries to access the __class__ property on the form object.

import clr
clr.AddReference(“System.Windows.Forms”)
from System.Windows.Forms import Form

form = Form()
print form.__class__

Determining the target language of an object is not always as straightforward as this. Here's another example that's slightly more complicated. This example accesses the __class__ property on a dynamic Ruby object and a static C# object within an IronPython function. For this example, I added a new Python function in the pythonExampleCode.py file. The new function is called printClassName and it is shown in Listing 4-8. The function is very simple. It takes an input parameter and prints the parameter's __class__ attribute.

Listing 4-8. The printClassName Function in pythonExampleCode.py

def addPrice(x, y):
        return x.price + y.price

def printClassName(x):
        print x.__class__

In order to make it easy to call the printClassName function in C#, I added a new static variable called PrintClassNameDelegate in the PythonExampleCode class (which we saw earlier in this chapter). Listing 4-9 shows the code I added for this example in bold. The new static variable PrintClassNameDelegate in Listing 4-9 is a reference to the printClassName Python function shown in Listing 4-8.

Listing 4-9. PythonExampleCode.cs

class PythonExampleCode
{
    public static Func<dynamic, dynamic, int> AddPriceDelegate;
    public static Action<dynamic> PrintClassNameDelegate;

    static PythonExampleCode ()
    {
        ScriptEngine pyEngine = IronPython.Hosting.Python.CreateEngine();
        ScriptScope scope = pyEngine.ExecuteFile("pythonExampleCode.py");
        AddPriceDelegate = scope.GetVariable("addPrice");
        PrintClassNameDelegate = scope.GetVariable("printClassName");
    }
}

At this point, everything is in place and we are ready to see what the example intends to demonstrate. The code in Listing 4-10 creates a Ruby object and passes the object to the printClassName Python function. What's significant here is that the Ruby object does not have any clue about the __class__ attribute. The __class__ attribute is a Python-specific thing. When the C# code in Listing 4-10 calls the Python function and passes it the Ruby object, the target language of the Ruby object changes from C# to IronPython. The source language of the Ruby object is still Ruby, of course, and will never change. Inside the printClassName function, the target language of the Ruby object is IronPython, and thus IronPython's binders will call the shots. In this case, the Ruby object is a dynamic object. It does not know how to bind the __class__ attribute. So the binding process falls back to IronPython's binders. Because the binders have late-binding logic that pertains to the Python language, they know how to bind the __class__ attribute. So the binding is successful and the class name is printed on the screen. The key point to note about this example is that an object's target language can change from one to another.

The code in Listing 4-10 goes on to show that the Python function can work with a static object too. The example creates an instance of the CSharpProduct class and passes it to the printClassName Python function.  Because the C# object is a static object, it does not know how to do any late binding. Within the printClassName Python function, the IronPython language binders are called upon to do the late binding for the C# object. And again, the binding is successful because the binders have late binding logic that pertains to the Python language, and they know how to bind the __class__ attribute.

Listing 4-10. C# Client Code That Calls a Python Function

private static void RunPrintClassNameExample()
{
    dynamic stretchString = RubyExampleCode.CreateRubyProduct("Stretch String", 7);
    PythonExampleCode.PrintClassNameDelegate (stretchString);

    dynamic handClapper = new CSharpProduct("Hand Clapper", 6);
    PythonExampleCode.PrintClassNameDelegate (handClapper);
}

If you run the example code, you'll see the following output on the screen:

<type 'RubyObject'>
<type 'CSharpProduct'>

Late-Binding Logic in Dynamic Objects

The key takeaway of the previous section is that the late-binding logic in binders pertains to a target language. In contrast, the late-binding logic in a dynamic object pertains to that object itself. The idea is that by letting the late-binding logic in a dynamic object pertain to that object itself, no matter where the dynamic object is used, its late-binding behavior is the same and will support the semantic of the dynamic object's source language. For example, instances of RubyProduct are dynamic objects and they carry their own late-binding logic with them. So they have the same late-binding behavior you'd expect a Ruby object to have, no matter if they are used by C# code, IronPython code, or some other language's code. Chapter 5 discusses late-binding logic in dynamic objects in detail. You'll see in Chapter 5 how to implement classes that derive from the DynamicMetaObject class and also how to make use of those classes. Until then, I'll focus the discussion on binders and language interoperability.

Late-Bound Actions

So far, we've talked about late-binding logic in binders and dynamic objects, but we haven't said what exactly can be bound late. The DLR defines twelve actions that can be bound by both binders and dynamic objects at run time. Let's first look at some examples of those late-bound actions, then I'll present the twelve late-bound actions and show you the DLR's binder class hierarchy that corresponds to those twelve actions.  

Examples

The following line of code, extracted from Listing 4-3, shows two actions that can be bound late:

Console.WriteLine("Product {0} is {1} dollars.", stretchString.name, stretchString.price);

First, it shows that accessing a member property can be late bound. In this code, the two operations that access the name and price properties of the stretchString variable are late bound. The variable stretchString is a dynamic object created from a Ruby class. Because of that, the C# compiler does not have the static type information to bind the two operations that access stretchString's member properties at compile time.

Another not so obvious operation that is bound late in the code is member method invocation. Because stretchString is a dynamic object, the return values of stretchString.name and stretchString.price are also dynamic objects. When the code passes stretchString.name and stretchString.price as parameters to Console.WriteLine, the invocation of the Console.WriteLine method also becomes late bound. This is because at compile time, without knowing the actual types of all the input parameters, the C# compiler can't decide how to bind the method invocation.

If you compile the code in Listing 4-3 and disassemble the generated assembly, you can verify that indeed the stretchstring.name, stretchString.price, and Console.WriteLine in the code are late bound. Listing 4-11 shows at a conceptual level what you'll see if you disassemble the code in Listing 4-3. The code in Listing 4-11 should look familiar because it's essentially the same kind of code as in the last chapter. The difference is that in Chapter 3, the examples create their own call sites and binders and here the call sites and binders are created by the C# compiler.

Listing 4-11. A Conceptual Illustration of the Code Generated by the C# Compiler

if (Site4 == null)
{
    Site4 = CallSite<Action<CallSite, Type, string, object, object>>
                .Create(Binder.InvokeMember(..., "WriteLine", ...));
}

if (Site5 == null)
{
    Site5 = CallSite<Func<CallSite, object, object>>
        .Create(Binder.GetMember(..., "name", ...));
}

if (Site6 == null)
{
    Site6 = CallSite<Func<CallSite, object, object>>
               .Create(Binder.GetMember(..., "price", ...));
}

Site4.Target.Invoke(Site4, typeof(Console), "Product {0} is {1} dollars.",
    Site5.Target.Invoke(Site5, stretchString),
    Site6.Target.Invoke(Site6, stretchString));

Listing 4-11 includes three call sites and three binders, one binder and one call site for each of the three late-bound operations—stretchstring.name, stretchString.price, and Console.WriteLine. Before creating each call site, the code generated by the C# compiler checks whether the call site is null. If it's not null, the call site has been created already and the code will not create it again. That's why you see those if conditions in Listing 4-11. This is a good idea because if the method RunRubyClassInstantiationExample is called multiple times, the code won't create three new call sites every time the method is called. Not only that, the code in Listing 4-11 also won't create three new binders every time the RunRubyClassInstantiationExample is called. That's because the code calls the InvokeMember and GetMember methods of the Binder class to get the C# language's binders. These methods ensure that the same canonical binders are returned when the method RunRubyClassInstantiationExample is called multiple times. As you can see, the code generated by the C# compiler avoids unnecessary creation of call sites and binders. This saves memory, of course. But more importantly, because the code reuses the same call sites and binders, the caches in those call sites and binders will also be leveraged to help improve performance.

In Listing 4-11, the variable Site4 is the call site for the late binding of the Console.WriteLine method invocation. The binder passed to Site4 contains the method name “WriteLine” to be late bound. Similarly, the variable Site5 is the call site for the late binding of the stretchString.name property access. The binder passed to Site5 contains the property name “name” to be late bound. The variable Site6 is for the late binding of the stretchString.price property access.

With the three call sites and binders in place, the code in Listing 4-11 invokes the Target delegate on Site5 and Site6. The binding results of Site5.Target and Site6.Target represent the results of stretchString.name and stretchString.price. Those results are then passed to Site4.Target, which eventually prints the text on the screen.    

Our discussion in this section shows that member property get–access operations and member method invocations are two kinds of operations that can be late bound. It turns out that the DLR defines in total twelve kinds operations that can be late bound, and those operations make up what's called the Common Type System of the DLR. Let's see what that means.

Common Type System

Most languages have their own type systems, and those type systems need to be considered if the languages are going to interoperate. For example, say we have a Java string object in a JVM and we want to bring it over to a .NET CLR runtime. There is more than one way to achieve this. One common approach is marshalling and unmarshalling objects. The idea of this approach is to serialize the Java string object to 0s and 1s, send the bits over to the CLR runtime, and then create a CLR string object by deserializing the bits. However, this approach is often cumbersome and not very efficient, mainly because we need to deal with two separate runtimes and two separate type systems. With this approach, types in one system are unrelated to types in the other system. To bridge the systems and make them interoperate, we have to do the mapping between the types. Nonetheless, it is an approach to consider when you have no control over the two languages you are trying to bridge.

So how does the DLR achieve language interoperability with regard to type systems? The DLR facilitates language interoperability by defining a common type system for DLR-based languages. This is essentially the same tactic used by the CLR, which defines the Common Type System (CTS) for static .NET languages. If two languages share the same type system, then types defined in one language will be understood by the other language without any need for mapping or translation. For example, say we have an abstract C# class. When the C# class is compiled, the fact that it is abstract is mapped to the equivalent “abstract class” construct in the Common Type System. So if we have some VB.NET code that uses the compiled C# class, the VB.NET code will know that the class is an abstract class. VB.NET code does not understand C# code directly; it knows the C# class is abstract only because the C# class has been compiled to IL (.NET Intermediate Language) based on the CTS, which VB.NET understands.

Because a common type system has to meet the needs of multiple languages so that those languages can map to it, the common type system has to be a superset of the type systems of the languages it intends to support. The challenge here is to find a superset that's sufficient and also compact. For dynamic languages, the DLR identified twelve actions/operations that together are sufficient to meet the needs of most dynamic languages. Table 4-1 summaries those twelve actions.

Table 4-1. Twelve Operations/Actions That Can Be Bound Late

Operation/Action Description
GetMember This action, when triggered on a dynamic object, will have the effect of getting a property of the object. You saw an example of this earlier in the chapter with stretchString.name. stretchString is a dynamic object and stretchString.name triggers the GetMember action on stretchString in order to get the name property of stretchString.
SetMember This action sets the value of a dynamic object's property. In this table, dynamic objects mean both truly dynamic objects as well as static objects that are treated as dynamic objects. An example of the SetMember operation is stretchString.name = “stretch string,” as we saw earlier.
DeleteMember This action removes a property from a dynamic object. C# doesn't have syntax for expressing this action. In Python, an example of this action looks like this: del baz.Foo, which deletes the Foo property (attribute) from the baz object.
Invoke This late-bound action occurs when you invoke a callable entity such as a delegate. The printClassName Python function we saw in Listing 4-8 is an example of a callable object. If we have some object foo, then the code printClassName(foo) in Python will trigger a late bound Invoke action.)
InvokeMember This late-bound action happens when you invoke a method on a dynamic object. For example, if baz is a dynamic object, baz.Foo() will trigger a late-bound InvokeMember action.
BinaryOperation This late-bound action will take place when you apply a binary operator on a dynamic object. For example, if baz is a dynamic object, baz + 2 will trigger a late-bound BinaryOperation action.
UnaryOperation This late-bound action is triggered when you apply a unary operator on a dynamic object. For example, if baz is a dynamic object, ++ baz will trigger a late-bound UnaryOperation action.
GetIndex This action returns the element at the specified index of a collection. For example, if baz is a dynamic object, baz[3] will trigger a late-bound GetIndex action.
SetIndex This action sets the element at the specified index of a collection. For example, if baz is a dynamic object, baz[3] = “hello” will trigger a late-bound SetIndex action.
DeleteIndex This action removes the element at the specified index of a collection. C# does not have syntax for expressing this action. In Python, an example of this action looks like this: del baz[3]. The Python code means deleting the fourth element from the collection baz.
CreateInstance This late-bound action happens when you create a new instance of a class. For example, if Baz is a class, the code Baz() in Python will trigger a late-bound CreateInstance action. Currently, C# does not have syntax for triggering this late-bound action. The C# code new Baz() will create an instance of Baz in a static fashion instead of triggering a late-bound CreateInstance action.
Convert This late-bound action will take place when you try to type cast a dynamic object. For example, if baz is a dynamic object, (String) baz will trigger a late-bound Convert action. IronPython and IronRuby don't have syntax for triggering this action.

After seeing the twelve actions that make up the DLR's common type system, let's see why this system is the key to enabling interoperability between dynamic languages. Let's begin with the familiar static language code. In the C# code snippet below, the type information of the String class is accessible to the C# code snippet regardless of which language the String class was written in. The type information is expressed in terms of the .NET Common Type System.

String bob =  "Bob";
String lowercaseBob = bob.ToLower();

Now if we change that code to the following, what difference does it make in terms of type systems?

dynamic bob =  "Bob";
dynamic lowercaseBob = bob.ToLower();

The most noticeable difference is that the second code snippet no longer uses the type information of the String class. Instead, it uses the DLR's common type system. In the previous case, it doesn't matter which language the String class was written in. In the latter case, it doesn't matter which language the class of the variable bob is written in. The class of the variable bob can be written in C#, VB.NET, IronPython, or some other .NET language—it doesn't matter and the example code doesn't need access to that class's type information. Instead, the example code only requires that the class of the variable bob knows how to bind and handle the method invocation when it calls the ToLower method on bob. Binding and handling method invocation is one of the twelve actions defined by DLR's common type system.

As the two examples in this section show, when we use an object, we need to know something about it. In the static case, we need to know the object's static type information. In the dynamic case, we assume that the object supports some of the twelve actions that make up the DLR's common type system.

Class Hierarchy of Binders

To recap a little, so far in this chapter you've learned two key concepts: (a) late binding logic can be implemented in binders and dynamic objects, and (b) the DLR defines twelve operations that can be late bound. Given these concepts, we can deduce that there is late-binding logic for each of the twelve operations in binders as well as in dynamic objects. I will save the explanation of the link between dynamic objects and the twelve operations for the next chapter. For now, let's see how binders and the twelve operations are related.

For each of the twelve late-bound operations in Table 4-1, the DLR defines a binder class to contain the late-binding logic for the operation. Figure 4-1 shows the class hierarchy of DLR binder classes. We have seen the class hierarchy in Chapter 3, but  Chapter 3 covers only the CallSiteBinder class in the class hierarchy. This chapter will introduce you to the rest of the classes. As Figure 4-1 shows, there is a binder class for each of the twelve late-bound operations. For the SetMember late-bound operation, the corresponding binder class is SetMemberBinder. For the GetMember late-bound operation, the corresponding binder class is GetMemberBinder and so on.

images

Figure 4-1. Class hierarchy of DLR binder classes

Each time a late-bound operation is encountered in the target language, the target language's binder class corresponding to the late-bound operation will be used. For example, in the previous section when we looked at the disassembled C# code, C# is the target language. When the C# compiler sees the code stretchString.name, it emits code that creates an instance of GetMemberBinder by calling the Binder.GetMember method as shown in Listing 4-11. The binder instance returned by the Binder.GetMember method is actually an instance of the CSharpGetMemberBinder class, which derives from GetMemberBinder. Of course, all of these C#-specific binders are the internal implementation details of C# and are well-hidden from the outside.

As you can see, the typical practice with regard to binders is that if you are implementing a new language and you want the language to interoperate with other DLR-based languages, you likely need to implement binder classes for the late-bound operations your language intends to support. When you implement your language's binder classes, you will likely do so by subclassing the twelve binder classes shown at the bottom of Figure 4-1. The binder classes you implement will contain the late-binding logic that pertains to your language. As with C#, your language does not need to be a dynamic language. It can have a mix of dynamic and static language features. Your language doesn't necessarily need to support all twelve late-bound operations by having twelve binder classes. For example, C# doesn't support the “delete member” and “delete index” late-bound operations because the C# language syntax simply doesn't allow that. It's not possible to write C# code like this:

String bob = "Bob";
del bob.Length;

The C# compiler will show a compilation error that says del is not a valid keyword. There is no language keyword in C# for deleting a member of an object. Therefore, the C# language does not need a binder class for the DeleteMember late-bound operation. Python, on the other hand, supports deleting members of an object. The code in Listing 4-12 shows an example. The code defines a class called Customer. The class has two member properties—Name and Age. After the class is defined, the code creates an instance bob of the Customer class. Then it prints the Name property of bob in line 8. The result is the text “Bob” printed on the screen. The code in line 9 deletes the Name property of bob. After that, when it prints the Name property of bob again in line 10, the IronPython runtime will throw an error because the Name property of bob has been deleted.

Listing 4-12. A Python Example That Deletes a Member of an Object

 1)  class Customer(object):
 2)     def __init__(self, name, age):
 3)        self.Name = name
 4)        self.Age = age
 5)
 6)  bob = Customer("Bob", 30)
 7)
 8)  print bob.Name
 9)  del bob.Name
10)  print bob.Name  # This will throw an error.

Implement a Custom Binder Class

We've looked at the twelve late-bound actions that make up the DLR's common type system and the corresponding binder classes in DLR's binder class hierarchy. This section is going to show you how to implement a custom binder class for the GetMember late-bound action. We will use the binder class to get a member property of a Python object.

Our example will consist of a Python class, a binder class that inherits from GetMemberBinder, and some client C# code that puts everything together. The Python class is the Customer class you saw in Listing 4-12. So that part is taken care of. Here is the complete code for the Customer class and the bob object we will use in this  example. The code is in the pythonExampleCode.py file.

class Customer(object):
   def __init__(self, name, age):
      self.Name = name
      self.Age = age

bob = Customer("Bob", 30)

In order to expose the bob object to C# code, we need to add the following two bolded lines of code in the PythonExampleCode class.

class PythonExampleCode
{
    public static Func<dynamic, dynamic, int> AddPriceDelegate;
    public static Action<dynamic> PrintClassName;
    public static dynamic Bob;

    static PythonExampleCode()
    {
        ScriptEngine pyEngine = IronPython.Hosting.Python.CreateEngine();
        ScriptScope scope = pyEngine.ExecuteFile("pythonExampleCode.py");
        AddPriceDelegate = scope.GetVariable("addPrice");
        PrintClassName = scope.GetVariable("printClassName");
        Bob = scope.GetVariable("bob");
    }
}

The code for our custom binder class is shown in Listing 4-13. This code is the main focus of the example. The binder class in Listing 4-13 inherits from GetMemberBinder and is called SimpleGetMemberBinder. It contains the late-binding logic that always returns the constant number 3. The constructor of SimpleGetMemberBinder takes two parameters—name and ignoreCase. The parameter name is the name of the member property to late bind. In this example, the member property to late bind will be the Name property (Python calls it attribute, but when there is no risk of confusion, I'll just call it property.). So in this example, the name parameter will be the string “Name”. The ignoreCase parameter indicates whether to treat the name of the member property in a case-sensitive manner when doing late binding. As to the FallbackGetMember method in Listing 4-13, I will have more to explain about it later. For the time being, let's move on to the C# client code and see how things work together.

Listing 4-13. A Custom Binder Class for the GetMember Action

class SimpleGetMemberBinder : GetMemberBinder
{
    public SimpleGetMemberBinder(string name, bool ignoreCase)
        : base(name, ignoreCase)
    { }

    public override DynamicMetaObject FallbackGetMember(DynamicMetaObject target,
        DynamicMetaObject errorSuggestion)
    {
        Console.WriteLine("Doing late binding in SimpleGetMemberBinder.");

        //The binding has no restrictions. It returns a constant number.
        return errorSuggestion ?? new DynamicMetaObject(
            Expression.Convert(Expression.Constant(3), typeof(object)),
            BindingRestrictions.GetTypeRestriction(target.Expression,
                                                       target.LimitType)
        );
    }
}

Listing 4-14 shows the C# client code. As before, even though the code is in C#, it is not using C#'s binders. It uses the binder class in Listing 4-13 instead. You can think of this as simulating the late-binding behavior of some custom language of our own. The code in Listing 4-14 looks very similar to the code we saw in Chapter 3. It first creates an instance of SimpleGetMemberBinder and passes it the string “Name” and the Boolean value false. That tells the binder that the name of the member property to late bind is “Name” and the name of the member property should be bound in a case-sensitive manner. Next the code creates a call site. The type of the call site's Target delegate is Func<CallSite, object, object>. That means it's a delegate that takes an instance of CallSite and an instance of object as input parameters and returns an instance of object as result. The input object instance is the Python object bob. A reference to the Python object bob is available as a static member of the PythonExampleCode class. So the code in line 8 of Listing 4-14 just gets the Python object bob from the static member of the PythonExampleCode class and passes it to the Target delegate. The invocation of the Target delegate will trigger the late-binding process. After the late-binding process finishes, the result of the late binding is assigned to the variable name and printed to the screen.

Listing 4-14. C# Code That Uses SimpleGetMemberBinder for a Late-Bound GetMember Action

 1)  private static void RunGetMemberBinderAndDynamicObjectExample()
 2)  {
 3)      CallSiteBinder binder = new SimpleGetMemberBinder("Name", false);
 4)
 5)      CallSite<Func<CallSite, object, object>> site =
 6)        CallSite<Func<CallSite, object, object>>.Create(binder);
 7)
 8)      object name = site.Target(site, PythonExampleCode.Bob);
 9)      Console.WriteLine("Customer name is {0}.", name);
10)  }

If you run the code in listing 4-14, you should see the following output:

Doing late binding in SimpleGetMemberBinder.
Customer name is Bob.

At this point, we've seen an implementation of a rather trivial custom binder class that always returns the number 3 as the binding result. Although the example and its implementation look trivial, there are a lot of activities going on behind the scenes between the Python object bob and our custom binder. Those activities follow a well-defined interoperability protocol, which the next section explains.

Interoperability Protocol

This section will use the example in Listing 4-14 to explain the interoperability protocol that's used between binders and dynamic objects. The whole late-binding process in Listing 4-14 begins with the call to the Target delegate of the call site. As I explained in the previous chapter, the L0, L1, and L2 caches will be checked to see if there is already a rule that's suitable for the late-binding operation. In this case, since this is the first time the call site and binder are called upon to do the late binding, there's nothing in the three caches. The details of the cache searching process are described in the previous chapter, so I'll spare those details here.

If there's a cache hit, the whole late-binding process is finished. Upon a cache miss, the Bind method that SimpleGetMemberBinder indirectly inherits from CallSiteBinder is invoked. The method's signature is shown below. The elements in the args array and the elements in the parameters collection pair up. The nth element in the parameters collection carries some information about the nth element in the args array. The first pair is special. The first element of the args array and the first element of the parameters collection represent the target object of the late-binding operation. In our example, the target object is the Python object bob. In our example, there are no more elements in the args array and the parameters collection because it is a GetMember late-bound operation. A GetMember operation does not have additional input arguments to go along with the target object.

public  abstract Expression Bind(object[] args, ReadOnlyCollection<ParameterExpression>
parameters, LabelTarget returnLabel);

The abstract Bind method defined in CallSiteBinder is implemented in DynamicMetaObjectBinder. SimpleGetMemberBinder derives from DynamicMetaObjectBinder and therefore inherits that implementation. The implementation basically takes each pair of the args array and the parameters collection and turns it into an instance of DynamicMetaObject. If you recall from the earlier discussion in this chapter, this is a key step in enabling language interoperability. In this step, all the objects in the args array, no matter if they are static or dynamic, will be turned into instances of DynamicMetaObject. And the defining characteristic of DynamicMetaObject instances is that they contain the late-binding logic pertaining to the objects they are associated with.

For each pair of the args array and the parameters collection, the implementation calls the static method Create of DynamicMetaObject. The Create method checks if the object from the args array is a dynamic object. In the DLR, an object is dynamic if it implements the IDynamicMetaObjectProvider interface, which I will explain in detail in the next chapter. In our example, the only element in the args array is a dynamic Python object. So the Create method simply gets the meta-object associated with the dynamic Python object. If the object were a static object, the Create method would wrap it up into an instance of DynamicMetaObject. If you take a look at the source code of DynamicMetaObject, you'll see that DynamicMetaObject doesn't really implement any late-binding logic. Instead, DynamicMetaObject delegates the job of late binding to the target language's binders, as I've mentioned before.  

Once all the DynamicMetaObject instances are ready, the Bind method implementation in DynamicMetaObjectBinder calls the following overloaded abstract Bind method. The overloaded Bind method is defined in DynamicMetaObjectBinder and it takes the DynamicMetaObject instances as input and returns as result an instance of DynamicMetaObject.

public abstract DynamicMetaObject Bind(DynamicMetaObject target, DynamicMetaObject[] args);

As you can see, all the inputs and output of this overloaded method are of type DynamicMetaObject. From this point on in the late-binding process, the level of abstraction is elevated from objects and expressions in the original Bind method (i.e., the Bind method defined in CallSiteBinder) to instances of DynamicMetaObject. The objects and expressions in the original Bind method know nothing about language interoperability, whereas the instances of DynamicMetaObject are part of the interoperability protocol. Let's continue tracing the late-binding process and see how that protocol works.

The Bind method that takes DynamicMetaObject instances is an abstract method. Each of the twelve binder classes that inherit from DynamicMetaObjectBinder has its own implementation of that Bind method. The code in the twelve binder classes shares the same pattern. Here I'll go through the implementation of the Bind method in the GetMemberBinder class and show you that pattern. As the code excerpt from GetMemberBinder in Listing 4-15 shows, the implementation of the Bind method simply calls the BindGetMember method on the target meta-object and passes the binder itself to the call. The args input parameter of the Bind method is of no use because, as I mentioned earlier, the GetMember late-bound operation does not have any extra arguments. Even though the implementation of the Bind method is essentially just one line of code, it's one of the most important parts of the interoperability protocol. First, it shows that the binder is passing the control of late binding to the target object. If the target meta-object (i.e., the target input parameter of the Bind method) is the meta-object of a true dynamic object, such as the Python object in our example, then the line of code means the binding logic pertaining to the dynamic object will take over. This ensures that no matter which target language is using the dynamic object, the dynamic object's late-binding logic will be honored.

Listing 4-15. Excerpt from the DLR's GetMemberBinder Class

public sealed override DynamicMetaObject Bind(DynamicMetaObject target,
        params DynamicMetaObject[] args)
{
    //code omitted
    return target.BindGetMember(this);
}

public abstract DynamicMetaObject FallbackGetMember(DynamicMetaObject target,
DynamicMetaObject errorSuggestion);

Passing the binder object itself to the call of the BindGetMember is also a crucial part of the interoperability protocol. It's crucial because it allows the target meta-object to call back the binder. In the code excerpt, there's a very important abstract method called FallbackGetMember. This is the callback method the target meta-object will invoke if it wants to pass the late-binding control back to the binder. And that's the method the SimpleGetMemberBinder in our example overrides and implements. The FallbackGetMember method is the method that contains the binder's late-binding logic. In summary, this part of the interoperability protocol is that the binder calls the target meta-object's BindGetMember method, and the target meta-object at its discretion may or may not choose to call back the binder. So far so good! Now if the target meta-object does call back the binder, there are basically two cases in which that callback happens. The first is when the target meta-object utterly, completely has no clue how to perform the late binding. In this case, the target meta-object can choose to pass null as the value for the errorSuggestion parameter when it calls the FallbackGetMember method on the binder. The target meta-object can also pass a DynamicMetaObject instance that contains an Expression instance that represents an exception. In the other case, the target meta-object is able to perform a portion but not all of the late binding and it relies on the binder to finish the rest. In such case, the target meta-object will pass its partial binding result as the value for the errorSuggestion parameter when it calls back the FallbackGetMember method on the binder.  

So far, this is just half of the story where the target meta-object is a true dynamic object. The other half is the case where the target meta-object is the wrapper meta-object of a static object. In this case, as I described earlier, the line of code in Listing 4-15 that calls the BindGetMember method on the input parameter target will result in a call back to the FallbackGetMember on the binder itself. This is because the BindGetMember method implemented in DynamicMetaObject simply calls the FallbackGetMember on the binder object it receives.

That's it for the interoperability protocol between binders and dynamic/static objects. I just described the interoperability protocol for GetMemberBinder. For the other eleven binder classes, the mechanism is the same. Like GetMemberBinder, each of the other eleven binder classes implements the overloaded abstract Bind method defined in DynamicMetaObjectBinder. Each binder class calls the Bind[Operation] method on the target meta-object in its implementation of the Bind method similar to what Listing 4-15 shows. Here [Operation] is the late bound operation the binder class is for. For example, the SetMemberBinder class is for the SetMember late-bound operation. The Bind method implementation in BindSetMember calls the BindSetMember method on the target meta-object. The Bind method implementation in InvokeMemberBinder calls the BindInvokeMember method on the target meta-object and so on. Like GetMemberBinder, each binder class defines an abstract Fallback[Operation] method. For example, SetMemberBinder defines an abstract FallbackSetMember method. InvokeMemberBinder defines an abstract FallbackInvokeMember method.

This description of the interoperability protocol is true only if the target language's binders choose to participate in the protocol. If you are implementing a language that does not need to interoperate with other DLR- based languages, you can derive your binder classes from just CallSiteBinder to take advantage of only the caching mechanism. This section covered a whole lot about the interoperability protocol. To help solidify your understanding of the protocol, let's see some more examples by extending the example we saw in Listing 4-14.

In Listing 4-14, the code passes a Python dynamic object to the binder. The binder is an instance of the SimpleGetMemberBinder class. Instead of a Python dynamic object, let's pass a static object to the binder and see what will happen. Listing 4-16 shows this example. The code is the same as the code in Listing 4-14, except that this time it creates an instance of the CSharpProduct class we saw earlier in this chapter. The instance is a static object and is referenced by the handClapper variable. The example code calls the Target delegate on the call site and passes it the handClapper variable.

Listing 4-16. Using an Instance of SimpleGetMemberBinder to Perform Late Binding for a Static Object

private static void RunGetMemberBinderAndStaticObjectExample()
{
    CallSiteBinder binder = new SimpleGetMemberBinder("name", false);

    CallSite<Func<CallSite, object, object>> site =
              CallSite<Func<CallSite, object, object>>.Create(binder);

    object handClapper = new CSharpProduct("Hand Clapper", 6);
    object name = site.Target(site, handClapper);
    Console.WriteLine("Product name is {0}.", name);
}

The result of running the example code is shown below. It may be a good exercise to stop reading for a moment and see if you can explain why the code prints “Product name is 3” using what you learned so far in this chapter. Read on if you are ready to see the answer. The reason it prints “Product name is 3” is because handClapper is a static object and is wrapped by a wrapper meta-object. The wrapper meta-object delegates the job of late binding to the binder. The binder in this example is an instance of our SimpleGetMemberBinder class and it returns the constant 3 as the late binding result. Therefore, the text “Product name is 3” shows on the screen.

Doing late binding in SimpleGetMemberBinder.
Product name is 3.

Obviously, in practice we would like to see “Product name is Hand Clapper” printed on the screen. The examples in this chapter and in Chapter 3 all show binder classes whose late-binding logic is as trivial as returning a constant number. That, of course, is not anywhere close to the real complexity of implementing practical classes. The examples so far leave out that complexity in order to demonstrate the key points without overwhelming you with details unnecessary to the discussion. But now that we are on the subject of implementing realistic late-binding logic, let's see how to quickly achieve that. Listing 4-17 shows the enhanced implementation of SimpleGetMemberBinder's FallbackGetMember method. Here the goal of the implementation is to provide realistic logic for binding to static .NET objects so that instead of printing “Product name is 3,” the late-binding logic in our binder class will cause the product name of a CSharpProduct object to be displayed. To achieve that, you normally need to write code that uses .NET reflection to find the right member property by name. Writing such code is no easy task. Just imagine the various cases you need to consider to bind to a method of a static .NET object using .NET reflection. The method may be overloaded. If so, you need to write the code that does method-overload resolution. If the method is an extension method, you need to figure out how to handle that. Some of the method's input parameters may have default values. Some of them may need to be matched by name instead of by position. This list goes on and on. Fortunately, since much of the code that deals with .NET reflection is the same across different languages' binder classes, the DLR provides some utility classes so that we don't need to do all the hard work. One of the utility classes is DefaultBinder and that's what the code in Listing 4-17 uses to bind to static objects. Using DefaultBinder is very easy and painless. The example code in Listing 4-17 simply creates an instance of DefaultBinder and calls its GetMember method.

Listing 4-17. Using DefaultBinder to Bind to Static .NET Objects

public override DynamicMetaObject FallbackGetMember(DynamicMetaObject target,
    DynamicMetaObject errorSuggestion)
{
    Console.WriteLine("Doing late binding in SimpleGetMemberBinder.");

    DefaultBinder defaultBinder = new DefaultBinder();
    return defaultBinder.GetMember(this.Name, target);
}

Now after enhancing the binding logic in SimpleGetMemberBinder, if you run the code, you'll see the following output:

Doing late binding in SimpleGetMemberBinder.
Product name is Hand Clapper.

Summary

This chapter has gone a long way from language interoperability in general to the details of the DLR's interoperability protocol. Here is a review of the most important concepts this chapter covers.

  • There are two kinds of objects—static objects and dynamic objects. Static objects can be treated as dynamic objects.
  • Binders have two main responsibilities—caching and interoperability.
  • There are two places in DLR where late-binding logic resides—binders and dynamic objects.
  • The late-binding logic in binders pertains to a target language.
  • The late-binding logic in dynamic objects pertains to those objects.
  • The key elements in the DLR that makes language interoperability possible are binders, dynamic objects, the interoperability protocol between binders and dynamic objects, and a common type system that consists of twelve operations,.

As you've seen in the previous chapter and this chapter, the nice thing about the DLR's design is that the various features, such as binders, dynamic objects, and some others that we'll look at later in the book, are well-decoupled and modularized. The two key responsibilities of binders are decoupled and implemented in separate classes. Caching is the responsibility of the CallSiteBinder class. Language interoperability is the responsibility of the DynamicMetaObjectBinder class and its derivatives.

For the twelve binder classes that derive from DynamicMetaObjectBinder, this chapter shows examples about the GetMemberBinder class. Towards the end of the chapter, we saw an example that uses the DefaultBinder utility class to perform late binding for the GetMember operation on a static .NET object. Even though we discussed a lot about binders, we haven't yet covered a number of important and advanced topics about binders. For example, other than GetMemberBinder, this chapter does not cover in detail the other eleven derivatives of the DynamicMetaObjectBinder class, or topics such as deferred binding, and COM interoperability. We'll discuss those topics in Chapter 11.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.227.10.45