C++ Scripting: Part 14 – Arrays
The series continues by adding support for a major feature: arrays. These are used very frequently throughout the Unity and .NET APIs and the lack of support for them has been a big missing piece of the puzzle for most games. The GitHub project has been updated to support single- and multi-dimensional arrays. Read on to learn how this support was implemented!
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
Conceptually, adding support for arrays sounds easy but there are a lot of little details to handle. Today we’ll walk through them one by one.
First of all, we’re talking about managed arrays today. These are the array types we have in C#, such as int[]
and string[]
. Though they have a similar syntax, we’re not talking about unmanaged arrays like we have in C++. There’s simply nothing to do to support unmanaged arrays, so we’ll focus on using managed arrays in the following ways:
- Array constructors:
new int[5]
- Array properties:
a.Length
anda.Rank
- Array methods:
a.GetLength(5)
- Array indexing:
x = a[5]
anda[5] = x
- Arrays as parameters:
void Foo(int[] a)
andMyClass(int[] a)
- Arrays as
out
parameters:void Foo(out int[] a)
- Arrays as fields:
class MyClass { int[] A; }
- Arrays as properties:
int[] A { get; set; }
It may seem like we already have support for all of this. After all, arrays are just a class with an overloaded array index operator, some properties, and some methods, right? Unfortunately, no. It turns out that these similarities are only superficial and there are many differences that need to be accounted for, especially syntactically. They’re so different from struct and class types that they need their own section of the code generator’s JSON config file:
{ "Arrays": [ { "Type": "System.Int32" }, { "Type": "System.Single", "Ranks": [ 1, 2, 3 ] } } }
Here we specify the types we want to expose to C++ as arrays. Optionally, we can specify the “rank” of the array to get access to 1D, 2D, 3D arrays, etc. By default, we just get a 1D array which is by far the most common. So the above JSON says that we want C++ to have access to int[]
, float[]
, float[,]
(2D), and float[,,]
(3D).
Next, we’ll need to establish a C++ type for arrays. We can’t use int[]
because that already has a specific meaning in C++: unmanaged array. Instead, we’ll use the System.Array
name on the C++ side like this:
// Base class of all arrays // Not generic, just like in C# // Provides basic properties: Length and Rank struct Array : Object { int32_t GetLength(); int32_t GetRank(); }; // Arrays of various dimensions template <typename TElement> Array1; template <typename TElement> Array2; template <typename TElement> Array3; template <typename TElement> Array4; template <typename TElement> Array5; // Specific array types output by the code generator template<> struct Array3<float> : System::Array { // Construct with a given length, like "new float[length0, length1, length2]" Array3(int32_t length0, int32_t length1, int32_t length2); // Length property, uses the base Array class int32_t GetLength(); // Get the length of a specific dimension int32_t GetLength(int32_t dimension); // Rank property, uses the base Array class int32_t GetRank(); // Get an item at given indexes, like "x = array[index0, index1, index2]" float GetItem(int32_t index0, int32_t index1, int32_t index2); // Get an item at given indexes, like "array[index0, index1, index2] = x" void SetItem(int32_t index0, int32_t index1, int32_t index2, float item); } // Basic usage Array1<String> array(10); String elem("howdy"); array.SetItem(5, elem); String item = array.GetItem(5); Debug::Log(item); // prints "howdy"
This seems like a reasonable alternative to the int[]
syntax we use in C#. Instead, it’ll feel more like generic collections such as List<T>
. We also have access to all the Array
base class functionality and polymorphism so we can pass any array type (e.g. Array1<float>
) as an Array
, just like in C#.
With this basis in place we can start implementing the code generator to output these array types. This is all based on .NET’s reflection functionality in System.Reflection
and it’s well-equipped to handle array types. There are handy properties like Type.IsArray
to identify arrays as method parameters, constructor parameters, properties, and fields. One important difference between arrays and other types (e.g. classes) is that they have different names in C# and C++. We simply have to call them int[]
in C# and have to not call them int[]
in C++. So when we encounter a Type.IsArray
we can output one name for C# (int[]
) and another for C++ (Array1<int32_t>
.
The reflection functionality also provides us with Type.MakeArrayType()
and Type.MakeArrayType(int)
to make an array Type
out of an element Type
. So we can take the System.Single
specified in JSON, get the Type
for it, and then call Type.MakeArrayType
to get the Type
for float[]
. That’s really handy since it allows our chosen JSON config format of specifying the type (System.Single
) and the ranks [1, 2, 3]
.
One tricky aspect is that Type.MakeArrayType()
and Type.MakeArrayType(1)
are not synonymous. Consider the following snippet:
Debug.Log(typeof(float).MakeArrayType().Name); // prints "float[]" Debug.Log(typeof(float).MakeArrayType(1).Name); // prints "float[*]"
Ther reason is that single-dimensional arrays like float[]
are actually “vectors” in .NET. They get special treatment in the bytecode compared to multi-dimensional arrays that happen to have only one dimension. So float[*]
indicates a multi-dimensional array that has only one dimension but that is not the same as “vector” which is what the float[]
means. Unfortunately, this means that the Name
here is invalid C# since we can’t write float[*]
.
There are more details to supporting arrays, but they’re mostly only important when working on the code generator. Instead of diving into those details, let’s look at some examples to see how to use arrays from our C++ scripts. First, let’s look at a little snippet that calls a function returning an array and then loops over it with a for
loop.
// Call a function returning an array Array1<Resolution> resolutions = Screen::GetResolutions(); // Loop over an array as usual for (int32_t i = 0, len = resolutions.GetLength(); i < len; ++i) { // Get the current element of the array Resolution resolution = resolutions.GetItem(i); }
Now let’s try using an array as a parameter:
// Create a ray Vector3 origin(-10, 0, 0); Vector3 direction(1, 0, 0); Ray ray(origin, direction); // Create an array to hold the results Array1<RaycastHit> results(10); // Pass the array as a parameter int32_t numHits = Physics::RaycastNonAlloc(ray, results);
Here’s a snippet to use an array property as both a “getter” and a “setter”:
// Create an array and set one of the elements Array1<GradientColorKey> colors(4); GradientColorKey colorKey; colorKey.color.r = 1; colorKey.color.g = 0; colorKey.color.b = 0; colors.SetItem(0, colorKey); // Use the property "setter" Gradient gradient; gradient.SetColorKeys(colors); // Use the property "getter" Array1<GradientColorKey> gotColors = gradient.GetColorKeys();
Finally, let’s use a multi-dimensional array:
// Make a multi-dimensional array of floats Array2<float> array(2, 3); // Get its overall length (6) from the Length property int32_t overallLength = array.GetLength(); // Get its rank (2) from the Rank property int32_t rank = array.GetRank(); // Loop over the dimensions of the array for (int32_t i = 0; i < rank; ++i) { // Get the length of the current dimension (2 then 3) int32_t len = array.GetLength(i); } // Set the element at [1, 2] array.SetItem(1, 2, 3.14f); // Get the element at [1, 2] float item = array.GetItem(1, 2);
That’s about all for arrays for today. There are some improvements to be added in the future, such as supporting the subscript operator, but this functionality should be enough for almost any game. The above code snippets show how we now have access to important APIs like Physics.Raycast
in Unity and many more in .NET. We are, once again, one step closer to a viable C++ scripting environment. At this point, there aren’t even many major items left on the list to implement. Still, we’ll press on in next week’s article with further enhancements to the system.
Check out the GitHub project to get access to the array functionality or to read through the details of how it was implemented. If you’re interested in collaborating, please leave a comment or e-mail me.