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:
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.