Compile C# at runtime in Unity3D
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
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:
- Download official mono release
- Delete everything that is not needed for mcs, download externals that are
needed by
mcs
. - Find a way to run jay (the parser generator), mostly from looking at the code of it and or the Makefiles
- Jay parser generator was compiled and then ran using the
mcs/jay/#_GENERATE_PARSER_FROM_cs-parser.jay.bat
- Once jay is used, the
cs-parser.jay
is transformed into parser file calledcs-parser.cs
cs-parser.cs
is the core of themcs
.- In order to compile the
mcs
for dynamic runtime compilation you need to adjust the compilation symbols:- Remove
STATIC
- Add
BOOTSTRAP_BASIC
- Change
NET_X_X
toNET_2_1
(we need older .NET because we want to use thismcs
inside Unity3D)
- Remove
- Change all internal classes to public, they can be used in modified driver
(the main class of
mcs
). - Compile
mcs.dll
with .NET subset for Unity provided by Microsoft Visual Tools for Unity. - 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.