C++ Scripting: Part 12 – Exceptions
Like them or not, exceptions are the standard way of handling programming errors in C#. We need to be able to catch C# exceptions in our C++ code so we can gracefully recover from them. Likewise, we need uncaught C++ exceptions to behave like unhandled C# exceptions: display an error and move on instead of crashing. Today’s article continues the series by implementing both those features in the GitHub project and explaining how that implementation was done.
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
Before we start to talk about exceptions, let’s have a quick followup to the poll in last week’s article. Thank you to everyone who took the time to fill it out, even if it was just a few seconds. If you still haven’t filled it out, it’s still open so you can still submit your opinions.
The poll results were very encouraging! The majority of you are interested in reading more about C++ scripting, so I’ll continue on starting with today’s article. A little over half of you would write at least some of your scripts in C++ and a few of you are even interested in collaborating on the GitHub project. If you answered that you want to collaborate on the project, please send me an e-mail, leave a comment, or otherwise get in contact with me. That said, let’s move on to exceptions…
Both C# and C++ support exceptions and they work very similarly. However, if C++ calls a C# function that throws an exception, the C# exception can’t be caught by C++. Just like other class instances in C#, exceptions can’t be simply passed between languages like we can with an integer. The internal implementation of the exceptions is also different, so the two languages are incompatible enough that we can’t simply throw in one language and catch in the other.
This means we’ll have to implement handling for catching and re-throwing both C# and C++ exceptions. Let’s start with the simpler one: C++ exceptions. Recall that C++ functions are invoked by C# for one of two reasons. First, when the plugin boots up the C++ Init
function is called to set up the plugin and then call the game code’s PluginMain
function. Second, MonoBehaviour
“messages” like Update
call into their corresponding C++ functions.
So there’s only two places we need to handle C++ exceptions. For starters, we’ll need to use try
and catch
just like in C#.
try { PluginMain(); } catch (Exception ex) { // PluginMain threw an Exception } catch (...) { // PluginMain threw something else }
Now that we’ve caught the exception, we need to pass it to the C# side. As usual for class instances, we’ll need to pass an object handle to a C# function. So let’s start by defining the C# side:
// The exception that C++ threw static Exception UnhandledCppException; // A function for C++ to call to pass C# an exception [MonoPInvokeCallback(typeof(SetExceptionDelegate))] static void SetException(int handle) { // Get the Exception corresponding to the handle // Store it in a static variable UnhandledCppException = ObjectStore.Get(handle) as Exception; }
Now we can call this from the C++ side:
try { PluginMain(); } catch (Exception ex) { // Pass the thrown exception to C# SetException(ex.Handle); } catch (...) { // Create a generic exception and pass it to C# Exception ex("Unhandled exception calling PluginMain"); SetException(ex.Handle); }
The final piece is to have C# re-throw that exception. All we need to do is check if an exception was set to UnhandledCppException
after the call to the C++ function and re-throw:
// Call a C++ function. In this case the boot function that calls PluginMain. Init(/*params*/); // Check if C++ called back during the function to set an unhandled exception if (UnhandledCppException != null) { // Make a local variable copy of the exception Exception ex = UnhandledCppException; // Clear the exception UnhandledCppException = null; // Re-throw the exception nested inside a helpful message throw new Exception("Unhandled C++ exception in Init", ex); }
To try this out, assume there’s a throw
in PluginMain
:
throw Exception("boom");
Now this won’t crash the game as normal, but instead be caught and re-thrown on the C# side. As a result, we’ll see a message like this in the editor’s Console
pane:
Exception: boom Rethrow as Exception: Unhandled C++ exception in Init NativeScript.Bindings.Open (Int32 maxManagedObjects) (at Assets/NativeScript/Bindings.cs:630) NativeScript.BootScript.Awake () (at Assets/NativeScript/BootScript.cs:21)
This works no matter what kind of exception C++ threw. So that’s basically all there is to handling uncaught C++ exceptions.
Now for the more difficult topic: handling C# exceptions. The core concept is the same as with C++ exceptions: catch and re-throw. So let’s start by defining the C++ side:
// A global variable to hold the thrown C# exception Exception unhandledCsharpException(nullptr); DLLEXPORT void SetCsharpException(int32_t handle) { // Clear any old exception delete unhandledCsharpException; // Store the new exception unhandledCsharpException = Exception(handle); }
Then we add try
and catch
to all the C# functions that C++ calls. These include constructors, methods, properties, and even fields. For example, here’s the wrapper function for GameObject.GetTransform
:
[MonoPInvokeCallback(typeof(UnityEngineGameObjectPropertyGetTransformDelegate))] static int UnityEngineGameObjectPropertyGetTransform(int thisHandle) { try { // Get "this" from the object handle var thiz = (GameObject)ObjectStore.Get(thisHandle); // Call the property "getter" var returnValue = thiz.transform; // Store the return value in ObjectStore // Return the handle it gives us return ObjectStore.GetHandle(returnValue); } catch (Exception ex) { // Store the exception in ObjectStore // Pass the object handle we get back to C++ SetCsharpException(ObjectStore.Store(ex)); // Still have to return something, so return the default return default(int); } }
So by the time this wrapper function returns it will have called the C++ SetCsharpException
function which will set the global unhandledCsharpException
variable. That means we can modify the C++ code to check if unhandledCsharpException
was set immediately after the C# function returns, just like we did before after C# called into a C++ function.
Transform GameObject::GetTransform() { // Call the C# wrapper function auto returnValue = UnityEngineGameObjectPropertyGetTransform(Handle); // Check if C# set an exception during the call if (unhandledCsharpException) { // Copy the exception to a local variable Exception ex = unhandledCsharpException; // Clear the exception unhandledCsharpException = nullptr; // Re-throw the exception throw ex; } // Still have to return something // If there was no exception, this is the real return value // If there was an exception, this is a dummy value (e.g. the default) // It's OK because this line will never execute due to the above "throw" return returnValue; }
The final step is to catch the C# exception in C++:
// Make a null GameObject GameObject nullGo(nullptr); try { // Try to call the "transform" getter property nullGo.GetTransform(); } catch (Exception ex) { // Handle the exception by logging it Debug::Log(ex); }
This actually works out really well. Catching C# exceptions is just as easy as catching them in C# code. All the complexities are handled behind the scenes so our game code can simply use the exceptions features. It’s even fine if we forget to catch a C# exception on the C++ side. To understand why, consider the following sequence:
- C# calls a C++ function like
Init
- The C++
Init
function callsPluginMain
in atry-catch
block PluginMain
in C++ calls a C# function like the GameObject.transform “getter” property- GameObject.transform throws an exception
- C# catches the exception and calls the C++
SetCsharpException
function with it - C++ re-throws the exception
- The
catch
block inInit
for the call toPluginMain
catches the exception and callsSetException
on C# - C# finishes the call to
Init
, checks its global exception variable, and re-throws it
So we’re really well covered here from both sides, even if we forget a catch
in C++.
Still, there’s one remaining issue: C++ can only receive exceptions of type Exception
. That means that even though the above code threw a NullReferenceException
we still just get a plain Exception
. There are two reasons for this.
First, SetCsharpException
constructs an Exception
object regardless of what type of exception the handle is for. So we need to work around this by defining more such SetCsharpException
functions. We need one per type that the C++ code is interested in. So let’s add on to the code generator’s JSON file to specify just that:
{ "Name": "UnityEngine.GameObject", "Properties": [ { "Name": "transform", "GetExceptions": [ "System.NullReferenceException" ] } ] }
Here we’ve said explicitly that we want to handle NullReferenceException
when calling GameObject.transform
“getter”. The same goes for “setter” properties, constructors, and methods. Fields can only throw a NullReferenceException
, so there’s no need to specify anything there.
Now that we have that specified, the code generator just needs to generate a few more bits of code. We’ll need some more catch
blocks in C# code to handle each of these exception types. When those catch
blocks are executed, they need to call a newly-generated function like SetCsharpException
. Here’s how those look:
// C# now handles the specific exception type // It passes the handle to a specific C++ function [MonoPInvokeCallback(typeof(UnityEngineGameObjectPropertyGetTransformDelegate))] static int UnityEngineGameObjectPropertyGetTransform(int thisHandle) { try { var thiz = (GameObject)ObjectStore.Get(thisHandle); var returnValue = thiz.transform; return ObjectStore.GetHandle(returnValue); } catch (NullReferenceException ex) { SetCsharpExceptionNullReferenceException(ObjectStore.Store(ex)); return default(int); } catch (Exception ex) { SetCsharpException(ObjectStore.Store(ex)); return default(int); } }
// C++ function to set a specific type of function DLLEXPORT void SetCsharpExceptionSystemNullReferenceException(int32_t handle) { delete unhandledCsharpException; unhandledCsharpException = NullReferenceException(handle); }
Now the correct type of exception is being set in C++, not just the base Exception
.
However, this still won’t work due to the second reason as mentioned above. It turns out that throw
in C++ uses static typing. So when the compiler sees a call to throw {variable of type Exception}
it doesn’t apply polymorphism to figure out that the actual variable assigned to the Exception
-type variable was a NullReferenceException
. That means it won’t go to the right catch
block even if there is one for a NullReferenceException
.
To work around this, we need to employ a couple of tricks. First, we need to be able to use throw
from a place where the correct type is known at compile time. A convenient location for this is within the exception type itself. So we can use a virtual
function and implement it in each type of exception:
struct Object { virtual void ThrowReferenceToThis() { // Throws an Object throw *this; } }; struct NullReferenceException : Object { virtual void ThrowReferenceToThis() { // Throws a NullReferenceException throw *this; } }
Now instead of re-throwing with throw unhandledCsharpException
, we call ThrowReferenceToThis
.
One final wrinkle is that we need the global variable to be a pointer, largely because the size of derived exceptions like NullReferenceException
is potentially larger than just Exception
so a non-pointer might not leave room for all its fields. So we’ll change that and use the new
and delete
operators when setting and accessing the global exception variable.
In the end we have a global variable like this:
Exception* unhandledCsharpException = nullptr;
SetCsharpException
functions that look like this:
DLLEXPORT void SetCsharpExceptionSystemNullReferenceException(int32_t handle) { delete unhandledCsharpException; unhandledCsharpException = new NullReferenceException(handle); }
But there’s even one more wrinkle to iron out! The ThrowReferenceToThis
function doesn’t exist in C#. We want to add it on in C++ only to help us throw exceptions. The code generator could be modified to add a special case for exception types, but there’s another route. Instead, we can generate a new derived type for each specifically-handled type of exception. That derived type can implement ThrowReferenceToThis
and the game code will still catch the right exception types due to polymorphism. Here’s how one of these exception types looks:
struct NullReferenceExceptionThrower : NullReferenceException { NullReferenceExceptionThrower(int32_t handle) : NullReferenceException(handle) { } virtual void ThrowReferenceToThis() { throw *this; } };
Now we just need to modify the SetCsharpException
functions to use these “thrower” types:
DLLEXPORT void SetCsharpExceptionSystemNullReferenceException(int32_t handle) { delete unhandledCsharpException; unhandledCsharpException = new NullReferenceExceptionThrower(handle); }
Finally, the code that handles these exceptions like this:
if (unhandledCsharpException) { // Copy the exception variable to a local variable Exception* ex = unhandledCsharpException; // Clear out the exception variable unhandledCsharpException = nullptr; // Use the virtual function to re-throw the right type of exception ex->ThrowReferenceToThis(); // Delete the exception now that we're done with it delete ex; }
The game code that uses this is unchanged. It still looks just like C# code, including the list of catch
blocks that can now receive specific types of exceptions:
// Make a null GameObject GameObject nullGo(nullptr); try { // Try to call the "transform" getter property nullGo.GetTransform(); } catch (NullReferenceException ex) { // Handle the exception by logging a specific message Debug::Log("NRE using the transform getter"); } catch (Exception ex) { // Handle the exception by logging it Debug::Log(ex); }
That about wraps up our support for exceptions. With this included we can now easily handle C# exceptions in C++ code and C++ exceptions in C# code. All the types are correct and everything works very similarly regardless of the language you’re programming in. The complexity is neatly tucked away in the bindings that the code generator outputs.
Reminder: if you’re interested in collaborating on this project, please contact me by e-mail or comment. Also, there’s still time to fill out the poll about this series if you haven’t done so already. Thanks!