The series to build a viable system to write Unity scripts in C++ continues! While these 11 articles have covered a lot of ground toward making a usable C++ scripting system, there’s still a lot to do. Writing the code for these articles takes quite a lot of time, so today I’m officially calling for collaborators on the GitHub project. If you’d like to join in, please leave a comment, send an e-mail, or submit a pull request. There’s plenty to do and your help would be greatly appreciated! Aside from that, today’s article is all about adding support for struct and enum types so we can use types like Vector3 and TextureFormat from our C++ scripts.

Table of Contents

Again, I’m officially calling for collaborators on the GitHub project. If you’d like to join in, please leave a comment, send an e-mail, or submit a pull request. There’s plenty to do and your help would be greatly appreciated!

Even if you’re not interested in contributing, I’d appreciate it if you’d take just a few seconds to answer this poll about the project. There are only three simple questions. Poll closed. Thanks for responding!

Moving on, let’s talk about what’s been added to the project this week. The big new additions this week are support for struct and enum types, including all the bells and whistles that were supported for classes. That means we can use generics to get access to KeyValuePair<TKey, TValue> and out parameters so we can call Physics.Raycast.

As it turn out, there are many kinds of types in C# and we need to support all of them to have a viable scripting system in C++. Here’s a list:

  • Classes
  • Structs that have class fields
  • Structs that don’t have class fields
  • Enums
  • Primitives
  • Pointers
  • decimal

There may actually be more classifications of types, but that’s a good list for now. So far we’ve talked about exposing classes to C++. We’ve also talked about how to use “structs that don’t have class fields” and primitives as parameters and return values, but never actually exposed them to C++. Today we’ll talk about that and how to expose enums.

First, let’s coin two terms. Structs that have class fields are called “managed structs”. Structs that don’t have class fields are called “full structs”. There might be more appropriate names than these, but they’ll suffice for the purposes of this article. Previously, we treated all primitives and full structs as being trivially copyable between C# and C++. We didn’t need an object handle to reference them. We could just pass the whole thing and make a copy.

That assumption is no longer valid once we want to start supporting managed structs. Consider a struct like RaycastHit that has a Transform field. We can’t simply copy this struct between C# and C++ for the same reason we can’t simply copy classes. The presence of a class field means we need a workaround. One option is to make a middle-man struct that replaces the class field with an object handle just like we did with classes:

struct RaycastHitMiddleMan
{
	// Non-class types are as normal
	public Vector3 point;
 
	// Class types are object handles
	public int transform;
}

To pass a managed struct from C# to C++, we’d need C# to make a middle-man struct, copy all non-class fields to it, make an object handle for all class fields, and set those object handles in the middle-man struct. Then on the C++ side, we’d need to make a “real” version of the struct that has nice types like Transform instead of object handles and essentially “unpack” the middle-man struct into it by doing the reverse of what C# did.

That’s a lot of copying work every time we want to pass one of these structs across the language boundary. This copying would need to be done recursively as the struct may contain another struct that has a class field. So instead, we’ll take another path. Just like how we had an ObjectStore to store class instances for future access with an object handle, we’ll add a new StructStore<T> type. This will hold structs of type T and allow access to them via int handles. The C++ side will simply hold a handle to the struct, just like with a class.

Full structs, on the other hand, will be fully defined in C++. The code generator can write this code for us because it knows all the fields that the struct has. That means frequently-used struct types like Vector3 and Quaternion don’t require a StructStore and can be modified entirely on the C++ side without calling into C#. Of course calling methods, constructors, and properties still requires calling these C# functions.

Similarly to full structs, enums can be defined on the C++ side with all of their enumerators and base type intact. For example, consider the QueryTriggerInteraction enum that’s passed to Physics.Raycast. The C++ equivalent is simple to generate:

namespace UnityEngine
{
	// "enum struct" behaves like C#'s struct
	// Size of the struct is given by its base type: int32_t/int
	enum struct QueryTriggerInteraction : int32_t
	{
		// All enumerators generated with their appropriate values
		UseGlobal = 0,
		Ignore = 1,
		Collide = 2
	};
}
 
// Example usage code
QueryTriggerInteraction qti = QueryTriggerInteraction::UseGlobal;

Now that we’re supporting five kinds of types (classes, managed structs, full structs, enums, primitives), we need to map out how we’re going to pass these as parameters to C# from C++. Here’s a table showing that:

Type Is Out? Is Ref? C++ OOP Param C++ Binding Param C# Binding Param
Full Struct Yes No Type* Type* out Type
Full Struct No Yes Type* Type* ref Type
Full Struct No No Type& Type& ref Type
Primitive Yes No Type* Type* ref Type
Primitive No Yes Type* Type* ref Type
Primitive No No Type Type Type
Enum Yes No Type* Type* ref Type
Enum No Yes Type* Type* ref Type
Enum No No Type Type Type
Managed Struct Yes No Type* int32_t* ref int
Managed Struct No Yes Type* int32_t* ref int
Managed Struct No No Type int32_t int
Class Yes No Type* int32_t* ref int
Class No Yes Type* int32_t* ref int
Class No No Type int32_t int

No two kinds of types are quite alike. In the end, we have a C++ API that’s quite “normal” when you’re used to C#. Passing an out or ref parameter requires a pointer (&myVariable) so it’s opt-in like in C# where you have to type out myVariable. Otherwise, you can simply pass the parameter and the system will handle it regardless of what kind of type it is. Here’s an example using some structs in C++:

// Call a constructor
Vector3 vec(1.0f, 2.0f, 3.0f);
 
// Read and write full struct fields without calling C#
vec.x = vec.y * 2.0f;
 
// Call a method
vec.Set(10.0f, 20.0f, 30.0f);
 
// Use generics with a managed struct type
KeyValuePair<String, int32_t> pair("key", 123);
 
// Use a "get" property
String key = pair.GetKey();

As designed, this C++ code looks very similar to C# code. Actually, the only difference is calling .GetKey() instead of accessing the property like a field with .Key. All of the complexity is taken care of for you behind the scenes.

There are some Type& parameters in the above table, which means “reference” in C++. This isn’t like a managed reference in C#. There’s no garbage collector or tracking whether you still have a reference to the object. Instead, it’s basically just like a pointer except it can’t be null, you don’t need to type &myVariable to pass one, and you don’t need to type *myVariable to get the struct back from one. It’s not suitable for class types since we need them to be null sometimes, but structs can’t be null so we’re free to use them there. The advantage over pointers is that our C++ code can look more like C# by avoiding the need for & and awkwardness of what to do about a “null struct” that should be an impossibility.

All this use of pointers (*) and references (&) behind the scenes means that we’re not making any additional copies of structs. We still have to make one copy to pass the struct as a non-out, non-ref parameter, but that’s required by the function’s API. Passing pointers and references in the binding code between C# and C++ means we don’t need to make copies for these internal function calls.

Now that we have support for generating struct types, there’s no need to hand-code a Vector3 type on the C++ side. We can simply generate it if we want to use it. A 2D game might only generate Vector2 instead, which will also work just fine.

And that’s about all there is to structs and enums for this article. We’ve still got to support some remaining types like decimal and C# pointers, but those are both very infrequently-used in games so they can wait for now.

If you’re interested in collaborating on the project, please leave a comment, send an e-mail, or submit a pull request.