C++ Scripting: Part 5 – Bindings Code Generator
Last week in the series we took a step back to verify that the C++ plugin’s performance was acceptable. With that confirmed, we’ll continue this week by making our programming lives easier. One pain point so far has been with exposing new Unity APIs to C++. It’s not that it’s difficult to do this, but there’s a lot of boilerplate required. That boilerplate takes time to write and it’s easy to make mistakes copying and pasting existing functions. So this week’s article introduces a code generator that will write the boilerplate for us! We’ll also reorganize the project a little so the code that supports C++ scripting is separated away from our game code. That’ll make it easy to add support for C++ scripting to any Unity project.
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
First, let’s add a code generator to generate all the boilerplate required to make C# functions callable by C++. The code generator is simply a Unity editor script that we can run with a menu item or via the command line. It loads a JSON file describing what should be exposed. For example, in previous articles we exposed the default GameObject
constructor and the GameObject.transform
and Transform.position
properties. Do do that, we’d add them and their base classes to a JSON document and say that those types can be found in UnityEngine.dll
:
{ "Assemblies": [ { "Path": "/Applications/Unity/Unity.app/Contents/Managed/UnityEngine.dll", "Types": [ { "Name": "UnityEngine.Object", "Constructors": [], "Methods": [], "Properties": [], "Fields": [] }, { "Name": "UnityEngine.GameObject", "Constructors": [ { "Types": [] } ], "Properties": [ "transform" ], "Fields": [] }, { "Name": "UnityEngine.Component", "Constructors": [], "Methods": [], "Properties": [], "Fields": [] }, { "Name": "UnityEngine.Transform", "Constructors": [], "Methods": [], "Properties": [ "position" ], "Fields": [] } ] } ] }
This document allows us to specify exactly what we want to expose to C++. This keeps bloat down and allows the code generator to run really fast: under a second on my machine. We simply fill in the constructors, methods, properties, and fields we want exposed and run the code generator to fill in all the required C# and C++ code.
The code generator is currently 1700 lines, so I won’t go over all of how it works. At a high level though, it uses C# reflection to load a DLL, builds a lot of strings of source code, and injects them into existing source files between start and end markers. Currently, the code generator supports some basic functionality but lacks support for some C# features:
Supported | Not Supported |
---|---|
Constructors | Arrays (single- or multi-dimensional) |
Properties (get and set) | out and ref parameters |
Fields | Struct types |
Methods | Generic functions and types |
Class types (static and regular) | Delegates |
With this tool available to us, we can quickly expose new Unity API functionality. For example, if we wanted to expose the Component.transform
property we’d simply modify one line of the JSON and run the code generator:
"Properties": [ "transform" ],
We can also expose functionality from non-Unity DLLs. For example, we can expose System.Diagnostics.Stopwatch
from the .NET API by adding this JSON block:
{ "Path": "/Applications/Unity/Unity.app/Contents/Mono/lib/mono/unity/System.dll", "Types": [ { "Name": "System.Diagnostics.Stopwatch", "Constructors": [ { "Types": [] } ], "Methods": [ { "Name": "Start", "Types": [] }, { "Name": "Reset", "Types": [] } ], "Properties": [ "ElapsedMilliseconds" ], "Fields": [] } ] }
Then we can use it just like the Unity types we exposed before:
// Make the Stopwatch and start it Stopwatch sw; sw.Start(); // ... do slow stuff // Find out how long the slow stuff took int64_t elapsed = sw.GetElapsedMilliseconds(); // Print the result to Unity's Debug console char buf[128]; sprintf(buf, "Slow stuff took: %lld", elapsed); Debug::Log(String(buf));
Next up for today, let’s split the existing code up into two parts. First is the code that supports C++ scripting. This part includes all the native plugin loading and unloading, exposing C# functions to C++, and object-oriented type definitions. This is the “bindings” layer that games shouldn’t ever need to touch. It should be easy to copy/paste into a new project and start scripting that project in C++.
The second part is the part where all the game-specific code goes. It just focuses on getting the job done for the game, not the gory details of how to bind C# to C++. It looks like the Stopwatch
code above.
So far everything has been in one C# file and one C++ file. Instead, we’ll split up the project this way:
Assets |- Game.cpp // Game-specific code. Can rename this file, add headers, etc. |- NativeScriptTypes.json // JSON describing which .NET types the game wants to expose to C++ |- NativeScriptConstants.cs // Game-specific constants such as plugin names and paths |- NativeScript/ // C++ scripting system. Drop this into your project. |- Editor/ |- GenerateBindings.cs // Code generator |- Bindings.cs // C# code to expose functionality to C++ |- ObjectStore.cs // Object handles system |- Bindings.h // C++ wrapper types for C# (declaration) |- Bindings.cpp // C++ wrapper types for C# (definition) |- BootScript.cs // MonoBehaviour to boot up the C++ plugin |- BootScene.unity // Scene with just BootScript on an empty GameObject
This way the NativeScript
directory can simply be dropped into a Unity project or updated. The game never modifies anything in there. It does, however, require the game to provide a few things. First, the game must define a NativeConstants
class in the global namespace with a few required fields:
public static class NativeScriptConstants { /// <summary> /// Name of the plugin used by [DllImport] when running outside the editor /// </summary> public const string PluginName = "NativeScript"; /// <summary> /// Path to load the plugin from when running inside the editor /// </summary> #if UNITY_EDITOR_OSX public const string PluginPath = "/NativeScript.bundle/Contents/MacOS/NativeScript"; #elif UNITY_EDITOR_LINUX public const string PluginPath = "/NativeScript.so"; #elif UNITY_EDITOR_WIN public const string PluginPath = "/NativeScript.dll"; #endif /// <summary> /// Maximum number of simultaneous managed objects that the C++ plugin uses /// </summary> public const int MaxManagedObjects = 1024; /// <summary> /// Path within the Unity project to the exposed types JSON file /// </summary> public const string ExposedTypesJsonPath = "NativeScriptTypes.json"; }
Second, NativeScriptConstants.ExposedTypesJsonPath
needs to point to a JSON file describing all the .NET types to expose. This is the same kind of JSON file as we talked about above.
Third, the game’s C++ code must define two functions:
// Called when the plugin is initialized void PluginMain() { } // Called for MonoBehaviour.Update void PluginUpdate() { }
That’s it! With those simple requirements out of the way, any Unity project can start writing scripts in C++.
At this point there are multiple files, a directory structure, and a sizable code generator involved so there’s too much to post right here in the article. So I’ve created a GitHub project which you can check out if you want to see the details or try out some C++ scripting for yourself.
The last major missing piece of this project is a system to easily build the C++ plugin for a variety of platforms: Windows, Mac, iOS, Android, etc. That will be the subject of next week’s article.
#1 by Young on April 19th, 2021 ·
Any idea how Unity does this? It has something called XXX.bindings.cs.
What I wanted to do at this point is : Defining a class, which is both in C++ and C#, so that I could mess around in both ends.
#2 by jackson on April 22nd, 2021 ·
I’m not sure how those files are created, but this article series and its GitHub project are one way to automatically create bindings for your C# code to your C++ code.