Last week we began the series by establishing two-way communication between C# and C++. We used object handles to pass class instances between the two languages. Everything was going great, but then there was a major productivity problem: we had to restart the Unity editor every time we changed the C++ plugin. Today’s article is all about how to overcome that obstacle so you can iterate on your code just like with C#.

Table of Contents

The issue we ran into last week was related to using the [DllImport] attribute to allow C# to call C++ functions. It turns out that the Unity editor will only load the C++ plugin library once, even if it changes. So when we make changes to our C++ code and rebuild the plugin, we had to restart the Unity editor so it would load the plugin again. As programmers, we’re constantly changing the code and really don’t want to wait for the Unity editor to restart every single time.

The solution is to do things a little more manually than just using the [DllImport] attribute. Instead, we’ll use some functions that the OS provides to load, use, and close C++ libraries. This can be done easily on Mac, Windows, and Linux editors with the following function families:

Mac Windows Linux
Open a DLL dlopen LoadLibrary dlopen
Get a Function Pointer dlsym GetProcAddress dlsym
Close a DLL dlclose FreeLibrary dlclose

To access these, we use [DllImport] just like we did with our C++ plugin. This time, however, we import from the __Internal library on Mac and Linux and kernel32 on Windows instead of our NativeScript plugin library. Since these functions are part of the OS, we don’t have to worry about them changing and needing to restart the Unity editor.

Here’s how these functions work: (Mac/Linux example)

// Open the C++ library
IntPtr handle = dlopen("/path/to/my/lib", 0);
 
// Get a function pointer symbol
IntPtr symbol = dlsym(handle, "MyFunction");
 
// Get a delegate for the function pointer
delegate bool MyFunctionDelegate(int a, float b);
MyFunctionDelegate myFunction = Marshal.GetDelegateForFunctionPointer(
	symbol,
	typeof(MyFunctionDelegate)) as MyFunctionDelegate;
 
// Use the delegate
bool retVal = myFunction(123, 3.14f);
 
// Close the C++ library
dlclose(handle);

This means there is one more step to initializing the C++ plugin. We can’t just call Init anymore because we don’t have the delegate for Init. So we insert some conditionally-compiled code with #if UNITY_EDITOR to open the plugin library then get a delegate for each C++ function we want to call. We store the delegates as fields with the same name as the functions we would have declared with [DllImport] when not in the Unity editor. Finally, when OnApplicationQuit is called, we close the plugin library.

The result is still a pretty small script:

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(
		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(
		IntPtr gameObjectNew,
		IntPtr gameObjectGetTransform,
		IntPtr transformSetPosition);
 
	[DllImport("NativeScript")]
	static extern void MonoBehaviourUpdate();
 
#endif
 
	delegate int GameObjectNewDelegate();
	delegate int GameObjectGetTransformDelegate(
		int thisHandle);
	delegate void TransformSetPositionDelegate(
		int thisHandle,
	Vector3 position);
 
#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
		ObjectStore.Init(1024);
		Init(
			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 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;
	}
}

The C++ doesn’t need to change at all from the last article to keep working on Mac and Linux. We’ve only changed how C# makes use of the C++ plugin library. For Windows, however, we need to add __declspec(dllexport) before each function that C# calls. A DLLEXPORT macro makes that easy:

#ifdef _WIN32
	#define DLLEXPORT __declspec(dllexport)
#else
	#define DLLEXPORT 
#endif
 
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;
 
	DLLEXPORT void Init(
		int (*gameObjectNew)(),
		int (*gameObjectGetTransform)(int),
		void (*transformSetPosition)(int, Vector3))
	{
		GameObjectNew = gameObjectNew;
		GameObjectGetTransform = gameObjectGetTransform;
		TransformSetPosition = transformSetPosition;
 
		numCreated = 0;
	}
 
	DLLEXPORT void MonoBehaviourUpdate(int thisHandle)
	{
		if (numCreated < 10)
		{
			int goHandle = GameObjectNew();
			int transformHandle = GameObjectGetTransform(goHandle);
			float comp = 10.0f * (float)numCreated;
			Vector3 position = { comp, comp, comp };
			TransformSetPosition(transformHandle, position);
			numCreated++;
		}
	}
}

That’s all the code we need to write to allow for changes to the C++ code without needing to restart the editor. Now let’s compare our workflows. In C#, we work like this:

  1. Edit C# sources
  2. Switch to the Unity editor window
  3. Wait for the compile to finish
  4. Run the game

With C++, we work like this:

  1. Edit C++ sources
  2. Compile C++
  3. Switch to the Unity editor window
  4. Run the game

The C++ workflow is more-or-less equivalent to with C# now: a quick edit-compile-run cycle. Development is no longer horrible, but can still be improved greatly. More on that in next week’s article.