C++ Scripting: Part 17 – Boxing and Unboxing
The GitHub project is closing in on supporting all the “must have” features. Today’s article tackles “boxing” and “unboxing” so our C++ game code will be able to convert types like int
into an object
and then convert an object
back into an int
. Usually we want to avoid this because it creates garbage for the GC to later collect and ruins type safety, but sometimes an API like Debug.Log
insists that we pass it an object
. Read on to see how to use boxing and unboxing in C++!
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
First let’s review what boxing and unboxing are in C#. Say we have an int
and we want to pass it to a function like Debug.Log
that takes an object
. We can make an object
out of anything in C# by boxing it. This puts the int
into a “box” of type object
. Think of a class like this:
// The box type class IntBox { // The boxed value private int val; // Create the box with a boxed value public IntBox(int val) { this.val = val; } // Unbox by getting the boxed value back out int Unbox() { return val; } }
Since all classes derive from object
, we can use IntBox
to convert an int
into an object
and then call Unbox
to convert an object
into an int
. Here’s how that would look:
// Create a value to box int val = 123; // Box the value // Store it as just an 'object' object boxed = new IntBox(val); // Unbox the value int unboxed = ((IntBox)boxed).Unbox();
It would be tedious to have to create an IntBox
for every type of primitive, enum, and struct. We could create a generic version of it, but we’d still end up with a lot of typing to box and unbox.
So the language designers of C# decided to add some syntax sugar to make boxing and unboxing easier. We don’t need to create a “box” class, we don’t need to explicitly call new
on the box type to perform boxing, and we don’t need to cast object
to the right type of “box” class to perform unboxing. Here’s how it looks with the syntax sugar in C#:
// Create a value to box int val = 123; // Box the value // Store it as just an 'object' object boxed = val; // Unbox the value int unboxed = (int)boxed;
Boxing is now implicit and unboxing is now a cast. As it turns out, this can be identically supported in C++. For boxing, all we have to do is add a constructor that takes the type to box. Likewise, we can define a conversion operator just like the overloading we did in part 13. Here’s how they look:
class Object { // Boxing: construct from an int Object(int32_t val); // Unboxing: conversion operator to an int explicit operator int32_t(); };
Using this is just like in C#:
// Create a value to box int32_t val = 123; // Box the value // Store it as just an 'Object' Object boxed = val; // Unbox the value int32_t unboxed = (int32_t)boxed;
Boxing and unboxing work the same way in C# and C++ for all primitive types like int
, enum types, and struct types.
So how do we implement support for boxing and unboxing? It comes down to implementing those two functions in Object
. For boxing, we pass the value to C# to perform boxing. If the value to box is a managed struct, we just pass its handle. Then C# boxes the value, stores it in ObjectStore
or StructStore<T>
, and returns the handle. Here’s how the C++ part looks:
// Box a primitive, enum, or "full struct" Object::Object(int32_t val) { Handle = BoxInt(val); } // Box a "managed struct" Object::Object(RaycastHit val) { Handle = BoxRaycastHit(val.Handle); }
Then the C# part looks like this:
// Box a primitive, enum, or "full struct" static int BoxInt(int val) { return ObjectStore.Store((object)val); } // Box a "managed struct" static int BoxRaycastHit(int handle) { return ObjectStore.Store((object)StructStore<RaycastHit>.Get(handle)); }
No JSON config file changes are necessary. The code generator outputs boxing and unboxing functions for all enum and struct types in the Types
section. It also automatically generates boxing and unboxing functions for all primitive types like int
, bool
, and float
.
With that, we have boxing and unboxing available to us. So now we can write C++ game code that takes advantage of it:
// Implicitly boxes the int32_t to an Object, just like in C# Debug::Log(123); void Foo(Object obj) { // Unbox the Object into a QueryTriggerInteraction enum QueryTriggerInteraction unboxed = (QueryTriggerInteraction)obj; }
It’s important to keep in mind that boxing and unboxing have their downsides. First and foremost is that boxing creates garbage for the GC to later collect. Think of the IntBox
class we manually created and remember that is essentially what’s happening behind the syntax sugar in either C# or C++. It’s always good to keep garbage creation to a minimum to avoid the GC’s frame-spiking wrath, so beware of boxing in either language.
A boxed object has an unknown type. It’s just a plain object
, which can be anything. Type safety goes mostly out the window by using plain object
types as we’ll have to cast them to do anything with the actual type. This can lead to bugs and will definitely lead to slower code as runtime type checking needs to be applied.
In short, it’s good practice to avoid boxing and unboxing whenever possible. It is, however, useful to comply with APIs like Debug.Log
which are highly useful and sometimes demand that types get boxed.
As usual, support for boxing and unboxing is now available on the GitHub project. Grab a copy if you want to try it out or see the nitty-gritty implementation details.