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

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.