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:
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()
{
.Log(""Hello, World!"");
Debug}
}");
var method = assembly.GetType("Test").GetMethod("Foo");
var del = (Action)Delegate.CreateDelegate(typeof(Action), method);
.Invoke();
del}
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()) {
.ReferencedAssemblies.Add(assembly.Location);
param}
// Add specific assembly references
//param.ReferencedAssemblies.Add("System.dll");
//param.ReferencedAssemblies.Add("CSharp.dll");
//param.ReferencedAssemblies.Add("UnityEngines.dll");
// Generate a dll in memory
.GenerateExecutable = false;
param.GenerateInMemory = true;
param
// Compile the source
var result = provider.CompileAssemblyFromSource(param, source);
if (result.Errors.Count > 0) {
var msg = new StringBuilder();
foreach (CompilerError error in result.Errors) {
.AppendFormat("Error ({0}): {1}\n",
msg.ErrorNumber, error.ErrorText);
error}
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.