C++ Scripting: Part 31 – Init Improvements
The C++ Scripting series continues today by going over some internal improvements that don’t add any features, but make the existing system more robust. We’ve lucked out in a couple of areas and today we’ll take the opportunity to fix them and learn about some inner workings of C++ and C# along the way.
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
- Part 31: Init Improvements
Init Parameters
The first change for today is in how the native plugin is initialized. Previously, we’d pass a few fixed parameters to Init
along with a ton of code-generated parameters. These were mostly function pointers for C++ to call when it wanted to invoke C# binding functions, but there were also some other parameters such as sizes of free lists. Here’s an example of what Init
has looked like until now:
DLLEXPORT void Init( uint8_t* memory, int32_t memorySize, void (*releaseObject)(int32_t handle), int32_t (*stringNew)(const char* chars), void (*setException)(int32_t handle), int32_t (*arrayGetLength)(int32_t handle), int32_t (*enumerableGetEnumerator)(int32_t handle), /*BEGIN INIT PARAMS*/ int32_t maxManagedObjects, void (*releaseSystemDecimal)(int32_t handle), int32_t (*systemDecimalConstructorSystemDouble)(double value), int32_t (*systemDecimalConstructorSystemUInt64)(uint64_t value), int32_t (*boxDecimal)(int32_t valHandle), int32_t (*unboxDecimal)(int32_t valHandle), UnityEngine::Vector3 (*unityEngineVector3ConstructorSystemSingle_SystemSingle_SystemSingle)(float x, float y, float z), UnityEngine::Vector3 (*unityEngineVector3Methodop_AdditionUnityEngineVector3_UnityEngineVector3)(UnityEngine::Vector3& a, UnityEngine::Vector3& b), int32_t (*boxVector3)(UnityEngine::Vector3& val), UnityEngine::Vector3 (*unboxVector3)(int32_t valHandle), /*END INIT PARAMS*/ InitMode initMode)
All the parameters between BEGIN INIT PARAMS
and END INIT PARAMS
are code-generated, so the number of parameters will grow as more and more C# functionality is exposed via the JSON config. C++ requires that at least 256 parameters are supported in function calls, which is excessive for hand-written code. However, the limit will easily be reached with the code generator adding more and more parameters to Init
.
To work around this, we need to move the parameters somewhere else. Thankfully, we’re already passing the memory
parameter. C# can store these code-generated parameters at the start of memory
and C++ can read them back out. We’re essentially passing parameters manually using the heap rather than in the normal way using the stack. The first step is to remove all the code-generated parameters from the signature of Init
so it looks like this now:
DLLEXPORT void Init( uint8_t* memory, int32_t memorySize, InitMode initMode)
Now there are only three parameters, so we’re well under the potential limit of 256. Next, we read the “parameters” out of memory
in Init
:
uint8_t* curMemory = memory; Plugin::ReleaseObject = *(void (**)(int32_t handle))curMemory; curMemory += sizeof(Plugin::ReleaseObject); Plugin::StringNew = *(int32_t (**)(const char*))curMemory; curMemory += sizeof(Plugin::StringNew); Plugin::SetException = *(void (**)(int32_t))curMemory; curMemory += sizeof(Plugin::SetException); Plugin::ArrayGetLength = *(int32_t (**)(int32_t))curMemory; curMemory += sizeof(Plugin::ArrayGetLength); Plugin::EnumerableGetEnumerator = *(int32_t (**)(int32_t))curMemory; curMemory += sizeof(Plugin::EnumerableGetEnumerator);
We use curMemory
as a pointer that moves forward after each parameter is read. The same technique works for arrays, such as the int32_t
arrays that store reference counts:
Plugin::RefCountsSystemDecimal = (int32_t*)curMemory; curMemory += 1000 * sizeof(int32_t); Plugin::RefCountsLenSystemDecimal = 1000;
On the C# side, we also change the editor and player definitions of Init
to only have three parameters:
// Editor definition used for hot reloading delegate void InitDelegate(IntPtr memory, int memorySize, InitMode initMode); // Player version used for direct calls [DllImport(PLUGIN_NAME)] static extern void Init(IntPtr memory, int memorySize, InitMode initMode);
Now we can stop passing all those parameters directly and instead pass them by writing them into memory
:
int curMemory = 0; Marshal.WriteIntPtr( memory, curMemory, Marshal.GetFunctionPointerForDelegate(ReleaseObjectDelegate)); curMemory += IntPtr.Size; Marshal.WriteIntPtr( memory, curMemory, Marshal.GetFunctionPointerForDelegate(StringNewDelegate)); curMemory += IntPtr.Size; Marshal.WriteIntPtr( memory, curMemory, Marshal.GetFunctionPointerForDelegate(SetExceptionDelegate)); curMemory += IntPtr.Size; Marshal.WriteIntPtr( memory, curMemory, Marshal.GetFunctionPointerForDelegate(ArrayGetLengthDelegate)); curMemory += IntPtr.Size; Marshal.WriteIntPtr( memory, curMemory, Marshal.GetFunctionPointerForDelegate(EnumerableGetEnumeratorDelegate)); curMemory += IntPtr.Size;
In this case, curMemory
is an offset from the start of memory
which allows us to pass it to Marshal
functions like WriteIntPtr
.
With these changes in place, we’re now avoiding passing too many parameters to Init
and running into issues with C++ environements that don’t support extreme numbers of parameters. No changes are necessary to game code as this is implemented purely in the bindings layer and code generator.
Static Delegates
The next change also relates to the function pointers that C# passes to C++ via Init
. So far, the call to Init
has included a bunch of parameters that look like this:
Marshal.GetFunctionPointerForDelegate(new ReleaseObjectDelegate(ReleaseObject)), Marshal.GetFunctionPointerForDelegate(new StringNewDelegate(StringNew)), Marshal.GetFunctionPointerForDelegate(new SetExceptionDelegate(SetException)), Marshal.GetFunctionPointerForDelegate(new ArrayGetLengthDelegate(ArrayGetLength)), Marshal.GetFunctionPointerForDelegate(new EnumerableGetEnumeratorDelegate(EnumerableGetEnumerator)),
Each of these parameters creates a new delegate for a static method and then gets a function pointer for it. In C#, the function pointer is of type IntPtr
but this is compatible with actual function pointer types like declared in C++. For example, the function pointer for Debug.Log
takes an int
handle for the object
to log and returns void
, so the C++ parameter is void (*debugLog)(int32_t)
.
There are two problems with this way of passing function pointers. First and foremost, C# isn’t retaining any references to the delegates it creates. So after it calls Init
the garbage collector is free to collect those delegates. Now if C++ calls a function pointer that invokes the delegate that’s been garbage-collected, there may be problems such as crashes.
The second problem is that a new delegate is created for every function exposed to C# every time Init
is called. As we’ve seen above, the parameter list can quickly grow to a very large size. Creating a delegate entails a managed allocation, which has the usual problems of being slow, causing heap fragmentation, and feeding the GC. So it’s best to only perform these allocations once, not every time the plugin is hot reloaded.
Both of these problems can be solved by creating the delegates only once and storing them in fields of the Bindings
class. Since functions exposed to C++ must be static, these fields can likewise be static. Here’s how it looks when we move the delegate creation to fields:
static readonly ReleaseObjectDelegateType ReleaseObjectDelegate = new ReleaseObjectDelegateType(ReleaseObject); static readonly StringNewDelegateType StringNewDelegate = new StringNewDelegateType(StringNew); static readonly SetExceptionDelegateType SetExceptionDelegate = new SetExceptionDelegateType(SetException); static readonly ArrayGetLengthDelegateType ArrayGetLengthDelegate = new ArrayGetLengthDelegateType(ArrayGetLength); static readonly EnumerableGetEnumeratorDelegateType EnumerableGetEnumeratorDelegate = new EnumerableGetEnumeratorDelegateType(EnumerableGetEnumerator);
Now the Init
call can use these, as we’ve seen above in the first part:
Marshal.GetFunctionPointerForDelegate(ReleaseObjectDelegate) Marshal.GetFunctionPointerForDelegate(StringNewDelegate) Marshal.GetFunctionPointerForDelegate(SetExceptionDelegate) Marshal.GetFunctionPointerForDelegate(ArrayGetLengthDelegate) Marshal.GetFunctionPointerForDelegate(EnumerableGetEnumeratorDelegate)
Conclusion
Today’s changes have made the project more able to handle the variability of systems that are outside of its direct control. By limiting the number of parameters passed to Init
to a low and constant three, we’re effectively steering clear of potential C++ problems with handling more than 256 parameters. By never releasing references to the delegates we create and use function pointers for, we’re ensuring that the GC will never invalidate our bindings when it collects. Regardless of whether either of these problems actually occurred, we no longer need to worry about them when supporting new platforms or debugging weird language binding issues.
As usual, the GitHub project has been updated with all the changes from this article. If you’ve got any questions, feature requests, or bug fixes, feel free to leave a comment or file an issue. If you’d like to contribute a feature, bug fix, or optimization directly, feel free to send a pull request.
#1 by Jrius on June 16th, 2018 ·
This is good news as my project uses a lot of different areas of Unity’s API, and I was rather concerned it would become an issue eventually as I’m already waaay past 256 parameters.
Incidentally, today I was getting issues with methods passed after the 785th-ish parameter missing from the C++ bindings. I’m not sure if that was directly related, but updating with the latest commit fixed it !
#2 by jackson on June 16th, 2018 ·
I’m glad this solved a problem for your project. They were the first couple of issues that came to mind while attempting to solve your Windows issue, so I finally wrote an article about them.
#3 by Morpheus on August 24th, 2018 ·
Not sure if this is related, but I’m having an issue where Time::GetDeltaTime() is returning a large negative value, but only when running standalone (outside the editor). When I run in-editor, it works as expected.
I’m on Windows 10 (Uniy 2017.3). The json and bindings for Time::GetDeltaTime look correct and I’m not having issues with any other floating point values outside of editor. Any ideas on what could be going on?
Thanks
#4 by jackson on August 25th, 2018 ·
I don’t know what could be happening. Does
Time.deltaTime
return similar values from C# in this environment? Do other functions return strange values for you?#5 by Morpheus on August 25th, 2018 ·
Time.deltaTime works correctly when used in C#, but it gives much larger and incorrect values when used from C++. The values in C++ are wrong both in-editor as well as standalone build, but the values are different between the 2 builds, which was confusing me before.
I’m using the latest githib sources and here is the json binding I am using:
{
“Name”: “UnityEngine.Time”,
“Properties”: [
{
“Name”: “deltaTime”,
“Get”: {}
}
]
},
All other functions are working fine so far, with the exception of Input.GetAxis(“”) which crashes with an invalid handle. But I am able to use other Input methods instead just fine (like GetKey(“”)).
Here’s my json for that as well:
{
“Name”: “UnityEngine.Input”,
“Methods”: [
{
“Name”: “GetAxis”,
“ParamTypes”: [
“System.String”
]
},
Thanks