Translator's Preface
This is more a free retelling, not a translation. I included in this article only those parts of the original that are directly related to the internal mechanisms of DLR or explain important ideas. Notes will be enclosed in square brackets.
Many .NET developers have heard of Dynamic Language Runtime (DLR), but know almost nothing about it. Developers writing in languages such as C # or Visual Basic avoid dynamic typing languages for fear of historically related scalability issues. They are also concerned about the fact that languages like Python or Ruby do not perform type checking at compile time, which can lead to runtime errors that are difficult to find and fix. These are well-founded fears that may explain why DLR is not popular among the majority of .NET developers even two years after the official release
[the article is quite old, but nothing has changed since then] . After all, any .NET
Runtime containing the words
Dynamic and
Language in its name should be designed strictly to support languages such as Python, right?
Slow down. While DLR was really designed to support the Iron implementation of Python and Ruby in the .NET Framework, its architecture provides much deeper abstractions.
Under the hood, DLR offers a rich set of interfaces for inter-process communication [Inter-Process Communication (IPC)]. Over the years, developers have seen many Microsoft tools for interaction between applications: DDE, DCOM, ActiveX, .Net Remoting, WCF, OData. This list may go on for a long time. This is an almost endless parade of acronyms, each of which represents a technology that promises that this year it will be even easier to exchange data or call remote code than before.
Language of languages
The first time I heard Jim Hugunin talk about DLR, his speech surprised me. Jim created a Python implementation for the Java Virtual Machine (JVM) known as Jython. Shortly before the show, he joined Microsoft to create IronPython for .NET. Based on his background, I expected him to focus on the language, but instead, Jim talked almost all the time about abstruse things like expression trees, dynamic call dispatch, and call caching mechanisms. Jim described a set of runtime compilation services that allowed any two languages to interact with each other with virtually no loss in performance.
During this speech, I wrote down a term that surfaced in my head when I heard Jim retelling the DLR architecture: the language of languages. Four years later, this nickname still characterizes DLR very accurately. However, having gained real-world experience in using it, I realized that DLR is not just about language compatibility. Thanks to the support of dynamic types in C # and Visual Basic, DLR can act as a gateway from our favorite .NET languages to data and code in any remote system, regardless of what type of equipment or software the latter uses.
To understand the idea behind DLR, which is an integrated mechanism in the IPC language, let's start with an example that has nothing to do with dynamic programming. Imagine two computer systems: one called the initiator, and the second - the target system. The initiator needs to execute the
foo function on the target system, passing there a certain set of parameters, and get the results. After the target system is discovered, the initiator must provide all the information necessary for the execution of the function in a format that is clear to her. At a minimum, this information will include the name of the function and the parameters passed. After unpacking the request and validating the parameters, the target system will execute the foo function. After that, it should pack the result, including any errors that occurred during execution, and send them back to the initiator. Finally, the initiator should be able to unpack the results and notify the goal. This request-response pattern is quite common and at a high level describes the operation of almost any IPC mechanism.
Dynamicmetaobject
To understand how DLR implements the presented pattern, let's look at one of the central classes of DLR:
DynamicMetaObject . We start by exploring three of the twelve key methods of this type:
- BindCreateInstance - create or activate an object
- BindInvokeMember - call the encapsulated method
- BindInvoke - object execution (as a function)
When you need to execute a method on a remote system, you first need to create an instance of the type. Of course, not all systems are object-oriented, so the term instance can be a metaphor. In fact, the service we need can be implemented as a pool of objects or as a singleton, so the terms “activation” or “connection” can be used with the same right as “instance”.
Other frameworks follow the same pattern. For example, COM provides a
CoCreateInstance function for creating objects. In .NET Remoting, you can use the
CreateInstance method from the
System.Activator class. DLR
DynamicMetaObject provides a
BindCreateInstance for similar purposes.
After using the
BindCreateInstance method,
something created can be a type that exposes several methods. The
BindInvokeMember metaobject
method is used to bind an operation that can call a function. In the picture above, the string foo can be passed as a parameter to indicate to the binder that a method with that name should be called. Additionally included is information about the number of arguments, their names and a special flag that indicates to the binder whether it is possible to ignore case when searching for a suitable named element. After all, not all languages are case sensitive.
When
something returned from
BindCreateInstance is just one function (or delegate), the BindInvoke method is used. To clarify the picture, let's look at the following small piece of dynamic code:
delegate void IntWriter(int n); void Main() { dynamic Write = new IntWriter(Console.WriteLine); Write(5); }
This code is not the best way to print the number 5 to the console. A good developer will never use anything so wasteful. However, this code illustrates the use of a dynamic variable whose value is a delegate that can be used as a function. If the delegate type implements the
IDynamicMetaObjectProvider interface, then the
BindInvoke method from
DynamicMetaObject will be used to bind the operation to the real work. This is because the compiler recognizes that the dynamic
Write object is
syntactically used as a function. Now consider another piece of code to understand when the compiler will generate
BindInvokeMember :
class Writer : IDynamicMetaObjectProvider { public void Write(int n) { Console.WriteLine(n); }
I will omit the implementation of the interface in this small example because it will take a lot of code to demonstrate this correctly. In this shortened example, we implement a dynamic meta object with just a few lines of code.
An important thing to understand is that the compiler recognizes that
Writer.Write (7) is an element access operation. What we usually call the "dot operator" in C # is formally called the "type member access operator". The DLR code generated by the compiler in this case will eventually call
BindInvokeMember , into which it will pass the string Write and parameter number 7 to the operation that is capable of making the call. In short,
BindInvoke is used to call a dynamic object as a function, while
BindInvokeMember is used to call a method as an element of a dynamic object.
Access properties through DynamicMetaObject
It can be seen from the above examples that the compiler uses the language syntax to determine which DLR binding operations should be performed. If you use Visual Basic to work with dynamic objects, then its semantics will be used. The access operator (dot), of course, is needed not only to access methods. You can use it to access properties. The DLR meta object provides three methods for accessing the properties of dynamic objects:
- BindGetMember - get property value
- BindSetMember - set the value of a property
- BindDeleteMember - delete an item
The purpose of
BindGetMember and
BindSetMember should be obvious. Especially now that you know how they relate to how .NET works with properties. When the compiler calculates the
get ("read") properties of a dynamic object, it uses a call to
BindGetMember . When the compiler computes set ("record"), it uses
BindSetMember .
Representation of an object as an array
Some classes are containers for instances of other types. DLR knows how to handle such cases. Each “array-oriented” meta-object method has an “Index” postfix:
- BindGetIndex - get value by index
- BindSetIndex - set value by index
- BindDeleteIndex - delete a value by index
To understand how
BindGetIndex and
BindSetIndex are used , imagine a
JavaBridge wrapper
class that can load files with Java classes and allows you to use them from .NET code without too much difficulty. Such a wrapper can be used to load the
Customer Java class, which contains some ORM code. The DLR meta object can be used to call this ORM code from .NET in the classic C # style. Below is sample code that shows how
JavaBridge can work in practice:
JavaBridge java = new JavaBridge(); dynamic customers = java.Load("Customer.class"); dynamic Jason = customers["Bock"]; Jason.Balance = 17.34; customers["Wagner"] = new Customer("Bill");
Since the third and fifth lines use the access operator by index ([]), the compiler recognizes this and uses the
BindGetIndex and
BindSetIndex methods when working with the meta object returned from
JavaBridge . It is understood that the implementation of these methods on the returned object will request the execution of the method from the JVM through the Java Remote Method Invocation (RMI). In this scenario, DLR acts as a bridge between C # and another language with static typing. Hopefully this clarifies why I called DLR “language of languages”.
The
BindDeleteMember method, just like
BindDeleteIndex , is not intended for use from languages with static typing such as C # and Visual Basic, since they do not support the concept itself. However, you can agree to consider “removal” some operation expressed by the means of the language, if it is useful to you. For example, you can implement BindDeleteMember as nulling an element by index.
Transforms and Operators
The last group of DLR meta-object methods is about handling operators and transformations.
- BindConvert - convert an object to another type
- BindBinaryOperation - using a binary operator on two operands
- BindUnaryOperation - applying a unary operator on one operand
The
BindConvert method
is used when the compiler realizes that the object needs to be converted to another known type. Implicit conversion occurs when the result of a dynamic call is assigned to a variable with a static type. For example, in the following C # example, assigning the variable
y leads to an implicit call to
BindConvert :
dynamic x = 13; int y = x + 11;
The
BindBinaryOperation and
BindUnaryOperation methods are always used when arithmetic operations ("+") or increments ("++") are encountered. In the example above, adding dynamic variable
x to constant 11 will call the
BindBinaryOperation method. Remember this little example, we use it in the next section to bang another key DLR class called CallSite.
Dynamic dispatch with CallSite
If your introduction to DLR did not go beyond using the
dynamic keyword, then you probably would never have known about the existence of CallSite in the .NET Framework. This modest type, formally known as
CallSite < T > , resides in the
System.Runtime.CompilerServices namespace . This is the “power source” of metaprogramming: it is filled with all sorts of optimization methods that make dynamic .NET code fast and efficient. I will mention
CallSite < T > performance aspects at the end of the article.
Most of what CallSite does in dynamic .NET code involves generating and compiling code in runtime. It is important to note that the
CallSite < T > class lies in the namespace that contains the words "
Runtime " and "
CompilerServices ". If DLR is a "language of languages", then
CallSite < T > is one of its most important grammatical constructions. Let's look at our example from the previous section again to get to know CallSite and how the compiler embeds them in your code.
dynamic x = 13; int y = x + 11;
As you already know, the
BindBinaryOperaion and
BindConvert methods will be invoked to execute this code. Instead of showing you a long listing of disassembled MSIL code generated by the compiler, I put together a diagram:
Remember that the compiler uses the language syntax to determine which dynamic type methods to execute. In our example, two operations are performed: adding the variable
x to the number (
Site2 ) and casting the result to int (
Site1 ). Each of these actions turns into CallSite, which is stored in a special container. As you can see in the diagram, CallSites are created in the reverse order, but are called in the right way.
In the figure you can see that the metaobject methods
BindConvert and
BindBinaryOperation are called immediately before the operations “create CallSite1” and “create CallSite2”. However, bound operations are only performed at the very end. I hope the visualization helps you to understand that binding methods and calling them are different operations in the context of DLR. Moreover, binding occurs only once, while a call occurs as many times as needed, reusing already initialized CallSites to optimize performance.
Follow the easy way
At the very heart of DLR, expression trees are used to generate functions tied to the twelve binding methods presented above. Many developers are constantly confronted with expression trees using LINQ, but only a few have deep enough experience to fully implement the
IDynamicMetaObjectProvider contract. Fortunately, the .NET Framework contains a base class called
DynamicObject , which takes care of most of the work.
To create your own dynamic class, you just need to inherit from
DynamicObject and implement the following twelve methods:
- TryCreateInstance
- TryInvokeMember
- Tryinvoke
- TryGetMember
- TrySetMember
- TryDeleteMember
- TryGetIndex
- TrySetIndex
- TryDeleteIndex
- Tryconvert
- TryBinaryOperation
- TryUnaryOperation
Do the method names look familiar? You must, because you just finished studying the elements of the Abstract
DynamicMetaObject class, which include methods like
BindCreateInstance and
BindInvoke . The
DynamicMetaObject class provides an implementation for
IDynamicMetaObjectProvider , which returns
DynamicMetaObject from its only method. The operations associated with the base implementation of the meta object simply delegate their calls to methods starting with “Try” on the
DynamicObject instance. All you have to do is to overload methods like
TryGetMember and
TrySetMember in a class inherited from
DynamicObject , while the meta object will take on all the dirty work with expression trees.
Caching
[You can read more about caching in my previous DLR article ]
The greatest concern when working with dynamic languages for developers is performance. DLR takes extraordinary measures to dispel these experiences. I briefly mentioned the fact that
CallSite < T > resides in a namespace called
System.Runtime.CompilerServices . In the same namespace lies several other classes that provide multilevel caching. Using these types, DLR implements three main levels of caching to speed up dynamic operations:
- Global cache
- Local cache
- Polymorphic Delegate Cache
The cache is used in order to avoid unnecessary waste of resources for creating bindings for a specific CallSite. If two objects of type
string are passed to a dynamic method that returns
int , then the global or local cache will save the resulting binding. This will greatly simplify subsequent calls.
The delegate cache, which is located inside CallSite itself, is called polymorphic, because these delegates can take different forms depending on which dynamic code is executed and which rules from other caches were used to generate them. The delegate cache is also sometimes called the inline cache. The reason for using this term is that the expressions generated by DLR and their binders are converted to MSIL code that goes through JIT compilation like any other .NET code. Compilation at runtime occurs simultaneously with the “normal” execution of your program. It is clear that turning on-the-fly dynamic code into compiled MSIL code during program execution can greatly affect application performance, so caching mechanisms are vital.