C++ Scripting: Part 29 – Factory Functions and New MonoBehaviours
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
- 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
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.