C++ Scripting: Part 22 – Full Base Type Support
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
- 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
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.