C++ Scripting: Part 21 – Implement C# Properties and Indexers in C++
Part 19 of this series started to allow our C++ game code to derive from C# classes and implement C# interfaces. The first step was to override methods as they’re the most common. Today we’ll tackle the second-most common: properties. We’ll also handle indexers, which are like properties with more parameters. Read on to see how to use this and how it works behind the scenes.
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
Let’s start by seeing how to override properties and indexers in C++. In the code generator’s JSON config, add to the BaseTypes
section:
{ "BaseTypes": [ { "Name": "System.Collections.IList" } ] }
The Name
here can be any interface or non-sealed
class. If it’s an interface, this is all there is to the config. All the interface methods, properties, and indexers will be generated since we have to override them all. If it’s a class, all the abstract methods, properties, and indexers will be generated. To override virtual methods, properties, and indexers that aren’t abstract, just add one more section saying which ones to allow overriding:
{ "BaseTypes": [ { "Name": "System.Collections.Queue", "OverrideProperties": [ { "Name": "Count", "Get": {}, "Set": {} } ] } ] }
This will allow C++ to derive from the Queue
class and override the virtual Count
function even though it’s not required that we override it because it’s not abstract.
Let’s say we ran the code generator on the IList
above. It’ll output a base class in C++:
namespace System { namespace Collections { struct IList : System::Object { // Various class lifecycle functions IList(); IList(decltype(nullptr) n); IList(Plugin::InternalUse iu, int32_t handle); IList(const IList& other); IList(IList&& other); virtual ~IList(); IList& operator=(const IList& other); IList& operator=(decltype(nullptr) other); IList& operator=(IList&& other); bool operator==(const IList& other) const; bool operator!=(const IList& other) const; // C#'s handle to the C++ instance int32_t CppHandle; // All the interface methods and properties from IList virtual int32_t Add(System::Object& value); virtual void Clear(); virtual System::Boolean Contains(System::Object& value); virtual int32_t IndexOf(System::Object& value); virtual void Insert(int32_t index, System::Object& value); virtual void Remove(System::Object& value); virtual void RemoveAt(int32_t index); virtual System::Collections::IEnumerator GetEnumerator(); virtual void CopyTo(System::Array& array, int32_t index); virtual System::Boolean GetIsFixedSize(); virtual System::Boolean GetIsReadOnly(); virtual System::Object GetItem(int32_t index); virtual void SetItem(int32_t index, System::Object& value); virtual int32_t GetCount(); virtual System::Boolean GetIsSynchronized(); virtual System::Object GetSyncRoot(); }; } }
Notice that the code generator created C++ methods for the interface methods of IList
and now the properties and indexers too. So there are methods like Add
, properties like Count
(via GetCount
and SetCount
) and indexers like Item
(via GetItem
and SetItem
). Only the required get
and set
accessors are generated for properties and indexers.
Another addition as of this week is that the full hierarchy of interfaces is now generated. That means that the code generator will now output methods, properties, and indexers for IList
plus its base interfaces ICollection
and IEnumerable
. That means methods like GetEnumerator
were generated for IEnumerable
and properties like Count
were generated for ICollection
.
The next step is to derive from this generated class in C++ game code:
struct MyList : IList { virtual int32_t GetCount() override { // TODO: implement return 0; } Object GetItem(int32_t index) { // TODO: implement return nullptr; } void SetItem(int32_t index, Object& value) { // TODO: implement } // ... override the rest of the methods, properties, and indexers };
Now C++ game code can use MyList
:
// Make a list MyList list; // Pass it to C# SomeCsharpClass.SomeCsharpMethod(list);
That’s about all there is to using the system. It should be easy and familiar to anyone who’s derived from classes or implemented interfaces in C#.
Now let’s look a bit at how this is implemented behind the scenes. C# properties are really just syntax sugar for two “accessor” functions. Consider this class:
public class Person { private string name; public string GetName() { return name; } public void SetName(string value) { name = value; } } void Foo() { Person p = new Person(); p.SetName("Jackson"); string name = p.GetName(); // name == "Jackson" }
This code is extremely common in languages like Java and C++. In C# it was built into the language so these “get” and “set” functions, collectively “accessors”, can appear like any other field. Here’s how it looks:
public class Person { private string name; public string Name() { get { return name; } set { name = value; } } } void Foo() { Person p = new Person(); p.Name = "Jackson"; string name = p.Name; // name == "Jackson" }
It’s really just syntax sugar built around a special case for accessors where the “get” method takes no parameters and the “set” method takes just one parameter of the variable’s type. There are also “automatically-implemented properties” like this:
public class Person { public string Name { get; set; } } void Foo() { Person p = new Person(); p.Name = "Jackson"; string name = p.Name; // name == "Jackson" }
In this case the compiler generates the “backing field” (i.e. name
) and trivial “get” and “set” accessor blocks of the property just like we had written manually before. Again, this is all just syntax sugar.
Indexers are like properties that take more parameters. For example, here’s one that let’s us “index into” a 3D vector:
public class Vector3 { private float x; private float y; private float z; public float this[int index] { get { switch (index) { case 0: return x; case 1: return y; case 2: return z; default: throw new Exception("Out of bounds"); } } set { switch (index) { case 0: x = value; case 1: y = value; case 2: z = value; default: throw new Exception("Out of bounds"); } } } } void Foo() { Vector3 v = new Vector3(); v[1] = 3.14f; float y = v[1]; // y == 3.14f }
This is really analogous to properties and actually how properties are implemented in .NET. Think of them like a normal, non-indexer property that takes extra parameters. It’s called by the array subscript operator (x[a, b, c]
) rather than looking like a field (x.y
).
Because this is all syntax sugar built up around regular old methods, the code generator can essentially use the same strategy as it did in part 19 with methods. It generates a class with a property or indexer that calls into C++ to do the real work:
class SystemCollectionsIList : IList { // Handle to the C++ object private int cppHandle; public int Count { get { // Call into C++ return SystemCollectionsIListGetCount(cppHandle); } } public object this[int index] { get { // Call into C++ int handle = SystemCollectionsIListGetItem(cppHandle, index); return ObjectStore.Get(handle); } set { // Call into C++ int handle = ObjectStore.GetHandle(value); SystemCollectionsIListSetItem(cppHandle, index, handle); } } }
Then on the C++ side these “binding” functions use the cppHandle
to look up the C++ object and call its methods:
int32_t SystemCollectionsIListGetCount(int32_t cppHandle) { return GetSystemCollectionsIList(cppHandle)->GetCount(); }
And with that, we have support for overriding properties and indexers in C++. Properties are quite common in .NET interfaces and classes, so supporting them is an important step to using C++ with an even wider range of C# APIs. Indexers are much less common, but still occasionally used. Events are the last remaining type of “function” to override in interfaces and classes and that’ll come in a future article. For now, check out the GitHub repo to try out C++ properties and indexers or dig into the source code that implements it.