Grokay DLR

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:



  1. BindCreateInstance - create or activate an object
  2. BindInvokeMember - call the encapsulated method
  3. 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); } //    } void Main() { dynamic Writer = new Writer(); Writer.Write(7); }
      
      





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:



  1. BindGetMember - get property value
  2. BindSetMember - set the value of a property
  3. 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:



  1. BindGetIndex - get value by index
  2. BindSetIndex - set value by index
  3. 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.



  1. BindConvert - convert an object to another type
  2. BindBinaryOperation - using a binary operator on two operands
  3. 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:



  1. TryCreateInstance
  2. TryInvokeMember
  3. Tryinvoke
  4. TryGetMember
  5. TrySetMember
  6. TryDeleteMember
  7. TryGetIndex
  8. TrySetIndex
  9. TryDeleteIndex
  10. Tryconvert
  11. TryBinaryOperation
  12. 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:



  1. Global cache
  2. Local cache
  3. 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.



All Articles