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

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.