Google Website Translator Gadget

Sunday, 29 August 2010

RunSharp – IL Generation for Dummies

We’ve been doing some interesting work the past 2-3 weeks on creating a Rule Engine for our company’s flagship product, On Key.  The Rule Engine needs to evaluate rules entered by the end user at run-time according to our pre-defined grammar to determine a true/false answer.  This allows us tremendous flexibility in that end-users are able to specify under which conditions certain actions should occur within the system.  The main requirements for the technical solution was that it should be as quick as possible but it also has to cater for rules being changed at run-time by the end-users.  

Before heading off to create our own custom solution, we did some research to consider possible solutions to solving the same kind of problem.  Some of the solutions we came across were:

  1. Flee
  2. NCalc
  3. Irony
  4. Dynamic method generation using Expression Trees 

After looking at all of these solutions, we decided to rather create our own to give us the ultimate control and flexibility over extending the Rule Engine going forward.  Setting up the grammar using the excellent ANTLR and ANTLRWorks was quite easy to do.  ANTLR takes care of generating the C# code that will do the lexing, parsing and type checking of the rules entered by the user.  Thereafter we moved onto the run-time evaluation/compilation of the rules represented in the Abstract Syntax Tree created by ANTLR.  For this purpose, we created 3 tree walkers/visitors on our AST to compare against each other:

  1. Interpreter – Evaluate the rules dynamically as we walk the AST
  2. C# Code Generator – Walk the tree and generate C# code that is compiled using the C# compiler into an assembly
  3. IL Code Generator – Walk the tree and generate dynamic IL code in memory

Interpreter

The main benefit of writing an interpreter is that you get rid of the complexities associated with managing rules that change at run-time as you don’t need to compile the rules.  You can still use caching to store a format of the constructed AST for the parsed rules to prevent the overhead of parsing/lexing the rule every time, but you don’t need to worry about unloading/loading DLL’s of code that was previously compiled.  The obvious down side of this solution is the performance overhead compared to compiled C# code.

C# Code Generator

The main benefit of writing a C# code generator is that we can use the C# compiler and our existing C# skill set to create rules that perform as quickly as possible.  The downside however is that you cannot unload an assembly out of a .NET AppDomain.  To support the requirement of the rules changing at run-time, we therefore would have to create a temporary AppDomain and load the assembly in there.  This is a technique used by the excellent CS-Script to evaluate C# scripts at run-time.  You also need FullTrust permissions to launch the C# compiler at run-time, which is problematic for our Silverlight IIS hosted application.

IL Code Generator

The main benefit of writing a IL generator is that it is gives you the performance equivalent to that of the compiled C# code but also the dynamics of the interpreter in the sense that you can generate the IL in memory at run-time and basically drop the DynamicMethod from memory when rule changes are made by the end users and the rules therefore need to be recompiled.  However, generating IL directly is notoriously difficult and error-prone.  Our immediate concerns were for the technical complexities of managing this solution going forward.  That was until, we discovered RunSharp.  We came across RunSharp in this CodeProject article.  It essentially provides a run-time wrapper on top of Reflection.Emit IL generation to allow you to easily construct dynamic code at run-time.  To illustrate, let me take an example of C# rule generated by the C# Code Generator we created and illustrate the equivalent RunSharp code.

C# Code

public static bool Rule2(IOptionValueProvider options)
{
  // "CA_FIL_STE_LIFTING_UNIT = 'WITH FIL STE LIFTING UNIT'"

  return String.Compare(options.GetOptionValueAsString(1782), "WITH FIL STE LIFTING UNIT", StringComparison.OrdinalIgnoreCase) == 0;
}

IL Code

public static void Rule2CIL(TypeGen typeGen)
{
   // "CA_FIL_STE_LIFTING_UNIT = 'WITH FIL STE LIFTING UNIT'"

   CodeGen g = typeGen.Public.Static.Method(typeof (bool), "Rule2").Parameter(typeof (IOptionValueProvider), "options");
   {
       Operand result = Static.Invoke(typeof (String), "Compare", g.Arg("options").Invoke("GetOptionValueAsString", 1782), "WITH FIL STE LIFTING UNIT", StringComparison.OrdinalIgnoreCase) == 0;
       g.Return(result);
   }
}

You’ll see that by using the RunSharp API we are able to construct code that looks quite similar to the C# code, except that in the background RunSharp will take care of generating the IL for us.  RunSharp is able to provide us with such an API through the judicious use of implicit type casting and operator overloading.  Let’s consider a more complex rule involving multiple operands:

C# Code

public static bool Rule10022(IOptionValueProvider options)
{
   // "CA_PACKSHAPE eq 'B' and NOT ('2755900-0100' IN CA_REBUILDING_KIT)"

   return (String.Compare(options.GetOptionValueAsString(1009), "B", StringComparison.OrdinalIgnoreCase) == 0) && (!(options.CheckValueInOption("2755900-0100", 1495, OptionDataType.String)));
}

IL Code

public static void Rule10022CIL(TypeGen typeGen)
{
   CodeGen g = typeGen.Public.Static.Method(typeof (bool), "Rule10022").Parameter(typeof (IOptionValueProvider), "options");
   {
       Operand op1 = Static.Invoke(typeof (String), "Compare", g.Arg("options").Invoke("GetOptionValueAsString", 1009), "B", StringComparison.OrdinalIgnoreCase) == 0;
       Operand op2 = g.Arg("options").Invoke("CheckValueInOption", "2755900-0100", 1495, OptionDataType.String);
       g.Return((op1) && (!op2));
   }
}

Creating a Tree walker/visitor to traverse the AST and emit the appropriate RunSharp API calls was a snap to do and really gives us the best of both worlds - the speed of compiled code and the flexibility of managing rules that change at run-time without having to worry about the complexities of IL generation.  The one or two issues that we discovered were quickly corrected by the author of the library Stefan Simek.  So if you ever want to create IL code at run-time again, you should really seriously consider RunSharp.  As the post title says, it really is IL generation for Dummies.

No comments:

Post a Comment