Since their introduction in part 7, support for C++ MonoBehaviour messages has always been a special case. The reason for this was that we didn’t have good enough support for what I’m calling “factory functions.” These are functions like GameObject.AddComponent<T> that instantiate a generic type. This week we’ll go over why that support was lacking, what was done to fix it, and how the new system works.

Table of Contents

Let’s talk about why support for “factory functions” has been weak so far. Say we had a C# function like this:

class Example
{
	Thing Create()
	{
		return new Thing();
	}
}

That’s no problem for the C++ scripting system. We’d call the function from C++:

void Foo(Example ex)
{
	Thing thing = ex.Create();
}

The C++ bindings for Create would call the C# bindings for Create. Next, Create would be called and the returned object would be put into ObjectStore to generate a handle. The handle (an int) would be returned to the C++ bindings for Create which would then create a Thing with the handle. Everything works just great so far!

Now let’s change up Create just a little bit:

class Example
{
	T Create<T>()
		where T : new() // T must have a default constructor
	{
		return new T();
	}
}

Now say we want to call Create with T being a class we derived in C++:

// C++ class deriving from a C# class
struct MyThing : Thing
{
	// Override a virtual or abstract method
	void Speak() override
	{
		String msg = "C++ MyThing";
		Debug::Log(msg);
	}
};
 
void Foo(Example ex)
{
	MyThing thing = ex.Create<MyThing>();
	thing.Speak();
}

This doesn’t work for several reasons. First, C# doesn’t know about MyThing since it’s a C++ class. There’s no way for the C# bindings for Create to call Example.Create<MyThing> and we’ll get a compiler error if we try. Second, there is a bindings class for Thing that’s generated so that it can call into C++ when Speak is called. It looks like this:

// C# bindings class
class BaseThing : Thing
{
	// Handle to the C++ object to call functions on
	int CppHandle;
 
	public override void Speak()
	{
		// Call the C++ bindings function
		ThingSpeak(CppHandle);
	}
}

If we try to adjust the C# bindings for Create so that it uses this class, we won’t get the desired behavior. Sure, the C# bindings class will get created by Create, but it won’t have a CppHandle because it wasn’t created by C++. So when Speak gets called on it, it’ll pass 0 into C++ and be unable to find the right Thing to call Speak on.

So how do we fix these issues? First, we need to come to terms with the fact that C# can create C++ objects too. Because of this, we need a place to store them. So we’ll need to create a free list in C++ to store whole objects that C# tells us to create. This is like the free list we used to generate CppHandle values for delegates and derived classes, except that it stores entire objects instead of just pointers to objects.

Now that we have a place to store C#-created objects, we can write a binding function for C# to call to create the object:

// C# calls this to create a Thing
DLLEXPORT int32_t NewBaseThing(int32_t handle)
{
	// Store a whole BaseThing in the free list
	BaseThing* memory = Plugin::StoreWholeBaseThing();
 
	// Use "placement new" to construct the MyThing at the memory we stored at
	MyThing* thiz = new (memory) MyThing(Plugin::InternalUse::Only, handle);
 
	// Return the CppHandle to C#
	return thiz->CppHandle;
}

This brings up another problem: the bindings layer doesn’t have any visibility into the game’s types. That means Bindings.cpp doesn’t know about MyThing (the game class), only BaseThing (the bindings class) and Thing (the class C++ wants to derive from). This is easily fixed by adding a new Game.h that the game code must provide to define any classes it wants to derive from C# types:

// Game.h
struct MyThing : BaseThing
{
	MY_THING_DEFAULT_CONSTRUCTOR
	void Speak() override;
};

The MY_THING_DEFAULT_CONSTRUCTOR here is a macro that’s generated for convenience purposes. NewBaseThing needs to call a constructor taking a Plugin::InternalUse and a int32_t handle when it creates the object. This doesn’t need to do anything except call the appropriate base class constructors. That’s unfortunately complex due to virtual inheritance, so the macro exists to make that easier.

There’s no need to put the implementation of Speak in the header file, just a declaration. The definition can be put in Game.cpp or any other implementation file:

// Game.cpp
void MyThing::Speak()
{
	String msg = "C++ MyThing";
	Debug::Log(msg);
}

There is an alternate form of the macro when we need to do work in the constructor:

// Game.h
struct MyThing : BaseThing
{
	MY_THING_DEFAULT_CONSTRUCTOR_DECLARATION
	void Speak() override;
};

