A Walkthrough of Dynamically Compiling C# code
One of my side-projects requires me to build some of my own tools. In this case: a code generation tool. In a future post, I’ll describe the tool and provide its source, but for now I did want to share some of what I discovered on the way.
First, the tool I am building essentially reads a simple DSL and generates C# source code from it. To be more specific it doesn’t just generate C# source code, it also compiles it into an assembly for you so that it is easy to consume in binary form. It’s this last part I will focus on: taking c# code and making it into an assembly.
I created a simple Console application called DemoCompiler. The entire source is below. I’ll walk you through it section-by-section to illustrate what is going on.
THE SOURCE CODE TO GET STARTED
using System;
using System.Collections.Generic;
namespace DemoCompiler
{
class Program
{
static void Main(string[] args)
{
// Create the source code
string source = @"
namespace Foo
{
public class Bar
{
static void Main(string[] args)
{
Bar.SayHello();
}
public static void SayHello()
{
System.Console.WriteLine(""Hello World"");
}
}
}
";
// Setup for compiling
var provider_options = new Dictionary<string, string>
{
{"CompilerVersion","v3.5"}
};
var provider = new Microsoft.CSharp.CSharpCodeProvider(provider_options);
var compiler_params = new System.CodeDom.Compiler.CompilerParameters();
string outfile = "D:\\Foo.EXE";
compiler_params.OutputAssembly = outfile ;
compiler_params.GenerateExecutable = true;
// Compile
var results = provider.CompileAssemblyFromSource(compiler_params, source);
// Print out any Errors
Console.WriteLine("Output file: {0}", outfile);
Console.WriteLine("Number of Errors: {0}", results.Errors.Count);
foreach (System.CodeDom.Compiler.CompilerError err in results.Errors)
{
Console.WriteLine("ERROR {0}", err.ErrorText);
}
}
}
}
FYI – pay attention to the two namespaces that are used here:
- Microsoft.CSharp
- System.CodeDom.Compiler
Now run the compiler
Excellent. No errors. Ok, we have a EXE.
Let’s run it…
And now step through the code
TIP: Always check for compile errors
Now let’s play around with it and see what happens.
USING OBJECTS ANOTHER ANOTHER ASSEMBLY
First. let’s try creating a simple Rectangle from System.Drawing.Rectangle and printing it.
// Create the source code
string source = @"
namespace Foo
{
public class Bar
{
static void Main(string[] args)
{
Bar.SayHello();
}
public static void SayHello()
{
System.Console.WriteLine(""Hello World"");
var r = new System.Drawing.Rectangle(0,0,100,100);
System.Console.WriteLine(r);
}
}
}
";
This time the compile doesn’t work…
The reason is that the output assembly doesn’t contain a reference to System.Drawing – we fix that very simply by modifying the compiler parameters
string outfile = "D:\\Foo.EXE";
compiler_params.OutputAssembly = outfile ;
compiler_params.GenerateExecutable = true;
compiler_params.ReferencedAssemblies.Add("System.Drawing.Dll");
And try compiling…
And then running the EXE
USING LINQ
We love LINQ these days, so let’s try using it to print the numbers from 0 to 9
// Create the source code
string source = @"
using System.Collections.Generic;
using System.Linq;
namespace Foo
{
public class Bar
{
static void Main(string[] args)
{
Bar.SayHello();
}
public static void SayHello()
{
System.Console.WriteLine(""Hello World"");
var r = new System.Drawing.Rectangle(0,0,100,100);
System.Console.WriteLine(r);
System.Console.WriteLine( string.Join("","", Enumerable.Range(0,10).Select(n=>n.ToString()).ToArray() ) );
}
}
}
";
What could be wrong here? We are missing a reference to “System.Core.Dll” (that’s where Linq is)
string outfile = "D:\\Foo.EXE";
compiler_params.OutputAssembly = outfile ;
compiler_params.GenerateExecutable = true;
compiler_params.ReferencedAssemblies.Add("System.Drawing.Dll");
compiler_params.ReferencedAssemblies.Add("System.Core.Dll");
Now try compiling.
It works, so run the EXE …
Perfect.
WHAT WE LEARNED
- the basics of compiling source
- How to tell if the compilation worked correctly
- how to specify that an executable is created
- how to ensure that we can use C# 3.0 syntax
- how to address common issues in using Linq and referring to other DLLs
CREATING A DLL INSTEAD OF AN EXE
This is very simple
string outfile = "D:\\Foo.DLL";
compiler_params.OutputAssembly = outfile ;
compiler_params.GenerateExecutable = false;
Just set the output filename to end with “.DLL” and set GenerateExecutable to false.
If you run the compiler now, it will generate a DLL that you can import into another project.
SOURCE CODE
You can get it here: http://cid-19ec39cb500669d8.skydrive.live.com/self.aspx/Public/Dev/SampleCode/Blog-Post-Samples/Saveenr-Blog-Post-%7C52009-08-11%7C6.zip