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:
Method | Purpose |
---|---|
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!