This version just declares the constructor, but doesn’t define it. To define it, including all the virtual inheritance initializer list calls, use the corresponding macro:

// Game.cpp
MY_THING_DEFAULT_CONSTRUCTOR_DEFINITION
	, MyVar(3.14f) // game-specific initializer list calls
	, OtherVar(42)
{
	// ... game-specific constructor body
}

This brings up two more issues. First, the code generator doesn’t know about MyThing, so it doesn’t know what class to generate the new MyThing line for. Second, MyThing needs to know which C++ bindings class to derive from. So let’s add these to the JSON config file:

{
	"Name": "Thing",
	"BaseTypes": [
		{
			"BaseName": "MyGame.BaseThing",
			"DerivedName": "MyGame.Thing"
		}
	]
}

BaseName is the name of the bindings classes and DerivedName is the name of the C++ type deriving from it.

With all this in place, we can now add a default constructor to the C# bindings class:

// C# bindings class
class BaseThing : Thing
{
	// Handle to the C++ object to call functions on
	int CppHandle;
 
	// Default constructor called by "factory functions" like Create<T>()
	public BaseThing()
	{
		// Store this object and get a handle
		int handle = ObjectStore.Store(this);
 
		// Tell C++ to create the derived object
		// Get a handle to it in return
		CppHandle = NewBaseThing(handle);
	}
 
	public override void Speak()
	{
		// Call the C++ bindings function
		ThingSpeak(CppHandle);
	}
}

This takes care of the creation portion of the problem. We can now call C# functions like Create<T> and have our derived C++ class instantiated!

Next, let’s tackle the destruction portion of the problem. When the MyThing is destroyed, it needs to be removed from the free list of whole objects that it was created in. So let’s put a call in its base class’ destructor to do just that:

BaseThing::~BaseThing()
{
	Plugin::RemoveWholeBaseThing(this);
 
	// ... normal destructor code omitted
}

There’s another, thornier issue though that wasn’t possible when only C++ could create its derived types. What happens if C# garbage-collects the object because it’s no longer in use on the C# side? We need to also destroy the C++ object it created when this happens. To do so, we’ll need another C++ bindings function for C# to call when the object is garbage-collected:

DLLEXPORT void DestroyBaseThing(int32_t cppHandle)
{
	// Get it by its CppHandle
	BaseThing* instance = Plugin::GetBaseThing(cppHandle);
 
	// Call the destructor
	instance->~BaseThing();
}

Now we need to call this from the C# side when the object is garbage-collected. We’re informed about this by adding a “destructor” or “finalizer” to the C# class:

class BaseThing : Thing
{
	int CppHandle;
 
	~BaseThing()
	{
		// ... ?
	}
}

After the GC determines that the object is garbage and that it has a finalizer, the object is put into a list for the finalizer to call the finalizers on. This typically runs in another thread, which can be problematic. We don’t want to introduce multi-threading into the system as it would cause a lot of additional complexity and introduce limitations such as not being able to call the Unity API from a C++ derived class’ destructor. So we’ll need to move the destruction work back onto the main thread.

We’ll do this movement by means of a queue of “command” objects. Each “command” is just a struct that says what C++ destroy function to call and with which CppHandle. Here’s how it looks:

// Types of C++ destroy functions to call
public enum DestroyFunction
{
	BaseThing,
	// ... other types
}
 
// The destroy "command"
struct DestroyEntry
{
	// C++ destroy function to call
	public DestroyFunction Function;
 
	// C++ handle to pass to the destroy function
	public int CppHandle;
 
	public DestroyEntry(DestroyFunction function, int cppHandle)
	{
		Function = function;
		CppHandle = cppHandle;
	}
}
 
// The queue
static DestroyEntry[] destroyQueue;
static int destroyQueueCount;
static int destroyQueueCapacity;
 
// An lock object to create a critical section from
static object destroyQueueLockObj;
 
// Queue an object for destroy
public static void QueueDestroy(DestroyFunction function, int cppHandle)
{
	lock (destroyQueueLockObj)
	{
		// Grow capacity if necessary
		int count = destroyQueueCount;
		int capacity = destroyQueueCapacity;
		DestroyEntry[] queue = destroyQueue;
		if (count == capacity)
		{
			int newCapacity = capacity * 2;
			DestroyEntry[] newQueue = new DestroyEntry[newCapacity];
			for (int i = 0; i < capacity; ++i)
			{
				newQueue[i] = queue[i];
			}
			destroyQueueCapacity = newCapacity;
			destroyQueue = newQueue;
			queue = newQueue;
		}
 
		// Add to the end
		queue[count] = new DestroyEntry(function, cppHandle);
		destroyQueueCount = count + 1;
	}
}
 
