Compiling C# at runtime – part two

Shortcut: T1P2

Hello,
this is the secound part of my how to compile C# code at runtime toturial. In the first part I have shown you how to compile code that is in a string. This time, we go a little further because we are now working with external files and add some object orientation. These are the steps of the tutorials:

  1. Getting started >> “Hello World!”
  2. Loading external files with code and saving the compiled result
  3. Using interfaces
  4. Simple use case
  5. From .Net to Mono

This time we will start with a practical part and have some theory in the end.

Open your C# IDE and create a new “Console Application” project. Next, we check our project references. We need the following:

  • System
  • Microsoft.CSharp

If you are missing one, please add it. This time, we are object-oriented because we want to reuse our classes from now on and it is cleaner.
We start with our compiler. Create a new file for the class and add it to your project. I have a new file for each class (it is a standard in many code conventions and more practical). In my example, I call the file and the class: CodeCompiler
You should now have an empty class or you have to create it (depends on the IDE). The namespace may differ from your project. For me it is an internal class as only them should be used in the assembly.

1
2
3
4
5
6
namespace T1P2
{
    internal class CodeCompiler
    {
    }
}

After this, you create the properties for your compiler and the parameters

1
2
3
4
5
6
7
8
namespace T1P2
{
    internal class CodeCompiler
    {
        protected CSharpCodeProvider CSharpCodeProvider { get; set; }
        protected CompilerParameters CompilerParameters { get; set; }
    }
}

The next step is the constructor to initialise our properties

15
16
17
18
19
20
21
22
23
24
public CodeCompiler()
{
    CSharpCodeProvider = new CSharpCodeProvider();
    CompilerParameters = new CompilerParameters
    {
        GenerateInMemory = false,
        GenerateExecutable = false,
        OutputAssembly = "Assembly.dll"
    };
}

You can see that I changed the parameters for your compiler since the last tutorial. Now you compile the code to a file (Assembly.dll). If you want to create an executable file change the value to “true”. Please note that you need a static “Main” function like in every C# project for this.
After this, I created some functions to control our compiler. This is your public interface. Please adjust this in your design of the compiler.

26
27
28
29
30
31
32
33
34
public void AddReferencedAssembly(string name)
{
    CompilerParameters.ReferencedAssemblies.Add(name);
}
 
public void RemoveReferencedAssembly(string name)
{
    CompilerParameters.ReferencedAssemblies.Remove(name);
}

Now we are ready to write the functionality. This is the same as in the other tutorial, but now as a function in our object.

36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public Assembly CompileSources(params string[] sources)
{
    Assembly returnAssembly = null;
 
    try
    {
        CompilerResults results = CSharpCodeProvider.CompileAssemblyFromSource(CompilerParameters, sources);
        returnAssembly = results.CompiledAssembly;
 
        foreach (string output in results.Output)
        {
            Console.WriteLine(output);
        }
    }
    catch (Exception exception)
    {
        Console.WriteLine(exception.Message);
    }
 
    return returnAssembly;
}

I also added the possibility to read source code from external files.

58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
public Assembly CompileFiles(params string[] files)
{
    //function in LINQ
    //return CompileSources(files.Select(t => ReadFile(t)).Where(source => source != "").ToArray());
 
    List sources = new List();
    for (int index = 0; index < files.Length; index++)
    {
        string source = ReadFile(files[index]);
        if (source != "")
        {
            sources.Add(source);
        }
    }
 
    return CompileSources(sources.ToArray());
}
 
protected string ReadFile(string file)
{
    string source = "";
    if (File.Exists(file))
    {
        source = File.ReadAllText(file);
    }
 
    return source;
}

Now there are only a few steps left to our goal. To test our compiler I created two files “Dwarf.cs” and “Robot.cs”. If you want, add these classes to your IDE but make sure that you only copy the files to the output directory and don’t compile them with the rest of your code.

1
2
3
4
5
6
7
8
9
using System;
 
public class Dwarf
{
    public void Speak()
    {
        Console.WriteLine("Ohr bi issakaz!");
    }
}
1
2
3
4
5
6
7
8
9
using System;
 
public class Robot
{
    public void Speak()
    {
        Console.WriteLine("Hello, Sir!");
    }
}

The last step is to modify our “Main” function of the project to run our compiler.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static void Main(string[] args)
{
    CodeCompiler codeCompiler = new CodeCompiler();
    codeCompiler.AddReferencedAssembly("System.dll");
 
    //you can also use command line parameters to add files
    //Assembly assembly = codeCompiler.CompileFiles(args);
    Assembly assembly = codeCompiler.CompileFiles(@"Files" + Path.DirectorySeparatorChar + "Dwarf.cs",
                                                  @"Files" + Path.DirectorySeparatorChar + "Robot.cs");
 
    dynamic robot = assembly.CreateInstance("Robot");
    robot.Speak();
    dynamic dwarf = assembly.CreateInstance("Dwarf");
    dwarf.Speak();
 
    Console.ReadKey();
}

Compile and run!

Our program should now compile our two test files and you should see the different outputs.
Now for some theory: this is a very simple example for how you can use external files and convert them into a DLL or EXE. Especially the object orientation needs some revision. For example, if you want to use VB as well, you should write a new class for that purpose. Since most of the logic can be reused, you could create an abstract class for this. Last but not least, you can add the possibility to configure everything via command line. If you are interested in that, check out “NDesk Options“.
Now you have the possibility to use external files, which can be used to slightly adjust your logic without having to create a completely new project.
One of our main applications was to create a script language for a game. We get closer to this goal with big steps.

In the next tutorial I will show you how you can optimize everything with interfaces.

I’m looking forward to your feedback and hope you have fun trying it out.

Yours, The Cup aka Christoph Becher

Leave a Reply