Introduction
Some days ago, I read the good CodeProject article "Compiling with CodeDom" by Gustavo Bonansea, where he shows how to use .NET classes from the CodeDom
namespace, to compile .NET code programmatically (and, of course, without using the command-line!). As rightly mentioned in the final comments of that article, .NET allows you not only to programmatically compile code, but also to programmatically generate code. In the following paragraphs, I will show a simple example of code generation.
Background
Imagine to have to implement an arithmetic expression interpreter, that is a piece of code capable to take an expression as an input string and to compute the result of that expression. The classic approach to this problem is to write a parser that recognizes each token of the input expression (numbers, operators, parenthesis and so on), builds an expression tree representing the expression structure, and then walks that tree to compute the result.
Another approach consists in generating a function that actually computes that expression. If you think about an expression like:
1 + 2 * 3 - 4 / 5
it would be perfect to compute the result simply invoking a function like:
Public Shared Function MyMethod() As Single
Return 1 + 2 * 3 - 4 / 5
End Function
Well, the code of this sample actually does that, in these steps:
- it creates the function (as a shared method of the class
MyClass
in the namespace MyNamespace
) through .NET CodeDom
;
- it compiles the generated code in an assembly;
- it loads the newly-created assembly and dynamically invokes
MyMethod
.
The Code
Here is the code of a console application that demonstrates the dynamic code generation and execution for the "arithmetic expression interpreter" depicted above.
Imports System
Imports System.Collections
Imports System.CodeDom
Imports System.CodeDom.Compiler
Imports System.IO
Imports System.Reflection
Imports Microsoft.CSharp
Imports Microsoft.VisualBasic
Public Class ParseExpr
Public Shared Sub Main()
Console.WriteLine("Expression evaluation demo")
Console.WriteLine("--------------------------")
Console.WriteLine("Type an arithmetic expression: ")
Dim Expression As String = Console.ReadLine()
Console.WriteLine("Your expression is evaluated as: ")
Console.WriteLine(ProcessExpression(Expression))
Console.ReadLine()
End Sub
Private Shared Function ProcessExpression(ByVal _
Expression As String) As Single
Dim VBcp As New VBCodeProvider()
Dim cg As ICodeGenerator = VBcp.CreateGenerator()
Dim AssemblySourceFile As String = "MyClass.vb"
Dim AssemblyFile As String = "MyClass.dll"
Dim tw As TextWriter
tw = New StreamWriter(New _
FileStream(AssemblySourceFile, FileMode.Create))
GenerateEvalMethod(tw, Expression, cg)
tw.Close()
Dim cc As ICodeCompiler = VBcp.CreateCompiler()
Dim cpar As New CompilerParameters()
cpar.OutputAssembly = AssemblyFile
cpar.ReferencedAssemblies.Add("System.dll")
cpar.GenerateExecutable = False
cpar.GenerateInMemory = False
Dim compRes As CompilerResults
compRes = cc.CompileAssemblyFromFile(cpar, AssemblySourceFile)
Dim ass As [Assembly]
ass = compRes.CompiledAssembly
Dim ty As Type = ass.GetType("MyNamespace.MyClass")
Dim mi As MethodInfo = ty.GetMethod("MyMethod")
Dim Result As Single = CType(mi.Invoke(Nothing, Nothing), Single)
Return Result
End Function
Private Shared Sub GenerateEvalMethod(ByVal tw As _
TextWriter, ByVal Expression As String, _
ByVal CodeGen As ICodeGenerator)
Dim ns As New CodeNamespace("MyNamespace")
Dim ty As New CodeTypeDeclaration("MyClass")
ty.IsClass = True
ty.TypeAttributes = TypeAttributes.Public
ns.Types.Add(ty)
Dim mm As New CodeMemberMethod()
mm.Comments.Add(New _
CodeCommentStatement([String].Format("Expression:{0}", _
Expression)))
mm.Name = "MyMethod"
mm.ReturnType = New CodeTypeReference("System.Single")
mm.Attributes = MemberAttributes.Public Or MemberAttributes.Static
mm.Statements.Add(New _
CodeMethodReturnStatement(New CodeSnippetExpression(Expression)))
ty.Members.Add(mm)
CodeGen.GenerateCodeFromNamespace(ns, tw, Nothing)
End Sub
End Class
Some notes
- Because the input expression is computed as a part of the application code, you can insert in it any valid function. For example:
System.Math.Pow(5,2) * System.Math.PI
will correctly compute the area of a circle with radius = 5. This works because the System.dll is already referred with:
cpar.ReferencedAssemblies.Add("System.dll")
If you want to support in your expressions other functions as well (also from custom assemblies), just remember to add an item to the ReferencedAssemblies
collection.
- It has been noted (by mav.northwind - thanks!) that there is no need to actually generate the physical assembly file MyClass.dll, because you can instruct the compiler to generate the assembly in memory. To do this, modify the code so:
cpar.GenerateInMemory = True
- We can also avoid the physical generation of the MyClass.vb source file, with few modifications to our code. You only need to comment out these lines:
tw = New StreamWriter(New FileStream(AssemblySourceFile, FileMode.Create))
GenerateEvalMethod(tw, Expression, cg)
tw.Close()
...
compRes = cc.CompileAssemblyFromFile(cpar, AssemblySourceFile)
substituting them with:
Dim sb As New System.Text.StringBuilder()
tw = New StringWriter(sb)
GenerateEvalMethod(tw, Expression, cg)
...
compRes = cc.CompileAssemblyFromSource(cpar, sb.ToString())
Conclusions
Of course, dynamically generating, compiling, and running code in this way is not a good solution when you need maximum performance. But you will agree with me that the flexibility of this approach could be decisive in a lot of situations.