// Destroy everything in the queue
static void DestroyAll()
{
	lock (destroyQueueLockObj)
	{
		int count = destroyQueueCount;
		DestroyEntry[] queue = destroyQueue;
		for (int i = 0; i < count; ++i)
		{
			DestroyEntry entry = queue[i];
			switch (entry.Function)
			{
				case DestroyFunction.BaseThing:
					// Call the C++ destroy function
					DestroyBaseThing(entry.CppHandle);
					break;
				// ... other types to destroy
			}
		}
		destroyQueueCount = 0;
	}
}

Finally, all we need is to call DestroyAll every frame or when hot reloading. This call is on the main thread, so the calls to C++ destroy functions are also on the main thread.

And with that we have support for the destruction side of the problem! The only piece left is to replace the special case code for MonoBehaviour. Now that we can properly support the GameObject.AddComponent<T> “factory function” by deriving the T type in C++.

Previously, we used the JSON config file to explicitly specify the MonoBehaviour classes it should generate and which “messages” (e.g. Update) we wanted to handle. Then we’d define those “message” functions in our C++ game code to handle them. This left a lot to be desired as we didn’t have control over the contents of the MonoBehaviour class since the code generator created it. We couldn’t add the fields we wanted, which is important for integration with the Inspector pane as a place to input data, establish links to other Unity objects, and provide debugging facilities.

So let’s remove that special case code and use the new factory function support to replace it with something better. To start, we need to create a class in C# that derives from MonoBehaviour. In here, we specify all the messages we want to handle and all the fields we want to expose to the Inspector pane. The class can be abstract since we’ll never actually instantiate it, only its derivatives. Here’s an example from the GitHub project that bounces a ball back and forth:

namespace MyGame
{
	public abstract class AbstractBaseBallScript : MonoBehaviour
	{
		public abstract void Update();
	}
}

Next, we add this abstract MonoBehaviour class to the Types section of the JSON config as with any derived class. We also add a generic parameter for the GameObject.AddComponent method for the base type:

{
	"Types": [
		{
			"Name": "UnityEngine.GameObject",
			"Methods": [
				{
					"Name": "AddComponent",
					"ParamTypes": [],
					"GenericParams": [
						{
							"Types": [
								"MyGame.BaseBallScript"
							]
						}
					]
				}
			]
		},
		{
			"Name": "MyGame.AbstractBaseBallScript",
			"BaseTypes": [
				{
					"BaseName": "MyGame.BaseThing",
					"DerivedName": "MyGame.Thing"
				}
			]
		}
	]
}

Now we add the definition of the class in C++:

// Game.h
namespace MyGame
{
	struct BallScript : MyGame::BaseBallScript
	{
		MY_GAME_BALL_SCRIPT_DEFAULT_CONSTRUCTOR
		void Update() override;
	};
}

Then we implememt the Update “message” function in an implementation file:

// Game.cpp
namespace MyGame
{
	void BallScript::Update()
	{
		Transform transform = GetTransform();
		Vector3 pos = transform.GetPosition();
		const float speed = 1.2f;
		const float min = -1.5f;
		const float max = 1.5f;
		float distance = Time::GetDeltaTime() * speed * gameState->BallDir;
		Vector3 offset(distance, 0, 0);
		Vector3 newPos = pos + offset;
		if (newPos.x > max)
		{
			gameState->BallDir *= -1.0f;
			newPos.x = max - (newPos.x - max);
			if (newPos.x < min)
			{
				newPos.x = min;
			}
		}
		else if (newPos.x < min)
		{
			gameState->BallDir *= -1.0f;
			newPos.x = min + (min - newPos.x);
			if (newPos.x > max)
			{
				newPos.x = max;
			}
		}
		transform.SetPosition(newPos);
	}
}

At long last, we can add a BallScript!

// Create a ball
GameObject go = GameObject::CreatePrimitive(PrimitiveType::Sphere);
 
// Add the script
go.AddComponent<BaseBallScript>();

It’s important to use the base type here because that’s what we declared in the JSON config file and that’s all the C# side knows about. It still doesn’t know about our BallScript, but we’ve established a one-to-one link between BaseBallScript and BallScript so there’s not much difference.

That’s all for today. Check out the GitHub project to see this in action and be sure to leave a comment if you’ve got any feedback.