Welcome to the first in a series of blog posts detailing how to integrate .NET 6.0 C# scripting support into a C++ Windows game engine. Most of what I am going to detail is based on my research and work in developing a C# scripting system for my DigiPen game projects and is most likely not perfect, but I can guarantee the solution works.
While the posts are targeted towards implementation in a game engine, the same techniques can be used for non-game applications for purposes of building a plugin system and more.
How C# Works
For most DigiPen students who are building a C# scripting system on the request of their designers, you might not have learnt C# before. So, I’m going to do a quick summary of the key components in running C# code compared to C++ which most of you are already familiar with.
The Common Language Runtime (CLR)
C# allows us to build cross platform applications and unlike C++, which is compiled into native machine code, C# compiles into an Assembly-like Intermediate Language (IL) which is compiled to machine code Just-In-Time and run on what is known as the Common Language Runtime.
Unlike C++ which requires a specific compiler to compile for each new platform, in theory, C# code compiled into IL can be run on any platform that has a CLR already written for it.
Another feature of the CLR is that we can embed it into our C++ application, allowing C# code to easily work with our C++ code.
Managed Code & Garbage Collection
While we refer to C++ code as native or unmanaged code, we refer to C# as managed code. This is because in C#, we do not explicitly deallocate memory via calls to delete or free(). In C#, the job of deallocating unused memory is tasked by the garbage collector of the CLR. It uses reference counting to keep track of memory usage and performs deallocation (known as garbage collection) at opportune times.
Before we begin, I’d like to quickly go through the common choices of integrating C# scripting into a native C++ application.
This is by far the most popular solution using Xamarin’s cross platform implementation of the Common Language Runtime (CLR). In fact, Unity programmers might realise that this is how Unity implements C# scripting when not using the IL2CPP scripting backend. Outside of game development, Mono is also used by Xamarin’s cross platform application suite of the same name.
While Mono is certainly powerful, it does have a few issues.
First, it is not as comprehensive as .NET having only support to features implemented up to .NET Framework 4.7 and only supports up to C# 6.0 and a subset of C# 7.0. Realistically, this does not matter too much significantly since C# 6.0 is a pretty mature language and you won’t find yourself limited by it even if it is significantly older.
Secondly, Mono requires a lot more boilerplate to get working but that is also it’s strength. Function calls from C# to C++ code require manual registration for each function in Mono but this also allows you to query your C# code from your C++ code which could be useful.
One other thing to note is that since Microsoft now owns Xamarin, Mono’s CLR (MonoVM) has been incorporated into .NET 6.0 and used for WebAssembly targets. Existing non-web targets still utilise .NET’s original CLR (CoreCLR).
.NET is Microsoft’s implementation of the CLR and has a long and somewhat convoluted history. .NET is largely used in Windows desktop application and web server development. Unity had also used it as their scripting backend for Universal Windows Platform Applications before deprecating it in Unity 2019 in favour of IL2CPP.
One could go on and on about how things have changed but for our purposes, all we need to know is that the latest version of .NET is .NET 6.0 and that gives us access to C# 10.
Function calls from C# to C++ code can also be easily implemented via Platform Invoke (P/Invoke) interoperability features.
This is the most performant option but also the most difficult to achieve. IL2CPP is Unity’s IL to C++ compiler, and you can imagine the difficulties in building such a system. We will not be looking into this method because of the inherent complexity and limitations of such a system.
For our implementation, we will be using .NET because it is newer and supports the latest version of C#. The method to embed .NET into our application, is known as hosting and there are a few possible methods to choose from.
The CoreCLRHost APIs allow us to host the .NET CLR and provides methods for accessing static functions within a managed DLL compiled from C# very easily. However, being an older API, it requires us to fill in more boilerplate.
The HostFxr APIs are newer APIs that allow us to host the .NET CLR and provides methods for accessing static functions within a managed DLL compiled from C# with some caveats. Function parameters must be packed into a struct that are then passed to the managed functions, but the API is able to detect the available SDKs and load them without additional boilerplate.
We have chosen CoreCLRHost as it provides the best convenience in C++ and C# interoperability. Being able to call any C# function from C++ without having to worry about packing parameters and more simplifies a lot of work. The additional boilerplate is just a small initial price to pay for easier usage after the scripting system is set up.
Now that we have a relatively good grasp on the intricacies on what we need to build a C# scripting engine, we’ll start getting our hands dirty in the next part.