Dynamic Compilation: Implementing Eval() in C#

Download CsEval application (22K): includes Compiler class

Abstract

Dynamic compilation of code can be very useful in an extensible system. Many interpreted languages have Eval() method that evaluates expressions on the fly. Eval() method of the Compiler class described in the article provides similar capabilities for C#. It offers a way to compile code on the fly in a secure fashion, and encapsulates most of the intricate details behind a simple interface.

Background

I was investigating how Roundup issue tracker handles database upgrades behind the scenes, and I found that they keep the Database schema as a string in one of the tables. The string is actually a Python dictionary, and they call Python's Eval() method to convert it into an object. See my blog post on the subject.

Using the language compiler (or interpreter) as your de-serializer may not have the best performance, but it does look interesting. This made me wonder whether it is possible to implement Eval() in C#. It may have more uses than just de-serializing a dictionary. For example, you may want to execute custom business rules, scripts, or other snippets. I worked on a real project that actually used dynamic C# to code transition rules between web pages.

Eval() In Interpreted Languages

In languages like Lisp, Python and JavaScript, Eval() is a function that interprets an arbitrary string as Lisp/Python/JavaScript code and executes it. The evaluated code has access to all symbols known to the interpreter at the time of the call. E.g., the following Python snippet calls f() from the evaluated code and prints "20":

# Python
def f(x):
 return 2*x

print eval("f(10)")

Dynamic Compilation in .NET

.NET framework has a built-in C# compiler, which allows .NET programs to dynamically compile and load new code. However, this is still a compiler. Dynamically compiled code is treated as an external entity. There is no easy way to achieve the same level of context sharing as in Python or Lisp. The compiled assembly may reference the calling assembly, thus gaining access to its public classes and interfaces, but it cannot see local variables or class fields available to the calling code.

// C#
namespace Foo.Bar
{
  public class Baz
  {
    //Public fields for demonstration only, don't do it at home
    public static string StaticString = "abc";
    public int IntField = 11; 
 
    void TestEval()
    {
      int localVar = 42;
      var c = new Compiler();
      c.Eval("localVar+1");                         // won't work, localVar is not defined
      c.Eval("this.IntField");                      // won't work, this is not defined
      c.Eval("Foo.Bar.Baz.StaticString.ToUpper()"); // returns "ABC"
      c.Eval("new Foo.Bar.Baz().IntField");         // returns 11
    }
  }
}

The Compiler Class

I have actually built the Compiler class that implements the Eval() method just as shown above. Note, that Eval() accepts only expressions like 2+2, Math.Cos(3.14). It cannot directly accept function or class definitions. However, the Compiler class implements a number of helper methods that make your life easier:

MethodPurpose
Using(namespace)Behaves like a normal "using" directive. Without it, all class names in the expression must be fully qualified.
Reference(assemblyPath)Adds reference to assemblyPath when compiling the expression
ReferenceAllLoaded()References all assemblies loaded in current domain
Define(code)Add a source file to the compiled assembly. You may define your classes here.
SetPermissions(permissionSet)Set elevated permissions for evaluated code
Clear()Forget all the definitions, using directives, references, and permissions.

The calls to the helpers may be chained together like this:

new Compiler()
 .Reference("System.Xml.dll")
 .Define("using System.Xml; namespace MyStuff { " +
        "public class MyDocument { " +
        "public XmlDocument Func() {return new XmlDocument();}}}")
 .Using("MyStuff")
 .Eval("new MyDocument().Func()");

You need Define() if you want to define your own classes or methods. The string passed to Eval() must be an expression, i.e. something that can go after a return. Unfortunately, expressions in C# are not very powerful. In particular, they cannot define classes, functions, or even loops. Expressions, however can call classes, which may contain arbitrary code.

Passing Arguments

You can supply an optional parameter to Eval() of type IDictionary<object,object>. The evaluated expression can access this value as args. E.g.

var args = new Dictionary<object,object>() { {0, "abc"} };
new Compiler().Eval("args[0].ToUpper()");

CsEval Application

CsEval application is a console application that passes its command argument to new Compiler().Eval(). E.g. cseval Math.Cos(0) would display 1. The application contains the Compiler class itself and the unit tests for it. The tests in EvalIntegrationTest.cs explore some more advanced scenarios for using the Compiler class. The tests require nUnit and Rhino Mocks 3.5 RTM.

Security

As with any scripting technology, you don't want to give potentially untrusted parties ability to run arbitrary code on behalf of your program. Before you let the users run scripts, you must make sure they are properly sandboxed. This is what SetPermissions() call is for. By default evaluated code has no elevated permissions, so attempt to write to a file or read a registry key will result in SecurityException. It proved to be somewhat tricky to assign permissions to the compiled code, see "Security of Dynamically Compiled Assembly" for details.

Performance

The most important thing to remember is that the actual compilation always occurs on disk. Even if you do something as simple as new Compiler().Eval(42), the framework actually saves the source strings to a temporary location and launches the csc.exe process. I have not found a way around it. CompilerParameters.GenerateInMemory property is a white lie - it just means that the framework will thoroughly clean up all temporary files after the compilation. So, beware of the poor performance.

C# Language Version

By default Compiler will accept C# 3.0 features such as collection initializers, "var" declarations, and the like. To stick to the .NET 2.0 version of the language, you can pass your own instance of CSharpCodeProvider: as in new Compiler(new CSharpCodeProvider()). The compiler cannot do .NET 1.1, because its wrapper code uses generics. Theoretically you can pass a VBCodeProvider, but it won't do any good, because the wrapper code is in C#.

The Wrapper Code

It is not possible to compile a single expression: the framework will accept only a well formed source file. Therefore, the compiler emits some helper code that wraps your expression. This will throw line numbers off when compiler errors are reported. Additional source files supplied via Define() are compiled as is.

Currently the wrapper code looks approximately like this:

// User-supplied using directives go here
[assembly:System.Security.SecurityTransparent] 
namespace Ikriv.Eval.GeneratedCode 
{
   public class Foo : IComparer<IDictionary<object,object>>
   { 
       public int Compare( IDictionary<object,object> args, 
                          IDictionary<object,object> result) 
       { 
           result[String.Empty] = evaluated expression goes here; 
           return 0; 
       } 
   } 
}

I say "approximately", because all class names are actually fully qualified. I avoided putting specific "using" directives into the code. First, they would pollute the expression's namespace. Second, the user may supply his own directives, and any duplicate "using" directives would cause compilation errors.

Admittedly, this is somewhat unorthodox use of the IComparer interface, to put it mildly. Keep in mind that this code ends up in a dynamically compiled assembly. To call it, one must either use reflection, or rely on one of the interfaces defined in the System assembly. Reflection is suboptimal, not because of the performance (any reflection delays pale compared to launching the csc.exe process), but because an exception thrown in the user expression will become TargetInvocationException, which is annoying and difficult to debug. I wanted a direct call, and IComparer provided the best (or should I say the least evil) solution.

The [assembly:SecurityTransparent] attribute prevents the generated assembly from elevating permissions beyond what Compiler sets. See security discussion for more details.

Conclusion

Dynamic compilation may not something you would use every day, but it may become a very powerful tool. The Eval() implementation oferred in this article hopefully brings it closer to home and reduces time to market. Happy compiling!