Background
In August, I submitted a post about Lambda Expressions and Differentiation. There, I told shortly about a dynamic compiler for lambda expressions using CodeDom. I've enhanced the compiler so that it now deserves a post of its own.
Introduction
I'll start with the original problem that I needed to solve. I wanted to make a console application that would accept lambda expressions wrote by the user, transform it in another lambda expression and show it. After some research, let me list some of the stuff that can or can't be done with .NET:
- One can write a lambda expressions to a easily readable string (that is similar to C# code, but it's not compilable);
- One can dynamically create lambda expressions using static functions of the Expression class;
- One cannot create (at least not directly) lambda expressions from a string containing valid C# code;
- One can dynamically compile code using CodeDOM.
Due to feature #1, it's effortless to show the transformed lambda expressions to the user. Feature #2 is essential to the transformations that I intended to do, which was finding derivatives (please, read the original post if you're interested).
A major problem came along when I realized the fact #3. So, the source lambda expressions in the first versions of my little application were hard coded. After I finished the derivation part, I began searching for options to generate lambda expressions from user inputted strings.
One alternative was to make a parser (or find on the web) that put together an expression using feature #2. The other alternative is to let the framework do it by dynamically compiling the code (feature #4). Since the second is promptly available and is much more reliable than any parser I could code, that's what I chose to use.
Dynamic Compilation
Before I continue, let me state that I'll not get into the details about using CodeDOM. Here is a much more detailed explanation. Also, although you can compile VB.NET too, I'll talk only about C# here, sorry.
Basically, it works like this. You need the full text of one or more source code files (including usings, namespaces, classes, and so forth). Then, through a CodeDomProvider, you can compile your code to an Assembly. The assembly can exist only in memory or it could be generated directly to a file. You can access and execute what is in the assembly by reflection.
For my particular case, where I wanted to generate lambda expressions from a string, CodeDOM is too generic. So, I made a wrapper around it that compiles a lambda expression code string by filling the template below. Then, using reflection I executed DynamicFunc and forwarded the returned LambdaExpression.
using $Using0$;
using $Using1$;
//...
using $UsingN$;
namespace JpLabs.DynamicCode.DynamicAssembly
{
public static class DynamicClass
{
public static LambdaExpression DynamicFunc() {
return (Expression<$DelegateType$>)( $LambdaExpresion$ );
}
}
}
This solution solved my problem, but it has some issues. First, it's slow. Actually, it is not nearly as fast as it could be. One could say that I'm using an elephant gun to kill a fox. It's possible to modify it to compile several expressions at once, but that was not the case. I needed to compiled expressions one by one as the user inputs it.
The bigger issue is that, each time an expression is compiled, an assembly is generated. Unlike object instances, the memory of loaded types and assemblies can't be release. In fact, there's one way to do that, it's by unloading the entire application domain.
Cross AppDomain Dynamic Compilation
One application might use multiple domains for several reasons. But I believe nearly that all of them have to deal with one typical problem. Code in one domain cannot access data in other domain. So, they have to communicate in alternative ways. The two ways that are most straightforward are serialization and remoting.
The data I wanted to cross between domains are lambda expresssions. Unfortunately, a LambdaExpression is not Serializable. And remoting is not an option too. I need to be able to unload the compiler domain, and by doing this, all remoting objects would be released and the proxies would cease to work.
I thought I had hit a impassable wall. But I found this article with a solution. It also uses multiple domains to dynamically compile code. If you read to this point, you will probably like to take a look there too.
The idea somewhat crazy. .NET reflection allows you to access the IL of a method. One can pack it into a Serializable class and send it to another domain. There, the method would be reconstructed using the IL. This can be accomplished with the semi-magic DynamicMethod class. To let you understand its wonderful features, I'll quote MSDN remarks about it:
You can use the DynamicMethod class to generate and execute a method at run time, without having to generate a dynamic assembly and a dynamic type to contain the method. The executable code created by the just-in-time (JIT) compiler is reclaimed when the DynamicMethod object is reclaimed. Dynamic methods are the most efficient way to generate and execute small amounts of code.
The input for a DynamicMethod is exactly what we have: the IL that came from the compiled code on the other domain. The output is a delegate to a newly created (and releasable) function. Since the function returns our so desired lambda expression, we just need to call it.
Now, let me summarize the steps involved in this lambda expression compiler:
- Create an AppDomain;
- Instantiate a compiler (actually, a wrapper around CodeDOM) on the new AppDomain;
- Compile the input string into an in-memory assembly using the template I've shown above;
- Unload the compiler domain (or keep it for reuse and unload it later);
- Extract the IL of the DynamicFunc method (please, refer to the template), and send it to the original domain;
- Employ a DynamicMethod to get a delegate from the compiled IL;
- Call the delegate and it shall return a LambdaExpression.
Conclusion
Here's an reliable way to dynamically compile code without requiring always more memory. To be honest, I did some improvements that I didn't mention above. To mention, I tried to make the compiler generic enough so that it could compile other stuff than lambda expression. I think you can simply extend the compiler to achieve that. Please, tell me if anyone of you get to the point of doing that. I'll even let you in a short future-possible-enhancements-list:
- Extend compiler to compile other stuff than lambda expressions;
- Improve the compiler so that it can compile a batch of code strips.
And, finally, the source code download link: here. Of course, this source includes a test project for the compiler. And, of course, the test project is my Differentiator.
Thanks!
8 comments:
Hi,
This is brilliant! You saved me a lot of time and digging. Exactly what I needed. Thank you!
Daniel
Thanks for the feedback. Is always good to know that someone is reading my posts and, even better, using my code. :)
JP
Interesting stuff. A colleague of mine had a similar problem to solve, but chose the to build a parser instead.
It works well for the most part but after I unit tested it thoroughly, I discovered it had some flaws that caused the entire app to die.
When I revisit the problem, I'll take a look at your approach, cheers :)
I considered building a parser, but I would never completely trust it.
A colleague of mine tried the CodeDOM approach, but he was struck by an increasing number of dynamic assemblies loaded in memory.
Then he saw my solution, and it worked without any need of adaptation. :)
Is there anywhere I can get the source?
jesse, in 2012 I move all my "JPLabs" code to https://bitbucket.org/jpbochi/jplabscode/src. I didn't touch it since then, and I don't even know if it compile these days. I'm glad to try to answer to whatever questions you have about it, anyway.
thank you,
JP
/// Based the modifications from
/// https://www.strathweb.com/2018/01/easy-way-to-create-a-c-lambda-expression-from-a-string-with-roslyn/
using Microsoft.CodeAnalsys.CSharp.Scripting;
using Microsoft.CodeAnalsys.Scripting;
using System;
using System.Reflection;
using System.Threading.Task;
namespace www.stratweb.com
{
public static async Task ActionAsync[T](T item, string code)
{
var options = ScriptOptions.Default.AddReferences(typeof(T).Assembly);
var expression = await CSharpScript.EvaluateAsync[Action[T]](code, options);
expression(item);
}
public static async Task[TResult] FunctionAsync[TValue, TResult](TValue value, string code)
{
var options = ScriptOptions.Default.AddReferences(typeof(TValue).Assembly, typeof(TResult).Assembly);
var expression = await CSharpScript.EvaluateAsync[Func[TValue, TResult]](code, options);
return expression(value);
}
}
Post a Comment