C++ Scripting: Part 19 – Implement C# Interfaces with C++ Classes
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
- 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
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:
IComparer<T>
, an interface whereT
isint
andstring
StringComparer
, an abstract classEventArgs
, a non-abstract class whosevirtual 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.