C++ Scripting: Part 2 – Update C++ Without Restarting the Editor
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
- 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
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:
- Edit C# sources
- Switch to the Unity editor window
- Wait for the compile to finish
- Run the game
With C++, we work like this:
- Edit C++ sources
- Compile C++
- Switch to the Unity editor window
- 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.
#1 by benjamin guihaire on July 17th, 2017 ·
Great article!
I see many other topics that could be covered in your c++/Unity serie:
-debugging c++ & Unity on mac and windows, and debugging on game/app running on Device
-stdout (printf) from c++ and UnityEngine.Debug.Log
-managing Textures from c++ (thanks to Unity API CreateExternalTexture, GetNativeTexturePtr, …), to for example compress or decompress textures from c++
-Do graphics GLES rendering calls from c++…
-c++ multithreading & Unity.
– I would also mention the Trampoline code for iOS in Unity (c++/objective C source)
Thanks for the great work !
#2 by jackson on July 17th, 2017 ·
These are some great ideas. For the purposes of this series, I’m trying to restrict the articles to just the setup for scripting in C++. What you can actually do once you’re in C++ (e.g. graphics calls) is quite interesting, but won’t be part of this series. Future articles about that will reference back to this series for how to do all the setup work to get you to a point where you can start making those graphics calls, threads, etc.
Thanks for the ideas!
#3 by Vincent on January 15th, 2019 ·
Hi,
Thanks to you I can now rebuild my dll without restarting Unity.
However I am not sure how I should handle the following problem.
I have a tree on my player, which is a c# class, but it is mainly a wrapper as all the logic is in c++ for speed.
It keeps has an IntPtr to the c++ tree.
My problem is that when I stop the editor and my player get deleted, the Tree destructor is not called until I hit Play again, causing a crash.
How do I delete my tree if players can be spawned and killed?
Should I do it manually on player death instead of tree destructor?
Cheers
#4 by jackson on January 15th, 2019 ·
Remember that C# doesn’t really have destructors, despite the
~Tree()
syntax. These are actually finalizers, which run in a non-deterministic fashion. See this two part article series on their many ambiguities. Usually it’s best to take manual control, such as by callingDispose
types of functions from aOnApplicationQuit
message handler.#5 by Vincent on January 16th, 2019 ·
Thanks for your quick answer!
#6 by zhmt on March 28th, 2019 ·
Very good, marked and collected.
I am released from restarting UnityEditor over and over, Thanks. I share my code .
#7 by Cai on August 12th, 2019 ·
This will not work if DLL is linked with another DLL that has not “external C” code. Has anyone a solution for this situation?
#8 by Eric Zinda on September 11th, 2019 ·
I’ve tried to use your approach using the simplest possible class, in a brand new unity project, using a DLL that has zero entry points and is empty. The DLL loads fine, and FreeLibrary returns true, but the second time I run it in the editor I get an IOException “sharing violation” that indicates the temp dll is locked. Checking what process locked it reports Unity.exe. What am I doing wrong?
#9 by jackson on September 14th, 2019 ·
In UnityNativeScripting, I delete the copy of the DLL after freeing it. That may help prevent the issue you’re seeing.