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

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.