Implementing interfaces and deriving from classes is commonplace in many codebases. Today we’ll make it so C++ classes can implement C# interfaces and derive from C# classes. This means our C++ game code will be able to implement custom IComparer classes for sorting a List and derive custom EventArgs for dispatching in events. Read on to see how this is implemented and how to use it in our projects.

Table of Contents

The core concept that allows us to implement C# interfaces and derive from C# classes was introduced in part 15 with support for delegates. In that article we created a C# class that forwarded the delegate invocation to a function in C++. Here’s a very brief version of what such a class looked like:

// C# class to hold the delegate and forward to C++
class ActionFloat
{
	// The delegate. It's implemented by Invoke().
	Action<float> del = Invoke;
 
	// Handle to the C++ class to invoke
	int cppHandle;
 
	// Function that the delegate calls
	void Invoke(float val)
	{
		// Forward to a C++ function
		CppInvoke(cppHandle, val);
	}
}

On the C++ side there was a base class for game code to derive from:

// Base class of all Action<float> delegates
template<> struct Action1<float>
{
	// Called when the delegate is invoked
	virtual void operator()(float val)
	{
	}
};

Then game code could derive from this to specialize the delegate:

struct MyFloatAction : Action1<float>
{
	void operator()(float val) override
	{
		Debug::Log("MyFloatAction was invoked");
	}
};

We’ll build on this foundation today. The first step is to allow for the C# class to implement an interface or derive from a class. Next, it should override every abstract function in the base interface or class plus whatever virtual functions the game code would like to override. We can also remove the delegate part, since that’s not required. The class ends up looking like this:

// C# class to implement the interface and forward to C++
class IComparerFloat : IComparer<float>
{
	// Handle to the C++ class to invoke
	int cppHandle;
 
	// Function defined in the interface
	int Compare(float a, float b)
	{
		// Forward to a C++ function
		return CppInvoke(cppHandle, a, b);
	}
}

This will result in a C++ base class like this:

// Base class of all IComparer classes
template<> struct IComparer<float> : Object
{
	// Called when the method is invoked
	virtual int32_t Compare(float a, float b) {}
};

Note that, unlike with delegates, the C++ base class does not need a second handle to refer to the delegate and the class containing it. A single handle (stored in Object as Handle) is enough to reference the C# class.

Finally, game code can implement this like so:

struct MyFloatComparer : IComparer<float>
{
	int32_t Compare(float a, float b) override
	{
		if (a < b)
		{
			return -1;
		}
		else if (b > a)
		{
			return 1;
		}
		return 0;
	}
};

Everything looks exactly the same for abstract classes and their abstract and virtual functions except that the C# class adds the override keyword.

Conceptually, there’s very little required to support this functionality in C++. Implementing it in the code generator is another story, though. Currently there is only support for methods, not properties, indexers, or events. Base classes must also have a default constructor for now.

Now let’s see how to use this in a game project. First, there’s a new section of the code generator’s JSON config file:

"BaseTypes": [
	{
		"Name": "System.Collections.Generic.IComparer`1",
		"GenericParams": [
			{
				"Types": [
					"System.Int32"
				]
			},
			{
				"Types": [
					"System.String"
				]
			}
		]
	},
	{
		"Name": "System.StringComparer"
	},
	{
		"Name": "System.EventArgs",
		"OverrideMethods": [
			{
				"Name": "ToString",
				"ParamTypes": []
			}
		]
	}
],

This section tells the code generator to generate three base classes for C++ game code to derive from:

  1. IComparer<T>, an interface where T is int and string
  2. StringComparer, an abstract class
  3. EventArgs, a non-abstract class whose virtual ToString is forwarded to C++

Let’s try out IComparer<T> first:

// Derive from IComparer<int>
struct MyComparer : IComparer<int32_t>
{
	// Implement the required function
	int32_t Compare(int32_t x, int32_t y) override
	{
		Debug::Log(String("Compare called in C++!"));
		return x - y;
	}
};
 
// Make a list of int
Collections::Generic::List<int32_t> list;
list.Add(3);
list.Add(1);
list.Add(2);
 
// Sort the list with a MyComparer
MyComparer myc;
list.Sort(myc);
 
// Print the sorted list: 1, 2, 3
Debug::Log(list.GetItem(0));
Debug::Log(list.GetItem(1));
Debug::Log(list.GetItem(2));

This is really useful for complying with various .NET and Unity APIs that require us to pass an interface to provide some custom functionality. Sorting a List is a common example of this and there are many other such places where this will come in handy.

Next, let’s look at deriving from an abstract class: StringComparer

struct MyComparer : StringComparer
{
	int32_t Compare(String x, String y) override
	{
		Debug::Log(String("Compare called"));
		return 1;
	}
 
	System::Boolean Equals(String x, String y) override
	{
		Debug::Log(String("Equals called"));
		return true;
	}
 
	int32_t GetHashCode(String obj) override
	{
		Debug::Log(String("GetHashCode called"));
		return 0;
	}
};

This works just like with interfaces, except that there are more abstract functions that need to be implemented in this case.

Finally, let’s look at deriving from a non-abstract class: EventArgs

struct MyEventArgs : EventArgs
{
	String ToString() override
	{
		return "Some Custom String";
	}
};

In this case there were no abstract functions to override. However, we specified a virtual function—ToString—that we’d like to be able to override. This makes it available in the base class for overriding in our game code.

Using this functionality is easy. It should feel very familiar to C# programmers except for minor differences like putting override at the end instead of the beginning. It’s also easy to configure the code generator to generate just the base classes and interfaces you want.

Check out the GitHub project for the full details or to try it out. Support for interfaces and base classes, though still somewhat limited, is one more helpful step toward a full C++ scripting environment.