Today we’ll complete our ability to use C++ classes to derive from C# classes and implement C# interfaces. So far we’ve been able to override methods, properties, and indexers. Today we’ll add the ability to override events and derive from classes that don’t have a default constructor.
Those are the last two pieces of the puzzle that will allow us to derive from any C# base type with a C++ class. Read on for all the details about how this works.

Table of Contents

As discussed in part 16, events are really just syntax sugar for two methods to add and remove a delegate. We can see this explicitly when we specify those two methods using events’ odd syntax:

namespace UI
{
	public class Button
	{
		public virtual event Action OnClicked
		{
			add
			{
				// the "add" method
			}
			remove
			{
				// the "remove" method
			}
		}
	}
}

This means we can forward the “add” and “remove” methods to C++ just like we did with properties and indexers in part 21:

// Class to derive from Button so we can override in C++
public class UIButton : Button
{
	// Handle to the C++ class
	int CppHandle;
 
	// Override the event
	public override event Action OnClicked
	{
		add
		{
			// Forward to C++
			UIButtonOnClickedAdd(CppHandle);
		}
		remove
		{
			// Forward to C++
			UIButtonOnClickedRemove(CppHandle);
		}
	}
}

Then in C++ the bindings function looks up the class by its handle and calls the appropriate method:

void UIButtonOnClickedAdd(int32_t cppHandle)
{
	GetUIButton(cppHandle)->AddOnClicked();
}
 
void UIButtonOnClickedRemove(int32_t cppHandle)
{
	GetUIButton(cppHandle)->RemoveOnClicked();
}

All the C++ game code needs to do is override these methods:

struct MyButton : UI::Button
{
	void AddOnClicked() override
	{
		String msg = "adding";
		Debug::Log(msg);
	}
 
	void RemoveOnClicked() override
	{
		String msg = "removing";
		Debug::Log(msg);
	}
};

The code generator will generate all this automatically for every abstract event and every event in an interface. There may be virtual events in base classes that aren’t abstract, so we’ll need to specify which of those we want to override. We’ll do that the same way as with methods, properties, and indexers. We just specify a simple section in the code generator’s JSON config file:

{
	"BaseTypes": [
		{
			"Name": "UI.Button",
			"Events": [
				{
					"Name": "OnMouseDown"
				}
			]
		},
	]
}

This tells the code generator to generate override code for the OnMouseDown event in UI.Button.

We now have support to override methods, properties, indexers, and events. That’s the last of the types of virtual items that can be in a C# interface or class, which means we can now override whatever is required of us by a C# interface or abstract keyword. We can also optionally override any virtual items in a base class. There’s still one big limitation though: we need our base classes to have a default constructor. So let’s remedy that now.

Just like with normal types we want C++ to be able to call, we’ll specify the constructors we want our derived types to be able to call via the code generator’s JSON config file:

"BaseTypes": [
	{
		"Name": "System.IO.FileStream",
		"Constructors": [
			{
				"ParamTypes": [
					"System.String",
					"System.IO.FileMode"
				]
			}
		]
	}
]

FileStream has no default constructor, so specifying this one means we’re able to derive from it. Constructors is an array, so we can specify as many constructors as we want our derived C++ classes to be able to call. If no constructors are specified, the code generator just generates a default constructor.

Now let’s look at what gets generated when we specify constructors in the config JSON, starting with the C++ class:

struct FileStream : System::IO::Stream
{
	FileStream(System::String& path, System::IO::FileMode mode);
 
	// ... other contents
};

Having this constructor available makes it easy for game code to derive and call that constructor:

struct MyFileStream : FileStream
{
	MyFileStream(System::String& path, System::IO::FileMode mode)
		: FileStream(path, mode) // call base class constructor
	{
		String msg = "opening a file stream";
		Debug::Log(msg);
	}
};

The constructor is implemented just like the default constructor for derived types, except that it has more parameters:

FileStream::FileStream(System::String& path, System::IO::FileMode mode)
{
	// Store this object and get a handle to it
	CppHandle = StoreFileStream(this);
 
	// Call the constructor
	SystemIOFileStreamConstructor_String_FileMode(
		cppHandle, // tell C# how to reference this class
		&Handle, // 'ref' parameter for C# to store the C# handle in
		path.Handle, // constructor parameters...
		mode);
}

On the C# side, this is also implemented like the default constructor:

static void SystemIOFileStreamConstructorSystemString_SystemIOFileMode(
	int cppHandle,
	ref int handle,
	int pathHandle,
	System.IO.FileMode mode)
{
	// Get parameters from their handles
	var path = (string)ObjectStore.Get(pathHandle);
 
	// Call the constructor
	var thiz = new SystemIOFileStream(cppHandle, path, mode);
 
	// Store the new object and 'return' the handle
	handle = ObjectStore.Store(thiz);
}

Finally, the C# class gets a constructor that passes through these parameters:

class SystemIOFileStream : FileStream
{
	// Handle to the C++ class
	public int CppHandle;
 
	// Constructor taking the requested parameters
	public SystemIOFileStream(
		int cppHandle,
		string path,
		System.IO.FileMode mode)
		// Call the specified constructor
		: base(path, mode)
	{
		CppHandle = cppHandle;
	}
}

This is a generalization of the existing default constructor functionality to support more parameters. It allows our C++ game code to extend a lot more classes since default constructors are no longer required. We can optionally specify a default constructor though if we want to sometimes call it and sometimes call constructors with more parameters.

With these new features implemented, we now have full support for base types: classes and interfaces. If we ever encounter a C# API where we need to derive from a class or implement an interface, we now have the tools to do so. As usual, this is all available now on the GitHub project, so take a look if you’re interested in using it or seeing all the details about how the code generator was implemented.