Compile C# at runtime in Unity3D

A bear dual wielding flaming chainsaws

Something you might not realize is that it is actually possible to compile and run C# code at runtime (not just in the editor!) with Unity3D. This may seem like the equivalent of equipping bears with flaming chainsaws, but it has several practical advantages.

The main benefits are as follows:

  • Little to no performance impact — After the C# script is compiled, you can execute it repeatedly with very little overhead, as if you had originally written that script as part of the game. Unlike a scripting language, no interpretation is required at runtime. You just execute the code.
  • Direct access to the API — You don’t have to write binding methods.
  • Familiarity — You don’t have to learn the syntax of a new language. This is good if you have a great fear of mysterious languages.

If you are convinced running C# code at runtime with Unity3D is something you want to do, great! Unfortunately, getting this to work is difficult because you’ll probably run into a mysterious error involving something called mcs that I had a very hard time finding a solution for. Additionally, the target platform must support dynamic compilation. This means you can’t compile code at runtime on AOT platforms such as iOS.

In this article I’ll explain the code you will need to compile and execute at runtime and how to actually make it work. While this guide is by no means comprehensive, by the end you should have enough information to find your way around.

The Code

First, make sure that your Unity3D project is set to use the .Net 2.0 Api Compatibility Level. Otherwise, some namespaces that we need will be missing and Unity won’t know what you are talking about.

There is an example in the official .NET API of how to use the compiler, but I’ve included a different example below:

using Microsoft.CSharp;
using System;
using System.CodeDom.Compiler;
using System.Reflection;
using System.Text;
using UnityEngine;
 
public class CompilerExample : MonoBehaviour
{
  void Start()
  {
    var assembly = Compile(@"
      using UnityEngine;
 
      public class Test
      {
      public static void Foo()
      {
      Debug.Log(""Hello, World!"");
      }
    }");
 
    var method = assembly.GetType("Test").GetMethod("Foo");
    var del = (Action)Delegate.CreateDelegate(typeof(Action), method);
    del.Invoke();
  }
 
  public static Assembly Compile(string source)
  {
    var provider = new CSharpCodeProvider();
    var param = new CompilerParameters();
 
    // Add ALL of the assembly references
    foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) {
      param.ReferencedAssemblies.Add(assembly.Location);
    }
 
    // Add specific assembly references
    //param.ReferencedAssemblies.Add("System.dll");
    //param.ReferencedAssemblies.Add("CSharp.dll");
    //param.ReferencedAssemblies.Add("UnityEngines.dll");
 
    // Generate a dll in memory
    param.GenerateExecutable = false;
    param.GenerateInMemory = true;
 
    // Compile the source
    var result = provider.CompileAssemblyFromSource(param, source);
 
    if (result.Errors.Count > 0) {
      var msg = new StringBuilder();
      foreach (CompilerError error in result.Errors) {
        msg.AppendFormat("Error ({0}): {1}\n",
        error.ErrorNumber, error.ErrorText);
      }
      throw new Exception(msg.ToString());
    }
 
    // Return the assembly
    return result.CompiledAssembly;
  }
}

First, we get our source code. In this case I’m just using a hard coded string, but you could also load it from a text asset, from a file on disk, etc. Then, we create a CSharpCodeProvider, which is the object that actually does the compilation for us.

We then instantiate CompilerParameters, which we will use to configure the CSharpCodeProvider. We can attach any assemblies we want the code to have access to. In this case, we add the System and UnityEngine DLLs. The CSharp.dll (or Assembly-CSharp.dll on MacOSX) file is the DLL where our Unity3D non-editor project code is compiled to. If you have multiple projects in your solution, which can happen if you have editor code for example, then you may have other project DLLs you would want to add.

You could generate the code into a DLL or an executable on disk, but in this case we just want to store the generated code in memory. We can change CompilerParameters later if we change our minds.

Then, we compile the code, check for errors, and return the generated Assembly.

With the compiled assembly, we can use reflection to find the method we want to execute, convert it to a delegate, and call it. Converting the method info to a delegate helps reduce the overhead from reflection and makes it nicer to call in the source code. If the function took an argument, we would use Func<T, TResult> instead of Action<T>.

How To Make it Work

The thing is, the example I provided won’t always work. In MacOSX you may get a FileNotFound exception for a file called mcs. If you don’t have that problem, this code will work in the Editor, but not in builds.

mcs is the Mono CSharp Compiler. CSharpCodeProvider depends on this executable to work but relies heavily on strange path magic to find it. It basically looks for your Mono SDK (and hence, mcs), but users that you distribute your game to are unlikely to actually have the SDK installed on their system. As a result, it typically fails.

A quick solution can be found by using Aeroson’s mcs-ICodeCompiler project on GitHub. Aeroson compiled mcs to a dll which you can simply add to the plugins folder inside of your Unity project (if you are not familiar with special folder names in Unity, please see this API documentation). They then implemented a different CSharpCodeProvider that uses the new mcs dll. You will only need a few minor adjustments to the example I provided to use the mcs dll, and the examples included in Aeroson’s repository should be sufficient to figure it out. What this means is we can use mcs at runtime, even when we build the project. Neat.

While Aeroson did explain the steps they took to compile mcs to a dll we can load at runtime, it’s honestly some sort of treacherous black magic to me. At some point I will have to sit down and do it myself to understand completely. I may make another blog post when I do so.

In case the GitHub repository ever goes down, I have copied verbatim the instructions they provided, in case you are cooler than me and the world needs saving:

  1. Download official mono release
  2. Delete everything that is not needed for mcs, download externals that are needed by mcs.
  3. Find a way to run jay (the parser generator), mostly from looking at the code of it and or the Makefiles
  4. Jay parser generator was compiled and then ran using the mcs/jay/#_GENERATE_PARSER_FROM_cs-parser.jay.bat
  5. Once jay is used, the cs-parser.jay is transformed into parser file called cs-parser.cs
  6. cs-parser.cs is the core of the mcs.
  7. In order to compile the mcs for dynamic runtime compilation you need to adjust the compilation symbols:
    1. Remove STATIC
    2. Add BOOTSTRAP_BASIC
    3. Change NET_X_X to NET_2_1 (we need older .NET because we want to use this mcs inside Unity3D)
  8. Change all internal classes to public, they can be used in modified driver (the main class of mcs).
  9. Compile mcs.dll with .NET subset for Unity provided by Microsoft Visual Tools for Unity.
  10. The modified driver is then used to implement ICodeCompiler interface.

Conclusion

Hopefully this article is a sufficient explanation for how to set up C# code compilation at runtime. This article was written for Unity3D version 5.x, so please take that into account if you live in the exciting new future.

Be sure to explore different ways of compiling C# scripts. For example, the CSharpCodeProvider includes another method that can compile a C# script from a file path.

You can load assemblies that are currently loaded by using System.AppDomain.CurrentDomain.GetAssemblies() and then adding them as references in CompilerParameters. You can also add specific assembly references if you want to sandbox the execution. The example adds all of the assemblies that are currently loaded for simplicity and also because the CLR behaves differently on different platforms when searching for a dll.

There are a lot of different things you can do with C# compilation at runtime. I hope you enjoy applying this technique to solve interesting problems.

Thanks to @shialatier for the image.