C++ Scripting: Part 3 – Object-Oriented Bindings
Last week’s article continued the series by eliminating the need to reboot the editor for every change to the C++ plugin. The idea is to make a more productive environment for us, the programmers, to work in. This week we’ll continue that theme by mimicking the object-oriented Unity API in C++. So instead of int transformHandle = GameObjectGetTransform(goHandle)
we’ll write a more familiar Transform transform = go.GetTransform()
. Also, we’ll build a simple system to automatically clean up object handles so we don’t have to do that manually.
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
In part one, we introduced two-way communication between C# and C++. The way we allowed C++ to call C# was by passing it function pointers. That meant that GameObject.transform
becomes this:
// Return Transform handle | Function pointer name | Take a handle to the GO int32_t (*GameObjectGetTransform) (int32_t thiz);
We’d like to mimic the Unity API and use Transform
and GameObject
types instead of just int32_t
object handles. Thankfully, C++ makes this quite easy to implement!
First, we’ll need a class to represent the managed object
type that we use handles for. Let’s start simple:
namespace System { struct Object { int32_t Handle; }; }
In C++, a struct
is just a class
with the default access specifier being public
instead of private
.
Now that we have an Object
type, we can make Transform
and GameObject
:
namespace UnityEngine { struct Vector3 { float x; float y; float z; }; struct Transform : System::Object { void SetPosition(Vector3 val) { TransformSetPosition(Handle, val); } }; struct GameObject : System::Object { GameObject() { Handle = GameObjectNew(); } Transform GetPosition() { Transform transform; transform.Handle = GameObjectGetTransform(Handle); return transform; } }; }
Basically we’ve just introduced some classes that wrap calls to the function pointers. It makes a world of difference in readability though:
// Old... int goHandle = GameObjectNew(); int transformHandle = GameObjectGetTransform(goHandle); Vector3 position = { 1, 2, 3 }; TransformSetPosition(transformHandle, position); // New... GameObject go; Transform transform = go.GetTransform(); Vector3 position = { 1, 2, 3 }; transform.SetPosition(position);
Even better is that the size of any Object
, GameObject
, or Transform
class is just the size of the int32_t Handle
it contains: 4 bytes. So it’s super cheap to pass these types around. It’s the same size as a pointer on 32-bit systems and half a pointer on 64-bit systems, so passing them by value is actually more efficient.
At this point we have our object-oriented C++ API mimicking the Unity API. As we can see, it was really easy to implement efficiently and has given a lot of gains in familiarity and type safety. Now that we have it, we can tie up some loose ends.
We’ve been using object handles this whole time, but never bothering to clean them up. Eventually we’d run out of object handles and the app would crash. We could manually clean them up, and in fact that’s a good place to start. All we need is a function for C++ to call when it’s done with a handle:
// C#... // Define a function for C++ to call static void ReleaseObject(int handle) { if (handle != 0) { ObjectStore.Remove(handle); } } // Define a delegate for the function delegate void ReleaseObjectDelegate(int handle); // This is the C++ function for C# to call to init the C++ plugin // Now it takes two additional parameters: // 1) maxManagedObjects - So C++ knows how many object handles it can use // 2) releaseObject - Function pointer to ReleaseObject (see above) [DllImport("NativeScript")] static extern void Init( int maxManagedObjects, IntPtr releaseObject, IntPtr gameObjectNew, IntPtr gameObjectGetTransform, IntPtr transformSetPosition); // Add the two new parameters to the delegate for Init public delegate void InitDelegate( int maxManagedObjects, IntPtr releaseObject, IntPtr gameObjectNew, IntPtr gameObjectGetTransform, IntPtr transformSetPosition); // This is C# initializing the C++ plugin // Just need to pass in the two new parameters const int maxManagedObjects = 1024; ObjectStore.Init(maxManagedObjects); Init( maxManagedObjects, Marshal.GetFunctionPointerForDelegate( new ReleaseObjectDelegate( ReleaseObject)), Marshal.GetFunctionPointerForDelegate( new GameObjectNewDelegate( GameObjectNew)), Marshal.GetFunctionPointerForDelegate( new GameObjectGetTransformDelegate( GameObjectGetTransform)), Marshal.GetFunctionPointerForDelegate( new TransformSetPositionDelegate( TransformSetPosition)));
That’s all for the C# side. Now on to the C++ side:
// C++... // Function pointer for C#'s ReleaseObject function void (*ReleaseObject)(int32_t handle); // Add the same two parameters to the Init function DLLEXPORT void Init( int32_t maxManagedObjects, void (*releaseObject)(int32_t), int32_t (*gameObjectNew)(), int32_t (*gameObjectGetTransform)(int32_t), void (*transformSetPosition)(int32_t, UnityEngine::Vector3)) // Store the function pointer ReleaseObject = releaseObject;
Now we could manually call to release object handles when we’re done with them:
// Get object handles GameObject go; Transform transform = go.GetTransform(); Vector3 position = { 1, 2, 3 }; transform.SetPosition(position); // Release object handles ReleaseObject(transform.Handle); ReleaseObject(go.Handle);
Manually releasing object handles is error-prone and unintuitive for those of us that are used to the garbage collector in C#. So in keeping with the conversion to object-oriented style for the GameObject
and Transform
types, we’ll also implement automatic handle releasing so those two ReleaseObject(x.Handle)
lines become unnecessary.
This sort of thing is actually quite a core feature of C++. It’s known as RAII: Resource Acquisition Is Initialization. The idea is that if we create an instance of a GameObject
then we have acquired a resource in the form of an object handle. When we destroy that GameObject
instance then the underlying object handle resource is also destroyed. As with the standard library’s shared_ptr type, this can be easily extended to do reference counting so we only release the object handle when the last GameObject
is destroyed. In practice, this works very similarly to a garbage collector when cleans up objects only when the last reference to them is removed.
To implement this, we’ll make use of the new maxManagedObjects
parameter to Init
. Just like on the C# side where handles are an index into an array of object
, we’ll set up an array of reference counts in C++:
// Global... int32_t managedObjectsRefCountLen; int32_t* managedObjectRefCounts; // In Init()... managedObjectsRefCountLen = maxManagedObjects; managedObjectRefCounts = (int32_t*)calloc(maxManagedObjects, sizeof(int32_t));
calloc
makes sure that all the reference counts start at zero. Now we just need to increment the reference count in the constructor for GameObject
:
GameObject() { Handle = GameObjectNew(); managedObjectsRefCounts[Handle]++; }
In the destructor, we decrement the reference count and release the handle if it’s hit zero:
~GameObject() { if (--managedObjectsRefCounts[Handle] == 0) { ReleaseObject(Handle); } }
At this point we no longer need to call ReleaseObject(go.Handle)
in the above example. However, we haven’t implemented this for Transform
so we still need to release that handle. Implementing this same code over and over for every System::Object
type means a lot of code duplication. Even worse, C++ doesn’t just have constructors and destructors but also “copy” and “move” constructors and assignment operators. Thankfully, we can work around this using a macro so we just need to do this to insert all the various constructors, destructors, and assignment operators that C++ requires:
struct GameObject : Object { SYSTEM_OBJECT_LIFECYCLE(GameObject, Object) // ... };
Now we never need to call ReleaseObject
and even our types like GameObject
don’t need to be concerned with it. It’s tucked away in a macro that deals with the reference counting for us. So we can safely remove the ReleaseObject
calls and we’ll still release the object handles when we’re done with them, regardless of how we pass them around through our code.
Here’s the full C# side, still at just 231 lines:
using System; using System.IO; using System.Runtime.InteropServices; using UnityEngine; class TestScript : MonoBehaviour { #if UNITY_EDITOR // Handle to the C++ DLL public IntPtr libraryHandle; public delegate void InitDelegate( int maxManagedObjects, IntPtr releaseObject, IntPtr gameObjectNew, IntPtr gameObjectGetTransform, IntPtr transformSetPosition); public delegate void MonoBehaviourUpdateDelegate(); public MonoBehaviourUpdateDelegate MonoBehaviourUpdate; #endif #if UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX [DllImport("__Internal")] public static extern IntPtr dlopen( string path, int flag); [DllImport("__Internal")] public static extern IntPtr dlsym( IntPtr handle, string symbolName); [DllImport("__Internal")] public static extern int dlclose( IntPtr handle); public static IntPtr OpenLibrary(string path) { IntPtr handle = dlopen(path, 0); if (handle == IntPtr.Zero) { throw new Exception("Couldn't open native library: " + path); } return handle; } public static void CloseLibrary(IntPtr libraryHandle) { dlclose(libraryHandle); } public static T GetDelegate<T>( IntPtr libraryHandle, string functionName) where T : class { IntPtr symbol = dlsym(libraryHandle, functionName); if (symbol == IntPtr.Zero) { throw new Exception("Couldn't get function: " + functionName); } return Marshal.GetDelegateForFunctionPointer( symbol, typeof(T)) as T; } #elif UNITY_EDITOR_WIN [DllImport("kernel32")] public static extern IntPtr LoadLibrary( string path); [DllImport("kernel32")] public static extern IntPtr GetProcAddress( IntPtr libraryHandle, string symbolName); [DllImport("kernel32")] public static extern bool FreeLibrary( IntPtr libraryHandle); public static IntPtr OpenLibrary(string path) { IntPtr handle = LoadLibrary(path); if (handle == IntPtr.Zero) { throw new Exception("Couldn't open native library: " + path); } return handle; } public static void CloseLibrary(IntPtr libraryHandle) { FreeLibrary(libraryHandle); } public static T GetDelegate<T>( IntPtr libraryHandle, string functionName) where T : class { IntPtr symbol = GetProcAddress(libraryHandle, functionName); if (symbol == IntPtr.Zero) { throw new Exception("Couldn't get function: " + functionName); } return Marshal.GetDelegateForFunctionPointer( symbol, typeof(T)) as T; } #else [DllImport("NativeScript")] static extern void Init( int maxManagedObjects, IntPtr releaseObject, IntPtr gameObjectNew, IntPtr gameObjectGetTransform, IntPtr transformSetPosition); [DllImport("NativeScript")] static extern void MonoBehaviourUpdate(); #endif delegate void ReleaseObjectDelegate( int handle); delegate int GameObjectNewDelegate(); delegate int GameObjectGetTransformDelegate( int thisHandle); delegate void TransformSetPositionDelegate( int thisHandle, Vector3 val); #if UNITY_EDITOR_OSX const string LIB_PATH = "/NativeScript.bundle/Contents/MacOS/NativeScript"; #elif UNITY_EDITOR_LINUX const string LIB_PATH = "/NativeScript.so"; #elif UNITY_EDITOR_WIN const string LIB_PATH = "/NativeScript.dll"; #endif void Awake() { #if UNITY_EDITOR // Open native library libraryHandle = OpenLibrary(Application.dataPath + LIB_PATH); InitDelegate Init = GetDelegate<InitDelegate>( libraryHandle, "Init"); MonoBehaviourUpdate = GetDelegate<MonoBehaviourUpdateDelegate>( libraryHandle, "MonoBehaviourUpdate"); #endif // Init C++ library const int maxManagedObjects = 1024; ObjectStore.Init(maxManagedObjects); Init( maxManagedObjects, Marshal.GetFunctionPointerForDelegate( new ReleaseObjectDelegate( ReleaseObject)), Marshal.GetFunctionPointerForDelegate( new GameObjectNewDelegate( GameObjectNew)), Marshal.GetFunctionPointerForDelegate( new GameObjectGetTransformDelegate( GameObjectGetTransform)), Marshal.GetFunctionPointerForDelegate( new TransformSetPositionDelegate( TransformSetPosition))); } void Update() { MonoBehaviourUpdate(); } void OnApplicationQuit() { #if UNITY_EDITOR CloseLibrary(libraryHandle); libraryHandle = IntPtr.Zero; #endif } //////////////////////////////////////////////////////////////// // C# functions for C++ to call //////////////////////////////////////////////////////////////// static void ReleaseObject( int handle) { ObjectStore.Remove(handle); } static int GameObjectNew() { GameObject obj = new GameObject(); int handle = ObjectStore.Store(obj); return handle; } static int GameObjectGetTransform( int thisHandle) { GameObject thiz = (GameObject)ObjectStore.Get(thisHandle); Transform retVal = thiz.transform; int handle = ObjectStore.Store(retVal); return handle; } static void TransformSetPosition( int thisHandle, Vector3 val) { Transform thiz = (Transform)ObjectStore.Get(thisHandle); thiz.position = val; } }
And here’s the full C++ side, clocking in at 257 lines:
// For assert() #include <assert.h> // For int32_t, etc. #include <stdint.h> // For malloc(), etc. #include <stdlib.h> // For std::forward #include <utility> // Macro to put before functions that need to be exposed to C# #ifdef _WIN32 #define DLLEXPORT extern "C" __declspec(dllexport) #else #define DLLEXPORT extern "C" #endif //////////////////////////////////////////////////////////////// // C# struct types //////////////////////////////////////////////////////////////// namespace UnityEngine { struct Vector3 { float x; float y; float z; Vector3() : x(0.0f) , y(0.0f) , z(0.0f) { } Vector3( float x, float y, float z) : x(x) , y(y) , z(z) { } }; } //////////////////////////////////////////////////////////////// // C# functions for C++ to call //////////////////////////////////////////////////////////////// namespace Plugin { using namespace UnityEngine; void (*ReleaseObject)( int32_t handle); int32_t (*GameObjectNew)(); int32_t (*GameObjectGetTransform)( int32_t thiz); void (*TransformSetPosition)( int32_t thiz, Vector3 val); } //////////////////////////////////////////////////////////////// // Reference counting of managed objects //////////////////////////////////////////////////////////////// namespace Plugin { int32_t managedObjectsRefCountLen; int32_t* managedObjectRefCounts; void ReferenceManagedObject(int32_t handle) { assert(handle >= 0 && handle < managedObjectsRefCountLen); if (handle != 0) { managedObjectRefCounts[handle]++; } } void DereferenceManagedObject(int32_t handle) { assert(handle >= 0 && handle < managedObjectsRefCountLen); if (handle != 0) { int32_t numRemain = --managedObjectRefCounts[handle]; if (numRemain == 0) { ReleaseObject(handle); } } } } //////////////////////////////////////////////////////////////// // Mirrors of C# types. These wrap the C# functions to present // a similiar API as in C#. //////////////////////////////////////////////////////////////// namespace System { struct Object { int32_t Handle; Object(int32_t handle) { Handle = handle; Plugin::ReferenceManagedObject(handle); } Object(const Object& other) { Handle = other.Handle; Plugin::ReferenceManagedObject(Handle); } Object(Object&& other) { Handle = other.Handle; other.Handle = 0; } }; #define SYSTEM_OBJECT_LIFECYCLE(ClassName, BaseClassName) \ ClassName(int32_t handle) \ : BaseClassName(handle) \ { \ } \ \ ClassName(const ClassName& other) \ : BaseClassName(other) \ { \ } \ \ ClassName(ClassName&& other) \ : BaseClassName(std::forward<ClassName>(other)) \ { \ } \ \ ~ClassName() \ { \ DereferenceManagedObject(Handle); \ } \ \ ClassName& operator=(const ClassName& other) \ { \ DereferenceManagedObject(Handle); \ Handle = other.Handle; \ ReferenceManagedObject(Handle); \ return *this; \ } \ \ ClassName& operator=(ClassName&& other) \ { \ DereferenceManagedObject(Handle); \ Handle = other.Handle; \ other.Handle = 0; \ return *this; \ } } namespace UnityEngine { using namespace System; using namespace Plugin; struct GameObject; struct Component; struct Transform; struct GameObject : Object { SYSTEM_OBJECT_LIFECYCLE(GameObject, Object) GameObject(); Transform GetTransform(); }; struct Component : Object { SYSTEM_OBJECT_LIFECYCLE(Component, Object) }; struct Transform : Component { SYSTEM_OBJECT_LIFECYCLE(Transform, Component) void SetPosition(Vector3 val); }; GameObject::GameObject() : GameObject(GameObjectNew()) { } Transform GameObject::GetTransform() { return Transform(GameObjectGetTransform(Handle)); } void Transform::SetPosition(Vector3 val) { TransformSetPosition(Handle, val); } } //////////////////////////////////////////////////////////////// // C++ functions for C# to call //////////////////////////////////////////////////////////////// // Init the plugin DLLEXPORT void Init( int32_t maxManagedObjects, void (*releaseObject)(int32_t), int32_t (*gameObjectNew)(), int32_t (*gameObjectGetTransform)(int32_t), void (*transformSetPosition)(int32_t, UnityEngine::Vector3)) { using namespace Plugin; // Init managed object ref counting managedObjectsRefCountLen = maxManagedObjects; managedObjectRefCounts = (int32_t*)calloc( maxManagedObjects, sizeof(int32_t)); // Init pointers to C# functions ReleaseObject = releaseObject; GameObjectNew = gameObjectNew; GameObjectGetTransform = gameObjectGetTransform; TransformSetPosition = transformSetPosition; } // Called by MonoBehaviour.Update DLLEXPORT void MonoBehaviourUpdate() { using namespace UnityEngine; static int32_t numCreated = 0; if (numCreated < 10) { GameObject go; Transform transform = go.GetTransform(); float comp = (float)numCreated; Vector3 position(comp, comp, comp); transform.SetPosition(position); numCreated++; } }
For only about 500 lines of code we now have a programming environment with the following features:
- All game code written in C++
- C++ changes don’t require restarting the editor
- C++ Unity API looks mostly the same
- C++ Unity API behaves mostly the same
We’re approaching a system we could actually use to make a game. Still, it’s missing some important pieces to make it truly easy to work with. One is that it takes more work than we’d like to expose more of the Unity API to C++. We’ll tackle that issue in next week’s article later in the series.
Continue to the next article in the series.
#1 by Alexis Barra on October 24th, 2017 ·
Hey there,
I’ve been reading this article and I think is great! Thanks for taking time to write this!
I’m curious about the GetTranform method .. Think about using it in the MonoUpdate for a single Object, after 1024 frames the assert is going to crash the editor because each time you are storing it. I’m thinking of a solution for it. What do you think?
#2 by jackson on October 24th, 2017 ·
I’m glad you’re enjoying the articles in this series! As to your question…
When the
transform
variable goes out of scope at the end of theif
block in theMonoBehaviourUpdate
, its destructor will be called. The destructor is called~Transform
in C++ and it’s part of theSYSTEM_OBJECT_LIFECYCLE
macro. The destructor just callsDereferenceManagedObject
, which decrements the reference count and callsReleaseObject
in C# to clean it up. So it shouldn’t crash due to running out of managed objects.#3 by Richard Biely on November 13th, 2017 ·
Std::forward in the SYSTEM_OBJECT_LIFECYCLE should actually be std::move.
Forward is used with templates to cast a template function parameter to an rvalue/lvalue based on how the called passed it.
Move allows you to treat a variable as a temporary and thus a rvalue.
As for the article itself – great as always! :)
#4 by jackson on November 13th, 2017 ·
Thanks for pointing this out. In future articles I removed the
SYSTEM_OBJECT_LIFECYCLE
macro and thestd::forward
in favor of code-generating all the boilerplate constructors, etc. It also allowed me to remove theutility
header altogether.Check out the table of contents at the top of the page for the rest of the series. You seem like you know a lot about C++, so I’d be interested to hear your feedback on the later parts of the series and the system as a whole.
#5 by Rory on May 28th, 2018 ·
Thanks for the article! I was writing my own version of this and I think I found an issue with the way objects are being dereferenced.
Destroying a C++ Transform object calls ~Transform(), which dereferences it, and then because Transform inherits from Component it then calls ~Component(), which in turn calls DereferenceManagedObject a second time. Since ReferenceManagedObject is called from the base Object class constructor it will only be called once even though dereference is being called twice, which means that if you have two references to an object, the C# object will be released after the first C++ reference is destroyed.
I found this by adding an extra assert to DereferenceManagedObject:
Moving the call to DereferenceManageObject out of SYSTEM_OBJECT_LIFECYCLE and into the destructor of Object fixes the issue but I’m wondering if there was another reason it needed to be included in SYSTEM_OBJECT_LIFECYCLE?
#6 by Rory on May 28th, 2018 ·
The comment box seems to think less-than and greater-than signs are HTML tags and cut out some of my paste. Full version is here: https://gist.github.com/RoryDungan/4c24b1b3c35f5e03f3083cf6948fcab0
#7 by jackson on May 29th, 2018 ·
I’m glad you enjoyed the article and that this series and project may be useful to you. Please point me to your version of this project if it’s publicly available as I’d love to take a look.
Regarding the issue you found, it’s indeed a problem with this version. However, 27 more articles have been written in this series and somewhere in there the bug was fixed. For example, see the new version of ~Transform() and ~Component() that the code generator (added in part five of the series) outputs:
DereferenceManagedObject
was renamed to DereferenceManagedClass but is basically unchanged since this article:The crucial change is the
Handle = 0
line right afterDereferenceManagedClass
is called by the destructors. That’ll prevent theif (Handle)
test from passing when the base class’ destructor is called, soDereferenceManagedClass
won’t be called multiple times for the same object.#8 by Rory on May 30th, 2018 ·
That makes sense. Setting the handle to 0 after calling DereferenceManagedClass will also fix the issue because that means that, even though DereferenceManagedClass is called for however many classes there are on the inheritance chain, it won’t do anything except for the first time it’s called for that object.