C++ Scripting: Part 1 – C#/C++ Communication
For all of the nice things about C#, writing code with it also comes with a lot of downsides. We spend so much time working around the garbage collector, working around IL2CPP, and worrying about what’ll happen if we use a foreach loop. Today’s article starts a series that explores what would happen if we escaped .NET and wrote our code as a C++ plugin instead of using C#.
Table of Contents
- Part 1: C#/C++ Communication
- Part 2: Update C++ Without Restarting the Editor
- Part 3: Object-Oriented Bindings
- Part 4: Performance Validation
- Part 5: Bindings Code Generator
- Part 6: Building the C++ Plugin
- Part 7: MonoBehaviour Messages
- Part 8: Platform-Dependent Compilation
- Part 9: Out and Ref Parameters
- Part 10: Full Generics Support
- Part 11: Collaborators, Structs, and Enums
- Part 12: Exceptions
- Part 13: Operator Overloading, Indexers, and Type Conversion
- Part 14: Arrays
- Part 15: Delegates
- Part 16: Events
- Part 17: Boxing and Unboxing
- Part 18: Array Index Operator
- Part 19: Implement C# Interfaces with C++ Classes
- Part 20: Performance Improvements
- Part 21: Implement C# Properties and Indexers in C++
- Part 22: Full Base Type Support
- Part 23: Base Type APIs
- Part 24: Default Parameters
- Part 25: Full Type Hierarchy
- Part 26: Hot Reloading
- Part 27: Foreach Loops
- Part 28: Value Types Overhaul
- Part 29: Factory Functions and New MonoBehaviours
- Part 30: Overloaded Types and Decimal
The general idea here is to only write as much C# as necessary to wrap the Unity API for a C++ plugin. The actual game code will all be written in C++. There are, of course, several pros and cons to this. Here are some of the more major ones:
C++ Pros
- No garbage collector to cause frame hitches and memory fragmentation
- No need for object pooling, delegate caching, and other GC workarounds
- Easy access to SIMD via intrinsics and inline assembly
- C++ templates are much more advanced than C# generics
- Deterministic destructors, not C# finalizers
- Robust macro system, not simple
#if
- No auto-generated IL2CPP code to work around
C++ Cons
- C++ call stack is not visible to the Unity profiler
- Crashes in C++ crash the editor
- Requires C# glue code
- Some language features (e.g. LINQ) aren’t in C++
- Integration with the Inspector pane requires more code
- Setup required to create per-platform plugins
- Not as “normal” as using C# in the Unity world
Neither choice is clearly better than the other. As always, you’ll have to take your own project, budget, timelines, and team into account when deciding which way you go. This series will assume you’ve chosen the C++ route and show you one way that you could implement support for it.
For this introductory article, we’ll keep things simple. First, you’ll need to create a native plugin for the platforms you’re interested in supporting. That should include the Unity Editor on whatever OS you develop on. Unity’s documentation is a good place to start. Here are some of the platforms:
Next, we’ll make some glue code so that C# can call C++ and visa versa. C# can call C++ very easily via the “P/Invoke” system. Here’s how it looks:
using System.Runtime.InteropServices; // Add the [DllImport] attribute to indicate which plugin has the function in C++ // Use the extern keyword since we're not providing the definition in C# // Use the same name, parameters, and return value as in C++ [DllImport("MyPluginName")] static extern int CppFunction(int a, float b); // Now let's call it from C# static void Foo() { // It's just a function call like normal! int retVal = CppFunction(2, 3.14f); }
Allowing C++ to call C# is a little trickier. We need C# to pass function pointers to C++ so that it has something to call. Fortunately, .NET provides Marshal.GetFunctionPointerForDelegate
so we can write this:
using System; using System.Runtime.InteropServices; // C# function to expose to C++ static int CsharpFunction(int a, float b) { // ... do something useful } // C++ function to initialize the plugin // Parameters are the function pointers to C# functions [DllImport("MyPluginName")] static extern void Init(IntPtr csharpFunctionPtr); // Call this to initialize the C++ plugin static int InitPlugin() { // Make a delegate out of the C# function to expose Func<int, float, int> del = new Func<int, float, int>(CsharpFunction); // Get a function pointer for the delegate IntPtr funcPtr = Marshal.GetFunctionPointerForDelegate(del); // Call C++ and pass the function pointer so it can initialize Init(funcPtr); }
On the C++ side, we fill in the Init
function and write C++ functions that call C# functions:
// C++ compilers "mangle" the names of functions by default // This prevents that from happening // If we didn't do this, C# wouldn't be able to find our functions extern "C" { // Function pointer to the C# function // The syntax is like this: ReturnType (*VariableName)(ParamType ParamName, ...) int(*CsharpFunction)(int a, float b); // C++ function that C# calls // Takes the function pointer for the C# function that C++ can call void Init(int(*csharpFunctionPtr)(int, float)) { CsharpFunction = csharpFunctionPtr; } // Example function that calls into C# void Foo() { // It's just a function call like normal! int retVal = CsharpFunction(2, 3.14f); } }
Now that we’ve established two-way communication between C# and C++, we just need to expose something useful instead of dummy functions. This is normally tricky because we can only pass primitive types between the two languages. We can’t pass a managed type like GameObject
as a parameter. Thankfully, object handles make it really easy to represent an object
as an int
. Here’s a little recap from that article:
// Make a managed object MyClass obj = new MyClass(); // Store the managed object and get an int representing it int handle = ObjectStore.Store(obj); // Get the stored object by its handle MyClass objFromStore = (MyClass)ObjectStore.Get(handle); // Remove a stored object ObjectStore.Remove(handle);
This allows us to use an int
as a function parameter when calling from either C# to C++ or C++ to C#. The C# side simply uses an ObjectStore
to get the real object for the int
. With that in mind, take a look at a very simple script that exposes some Unity API functions to create a GameObject
, get its transform
property, and set the position
property of that Transform
:
using System; using System.Runtime.InteropServices; using UnityEngine; class TestScript : MonoBehaviour { void Awake() { ObjectStore.Init(1024); Init( Marshal.GetFunctionPointerForDelegate( new Func<int>(GameObjectNew)), Marshal.GetFunctionPointerForDelegate( new Func<int, int>(GameObjectGetTransform)), Marshal.GetFunctionPointerForDelegate( new Action<int, Vector3>(TransformSetPosition))); } void Update() { MonoBehaviourUpdate(); } //////////////////////////////////////////////////////////////// // C++ functions for C# to call //////////////////////////////////////////////////////////////// [DllImport("NativeScript")] static extern void Init( IntPtr gameObjectNew, IntPtr gameObjectGetTransform, IntPtr transformSetPosition); [DllImport("NativeScript")] static extern void MonoBehaviourUpdate(); //////////////////////////////////////////////////////////////// // C# functions for C++ to call //////////////////////////////////////////////////////////////// static int GameObjectNew() { GameObject go = new GameObject(); return ObjectStore.Store(go); } static int GameObjectGetTransform(int thisHandle) { GameObject thiz = (GameObject)ObjectStore.Get(thisHandle); Transform transform = thiz.transform; return ObjectStore.Store(transform); } static void TransformSetPosition(int thisHandle, Vector3 position) { Transform thiz = (Transform)ObjectStore.Get(thisHandle); thiz.position = position; } }
In addition to exposing those three functions to C++, C# is also calling into C++ on every Update
to give it a chance to do some work.
Now let’s see how the C++ side looks:
extern "C" { //////////////////////////////////////////////////////////////// // C# struct types //////////////////////////////////////////////////////////////// struct Vector3 { float x; float y; float z; }; //////////////////////////////////////////////////////////////// // C# functions for C++ to call //////////////////////////////////////////////////////////////// int (*GameObjectNew)(); int (*GameObjectGetTransform)(int thisHandle); void (*TransformSetPosition)(int thisHandle, Vector3 position); //////////////////////////////////////////////////////////////// // C++ functions for C# to call //////////////////////////////////////////////////////////////// int numCreated; void Init( int (*gameObjectNew)(), int (*gameObjectGetTransform)(int), void (*transformSetPosition)(int, Vector3)) { GameObjectNew = gameObjectNew; GameObjectGetTransform = gameObjectGetTransform; TransformSetPosition = transformSetPosition; numCreated = 0; } void MonoBehaviourUpdate(int thisHandle) { if (numCreated < 10) { int goHandle = GameObjectNew(); int transformHandle = GameObjectGetTransform(goHandle); float comp = (float)numCreated; Vector3 position = { comp, comp, comp }; TransformSetPosition(transformHandle, position); numCreated++; } } }
The C++ side creates a new GameObject
, gets its transform
, and sets its position
every frame until it’s created 10 of them. Since the C++ side doesn’t have visibility into the structs defined on the C# side, like Vector3
, it’s necessary to define them in C++.
And that’s all there is to getting a basic C++ scripting environment up and running. In total, this was just about 100 lines of combined C# and C++ and most of it is glue code. However, as it stands this isn’t a very good way to program. Perhaps the biggest issue is that the Unity editor will only load a plugin one time so you need to restart the editor every time you recompile the plugin. We’ll address that in the next article of the series.
#1 by benjamin guihaire on July 11th, 2017 ·
the C# functions in the Unity API are often just a glue to call a c++ functions,
for example, MonoBehaviour.StartCoroutine calls a c++ function.
So in your c++ code, you could in theory avoid the c++ => c# => c++ calls by calling directly the final c++ function
(in my exmampler MonoBehaviour_CUSTOM_StartCoroutine_Auto_Internal)
It might be worth creating c++ helper function to call the c++ Unity API directly and easily, (we can get the c
++ names by looking at the il2cpp generated code once) , and do that for all the common api for example (transform, gameobject, monobehaviour..).
#2 by jackson on July 11th, 2017 ·
That would be much easier and faster than using C#, but also much less supported by Unity. Since those functions are internal to the engine they can change with any release. Still, it’s probably worth looking into for some projects because the upside is so strong. Thanks for the tip!
#3 by benjamin guihaire on July 17th, 2017 ·
Good point about maintaining the special c++ helpers to call directly the Unity c++ functions.
It looks like Unity use a Naming convention, so unless they change their naming convention, it should be pretty resistant to new Unity versions… Best would be to auto generate the direct c++ call helper by looking at the IL code (using cecil for example to extract IL informations) of UnityEngine.dll C# dll, to find what C++ code is being called…
#4 by Alex on July 11th, 2017 ·
Hi, thanks for the article, very informative. But so far we have a theoretical intro on how to marry C++ and C#, without any clue on how to compile C++ properly, where to put it and so on. Will it be described in the next section?
#5 by jackson on July 11th, 2017 ·
I hadn’t planned on going into the nuts and bolts of how to create the C++ plugin itself, besides the links to Unity’s documentation on how to do so that I included in this article. That said, the documentation I ran across was lacking in detail, so perhaps an expanded guide on how to create plugins would be useful.
#6 by Alex on July 12th, 2017 ·
Yes, would be nice to have it. Respect!
#7 by Ed Earl on July 13th, 2017 ·
Interestingly, marshaling the C# function pointer in that way fails in a standalone .NET program at runtime. The call to GetFunctionPointerForDelegate throws an ArgumentException with the message “The specified Type must not be a generic type definition”. It works without complaint in Unity, though, so perhaps this is an interesting implementation difference between .NET and Mono? I wonder what the C# standard has to say about it?
#8 by jackson on July 13th, 2017 ·
If you get this error, the workaround should be very easy. Simply define a delegate and use that instead of
Action
orFunc
:#9 by Ed Earl on July 14th, 2017 ·
Ah yes, you’re right! Thanks!
#10 by Thomas Viktil on September 3rd, 2017 ·
Why isn’t object pooling needed in C++?
#11 by jackson on September 5th, 2017 ·
The purpose of object pooling in C# is to avoid ever releasing the last reference to an object. That prevents the object from ever being garbage-collected. Garbage collection in Unity is often the cause of frame hitches and memory fragmentation, so it’s good to avoid it.
C++ doesn’t have a garbage collector, so there’s no garbage collector to avoid by using object pooling.
#12 by Thomas Viktil on September 7th, 2017 ·
I understand that, but how can you reuse objects if no one knows about it? Or don’t you reuse object instances?
#13 by jackson on September 7th, 2017 ·
C# objects can still be pooled when using C++. For example, here’s a simple pool of
GameObject
instances:The
GameObjectPool
prevents creatingGameObject
instances over and over by keeping a list of them that are ready to be reused.#14 by Thomas Viktil on September 13th, 2017 ·
Sorry for being so stupid, but this made me more confused. If I haven’t completely misunderstood you, the code you posted is simply an example of how you could have made an object pool in C++. Due to the lack of a garbage collector in C++, you don’t use object pools. At least, that’s what you have stated in the beginning of this article, as an argument for using C++.
Let’s say you were making a shmup and you spawn lots of bullets. They fly off screen and would normally get despawned and put back into a pool. Do you kill the object completely? Deallocate it from memory and create a new instance for the next bullet? If you don’t, how do you keep track of all the bullets if not with an object pool?
#15 by jackson on September 13th, 2017 ·
No problem at all. I’m happy to explain the differences.
The bullet scenario you mention is a good example. Let’s say a bullet is a
GameObject
in Unity. You definitely want to be reusing a pool of theseGameObject
instances to avoid the expensive work of creating a new one every time there’s a shot and destroying it when it either hits or goes off screen. So normally in C# you’d have an object pool like this:Now with C++ scripting you don’t want to write a C# pool class like that, so you write one like in my previous comment. This allows you to reuse the
GameObject
instances and avoid the expensive work to create and destroy them every time.Starting in part three of this series, the differences become even smaller because a
GameObject
type is available on the C++ side. Here’s what that pool would look like:It’s almost identical to C# both conceptually and syntactically. Starting in part ten you could even keep the
GameObject
instances in aQueue<T>
and the pool would look like this:Basically you keep using the same strategies like pooling
GameObject
in C++, but you implement it in another language. It just happens that I’ve made C++ look almost like C# so it looks like nothing much has changed.The really huge advantage to C++ scripting is that you don’t need to pool objects that should be “cheap”.
GameObject
instances are very heavy weight so it makes sense to pool them. When you’re trying to avoid the GC in C#, as you should to avoid frame hitches and memory fragmentation, you end up using object pools for almost everything. For example, say you have aBullet
class that controls a bulletGameObject
:If you’re spawning lots of bullets then you don’t want to
new Bullet(go)
every time you shoot one andmyBullet = null
every time you destroy one. If you did, you’d create a lot of garbage for the GC to collect. Instead, you need to pool theBullet
instances:This leads to lots of problems. If you’d used C++, you wouldn’t need to worry about GC for
Bullet
instances becauseBullet
would be a C++ class/struct that isn’t managed by the garbage collector. Only theGameObject
it uses would be managed by the garbage collector and Unity.Notice that
Bullet
can have aGameObject
without actually being a managed/.NET class. So using it has no impact on the GC:In summary, C++ scripting allows you to keep using object pools for expensive Unity resources like
GameObject
with very little syntactical change to the code you write and allows you to stop writing object pools altogether for cheap resources likeBullet
since the GC is no longer involved at all.Hopefully that clears things up. Please let me know if anything’s still unclear.
#16 by Rory on April 19th, 2018 ·
Great article! When I tried implementing the code I was only able to get it to work by prefacing each C++ function exported to C# with “__declspec(dllexport)” like this:
If I didn’t do this, I would get an EntryPointNotFoundException when attempting the call the [DllImport] function from the C# side. Any idea why this might be? Was using MSVC/VS 2017 with Unity 2017.3 just in the editor.
#17 by jackson on April 20th, 2018 ·
I’m glad you enjoyed the article! You’re absolutely right about adding
__declspec(dllexport)
on Windows. It looks like I missed that in the first article of this series, but started adding it in the second article. I probably didn’t notice this because I wrote the code for the article on macOS, but shortly thereafter added support for Windows, iOS, and Android. If you haven’t seen the GitHub project, it’s quite a bit more advanced than this and might be of interest to you. Thanks for pointing out this issue!#18 by Rory on April 22nd, 2018 ·
Thanks, yeah I’ll check that out too. Wanted to read through the articles to make sure I understand everything that’s going on in the project though. We’re planning on using this for a project that will run on both Android and Windows so will probably be setting our project up with CMake like your example on GitHub too.
#19 by Can Akpinar on May 4th, 2018 ·
Thanks mate! Great tutorial
#20 by Stas on May 16th, 2018 ·
Great to see that mobile platforms are supported from the start. Excited to see where this will go
#21 by nxrighthere on May 9th, 2019 ·
I recently started a new thread on Unity forums regarding this topic, so I might it interesting for some people here: https://forum.unity.com/threads/partial-integration-with-the-engine-and-api-that-powers-dots-using-c-c.674905/
#22 by RiderV on June 12th, 2019 ·
// Make a managed object
MyClass obj = new MyClass();
// Store the managed object and get an int representing it
int handle = ObjectStore.Store(str);
///// May be obj instead str?
#23 by jackson on June 12th, 2019 ·
Thanks for pointing this out. I’ve updated the article to fix the typo.
#24 by Ultran3D on June 16th, 2020 ·
So you compile c++ to C# which calls c++ again…
and you call this fast?
unity C# is built as a bridge on unity C++ internal functions using mono , who said c++ compile is faster I get at least 5 seconds for a empty project using VC++ when C# takes 0.5s but yes unity C# sucks because of low-speed of Mono not C#…
#25 by jackson on June 16th, 2020 ·
This approach doesn’t compile C++ to C#. It compiles C++ to machine code and provides interoperability between that machine code/C++ and C#/.NET.
C# in Unity is compiled to machine code by either IL2CPP or Burst these days. For more detail on that, check out the IL2CPP tag. There are 22 articles, but it sounds like you might be most interested in those on interoperability, so check out How IL2CPP Calls Burst and How to See What C# Turns Into.
Regarding compile times, it sounds like something is wrong with your environment. An empty project really shouldn’t take 5 seconds to compile. Regardless, see this repo for a comparison I did of compile times between the two languages. It’s a bit dated at this point, but then again so is this article.
#26 by Jeremy Fisher on April 23rd, 2021 ·
Will this work with unity 2020 LTS?
#27 by jackson on April 24th, 2021 ·
I haven’t tried yet, but I’d assume so. It’s worked for several years of releases and I haven’t seen anything that would break it so far. Let me know if you give it a try.
#28 by kice on November 7th, 2021 ·
Since C++ 20 already much ready for most compilers, and I think it would be interesting to investigate c++ co-routine with unity.
#29 by Ayyappa on April 18th, 2023 ·
Any specific reason for the method that is being called needs to be static always?
#30 by jackson on April 22nd, 2023 ·
Yes, non-
static
functions require a hiddenthis
argument which C++ can’t provide because it doesn’t have access to managed objects.