Wednesday, 9 March 2016

Runtime Code Generation, Methods

The most common occurrence of runtime code generation is not creating new classes like in the previous article, but just creating a "function" (a static method). The idea of expanding one class with new methods or replacing one method implementation for another is not directly doable in C# (well, for the latter maybe you could use some instrumentation and IL library, but the normal way would be to use some dynamic proxy approach).

We have several options for creating new methods. The most immediate one after what we saw in the previous post is just creating a class with that method in it, following any of the 3 approaches used in that post. Once we have created the new Type, we can easily obtain a Delegate pointing to the method using some of the CreateDelegate overloads, let's say:

//use any of the 3 methods "CodeDOM, Roslyn or Mono) from previos post
Type tp = @"
    using System;
    namespace Formatters
             {
              public static class StaticDateFormatter2
              {
                  public static string Format(DateTime dt) { return dt.Year + ""_"" + dt.Month + ""_"" + dt.Day; }
                  //public static string Format(DateTime dt) { return '-'; }
              }
    }",
                "Formatters.StaticDateFormatter2", 
                new List<Assembly>()
                {
                 
                });

Func<string, string> formatter = (Func<string, string>)System.Delegate.CreateDelegate(typeof(Func<string, string>), tp, "Format");

formattedText = formatter("hi");

This approach loads a new assembly in memory to host the new class, and the assembly will remain there until the application is closed. If we want a cleaner approach, creating just a method, without a new class and a new assembly, there are 2 ways to go:

  • Dynamic Methods, aka Lightweight code generation. No new assembly is loaded and no new class is loaded, the method is just compiled and kept somewhere in memory. If you create a Delegate from a dynamic method and check for its DefiningType, you'll see it's null:
    myDynamicMethod.CreateDelegate(typeof(Func)).Method.DeclaringType)
    The obvious problem with Dynamic Methods is that you have to use IL. I wrote some IL code many years ago, it's pretty simpler than writing assembly for a real, register based, processor, but anyway it's quite unnatural and error prone for many of us. This project tries to make it a bit more friendly.

  • Expression Trees. Honestly, I've never written expression trees myself, but it seems tedious to say the least.

What seems puzzling to me is that Roslyn has not improved anything in this respect. It's useless if what you want is to compile just a function (method) from a C# string and want to avoid the burden of a new assembly being generated and loaded in memory. If you want to spare the loading of new assemblies you still have to resort to the uncomfort of IL or Expression Trees. In this sense, there is library, Sigil acting as a wrapper around ILGenerator that intends to make it easier to write dynamic methods. Years ago Rick Strahl expressed his surprise for not being able to create a Dynamic Method from a C# string. I find it odd that years later the so powerful Roslyn (Microsoft.CodeAnalysis) has not some sort of "DynamicMethodGenerator". Wondering if some "compiler freak" would ever think of doing just something like that, in the recent comments to that post we can find a link to a CodeProject article from one guy that basically did it. I've downloaded the code and the man basically implemented a C# to ILGenerator compiler!

There's another option that spares us having to create the class (in the source code string), we can use the Roslyn Scripting API. You can create a ScriptRunner delegate from a script piece. The third line is to avoid coupling with the technique used to create our method, we better wrap the ScriptRunner in a generic Func delegate:

var script = CSharpScript.Create<string>("var up = '{' + x.ToUpper() + '}'; return up;", globalsType: typeof(Globals));
ScriptRunner runner = script.CreateDelegate();
Func func = (it) => runner(new Globals { x = it }).Result;

But this Roslyn Script still will create a new class and load a new assembly, so it's not any improvement really. I'm pretty surprised by this. I would have expected the scripting api to try to be as lightweight as possible, hence using Lightweight Code Generation (Dynamic Methods), but unfortunately it's not the case.

No comments:

Post a Comment