C++ Scripting: Part 10 – Full Generics Support
C# APIs are chock-full of generics. Generic types, generic method parameters, generic return types, generic fields, generic properties, deriving from generic types, and generic constructors. We can find all of these in the Unity and .NET APIs. Some are more frequent than others, but we’re going to need support for all of them to make C++ scripting a viable alternative to C#. Today’s article continues the series by adding just that: support for all of these kinds of generics. Let’s dive into how to use them as well as some bonus items added to the project this week.
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
As usual, ease of use is a top consideration for the C++ scripting system. As such, it should be easy to specify which generics for the code generator to expose to C++ game code. Things have changed a little since part 7 where we first dipped our toes in the water with rudimentary support for generic return types so we could call AddComponent<T>
with our C++
MonoBehaviour
types. Let’s see how AddComponent<T>
is specified in the code generator’s JSON now:
{ "Name": "UnityEngine.GameObject", "Methods": [ { "Name": "AddComponent", "ParamTypes": [], "GenericParams": [ { "Types": [ "MyGame.MonoBehaviours.TestScript" ] } ] } ] }
This says a few things. It says the name of the method and the method’s parameters (none in this case) as with any other method, but now has a GenericParams
array. This array specifies sets of generic parameters that we want to use from C++ code. Each set contains just one item (Types
) which are the names of the generic/type parameters. In this case, we just specify one set so AddComponent<MyGame.MonoBehaviours.TestScript>
will be generated.
Using this from C++ code is the same as before. It’s just like in C#:
GameObject go; TestScript script = go.AddComponent<TestScript>();
But now we have support for much more than just methods. By adding support for generic types, we allow for all of those generic type parameters to filter down to constructors, fields, properties, and methods. All we need to do is specify which type parameter sets we want to use with the type itself, just like with the method. Here’s an example:
{ "Name": "System.Collections.Generic.List`1", "GenericParams": [ { "Types": [ "System.String" ] }, { "Types": [ "System.Int32" ] } ], "Methods": [ { "Name": "Add", "ParamTypes": [ "T" ] } ] }
Here we expose the often-used List<T>
class to C++. We’ll specify two sets of generic parameters this time so we’ll have access to List<string>
and List<int>
in C++. There’s one difference here compared to normal class names due to the class having one generic type. The .NET name for the class includes a `X
suffix on it indicating that it’s a generic class that has X
generic types. So it’s List`1
for List<T>
and Dictionary`2
for Dictionary<TKey, TValue>
. It seems strange, but it’s an easy and logical rule that makes sense once used a few times.
We’ve also added a method that takes a generic parameter from the class, not the method itself. As such, we have access to List.Add(T)
from C++ for all the generic type sets we specified for the class. That’s why we specify T
instead of a full type name. Keeping it generic means the code generator will generate an Add<string>
and a Add<int>
for us.
To use this generic method in C++, all we do is write code that looks like C#:
List<String> strings; strings.Add("one"); strings.Add("two"); strings.Add("three"); List<int32_t> ints; ints.Add(123); ints.Add(456); ints.Add(789);
The same goes for constructors, which look almost like methods. Here’s one for LinkedListNode<T>
. As a bonus, it also includes a generically-typed property (get
and set
):
{ "Name": "System.Collections.Generic.LinkedListNode`1", "GenericParams": [ { "Types": [ "System.String" ] }, { "Types": [ "System.Int32" ] } ], "Constructors": [ { "ParamTypes": [ "T" ] } ], "Properties": [ "Value" ] }
Note again the `1
at the end of the LinkedListNode
type name.
The constructor takes a T
, which is the one generic type for the class. So we’ll end up generating several things with this little bit of JSON:
LinkedListNode<string>
andLinkedListNode<int>
classesLinkedListNode(string)
andLinkedListNode(int)
constructorsstring Value { get; set; }
andint Value { get; set; }
properties
Again, using this from C++ looks very similar to C#:
LinkedListNode<string> stringNode("string node value"); String value = stringNode.GetValue(); stringNode.SetValue("new node value");
There aren’t many generic field types, but I did manage to find one:
{ "Name": " System.Runtime.CompilerServices.StrongBox`1", "GenericParams": [ { "Types": [ "System.String" ] } ], "Constructors": [ { "ParamTypes": [ "T" ] } ], "Fields": [ "Value" ] }
This is just like with properties above and is used the same way:
StrongBox<String> box("secret"); String value = box.GetValue(); box.SetValue("new secret");
Lastly, we can also derive from a generic class. That’s also hard to find, but here’s one I found:
{ "Name": "System.Collections.ObjectModel.Collection`1", "GenericParams": [ { "Types": [ "System.Int32" ] } ] }, { "Name": "System.Collections.ObjectModel.KeyedCollection`2", "GenericParams": [ { "Types": [ "System.String", "System.Int32" ] } ] }
In this case we have KeyedCollection<TKey, TItem>
(note the `2
for the two generic types) which derives from Collection<TItem>
(`1
for just one generic type). So these two types will be available, albeit empty in this example, in C++:
void Foo(KeyedCollection<string, int32_t> collection) { // ... do something with the collection }
That’s about all there is for generics support. At this point we can expose mostly any generic syntax piece to C++: types, methods, constructors, fields, properties, and base classes. Now let’s talk about some miscellaneous other recent changes to the C++ scripting system.
One annoyance before was the need to break down the JSON into per-assembly parts and specify the full path to the assembly. That’s gone now and we can simply specify all our types in the same Types
block. So which assembly does the code generator look for the types in? Previously we had to specify the full path to the assembly, but now there are seven default assemblies that will be searched:
- .NET’s mscorlib.dll (e.g.
string
) - .NET’s System.dll (e.g.
Uri
) - .NET’s System.Core.dll (e.g.
Action
) - UnityEngine.dll (e.g.
Vector3
) - UnityEditor.dll (e.g.
EditorPrefs
) - The project’s runtime scripts in the
Assets
directory - The project’s editor scripts in the
Assets
directory
If we want to expose something from another DLL, there’s now an Assemblies
block that’s just an arry of full paths to DLLs. That section has support for some built-in variables so we don’t need to know the full paths to the most common, sometimes machine-specific, directories:
UNITY_PROJECT
– The Unity projectUNITY_ASSETS
– The Unity project’sAssets
directoryDOTNET_DLLS
– Unity’s directory that holds .NET/Mono DLLs (e.g. mscorlib.dll)UNITY_DLLS
– Unity’s directory that holds its DLLs (e.g. UnityEngine.dll)
So for example, we can now specify our custom DLLs in the code generator JSON like this:
"Assemblies": [ "DOTNET_DLLS/System.Xml.dll", "/path/to/my/Custom.dll" ]
That would allow us to specify types in the JSON to use the .NET XML library and our Custom.dll library.
Finally for today, there’s one simple tweak to System::Boolean
on the C++ side that should make life a little easier. Previously it was simply an alias for int32_t
since C#’s bool
type is four bytes large. Now it’s a struct
that holds an int32_t
, which means it’s still four bytes large but it can gain much more functionality. The primary goal is to support compatibility with the built-in bool
type in C++. That’s easy to do by implementing enough special elements of a C++ struct/class:
struct Boolean { // The only field is the 4-byte value for C#'s bool (System.Bool) int32_t Value; // Default to false: Boolean flag; Boolean() : Value(0) { } // Copy constructor: Boolean flag(Boolean()); Boolean(const Boolean& other) : Value(other.Value) { } // Construct from a bool: Boolean flag(false); Boolean(bool value) : Value((int32_t)value) { } // Convert to a bool: // 1) bool flag = Boolean(); // 2) if (Boolean()) operator bool() const { return (bool)Value; } // Equality with other Booleans: if (Boolean() == Boolean()) bool operator==(const Boolean other) const { return Value == other.Value; } // Inequality with other Booleans: if (Boolean() != Boolean()) bool operator!=(const Boolean other) const { return Value != other.Value; } // Equality with bools: if (Boolean() == false) bool operator==(const bool other) const { return Value == other; } // Inequality with bools: if (Boolean() != false) bool operator!=(const bool other) const { return Value != other; } };
This is a small thing, but it makes coding life a little smoother in C++ since System::Boolean
and bool
are now essentially interchangeable. For example, we can use the implicit conversion to and from bool
with some functions in the C# Assert
class:
// Implicitly convert the System::Boolean return type to a bool // Then apply the ! operator if (!Assert::GetRaiseExceptions()) { // Implicitly convert the "true" bool to a System::Boolean Assert::SetRaiseExceptions(true); }
That’s all for today. As usual, the GitHub project has been updated with all of this if you want to take a look or put any of this to use. Feedback is welcome in any form: comments on this page, GitHub issues, pull requests, or e-mail. Feel free to drop me a line and let me know what you think of the project and this series so far.