C++ Scripting: Part 9 – Out and Ref Parameters
It’s been quite a while in the series since we’ve added any fundamental C# language features. Today we’ll address one of the limitations of the C#/C++ communication: the lack of support for out
and ref
parameters. This is important as they’re commonly used by both the Unity API and .NET and we’d like C++ to be able to call functions with these kinds of parameters. So let’s delve into what it means for C++ to use out
and ref
parameters and see how to implement support for that across the language boundary.
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 off, C++ has no concept of out
or ref
parameters. It does, however, have a similar concept that we can use to gain a close mapping of idiomatic C++ to C#.
Let’s briefly review what out
and ref
mean in C#. Both of them mean that the parameter shouldn’t be passed as it normally would, but as a pointer to what it normally would be passed as. Remember that C# parameters are either a reference type (i.e. class instances) or a value type (i.e. struct instances). A reference type is essentially a pointer behind the scenes, so when you use out
or ref
you’re saying that you want to pass a pointer to a pointer to a struct for reference types or a pointer to a struct for struct types.
There’s also a small difference between out
and ref
. When you use ref
, it’s required that the parameter be initialized before the function is called but there is no requirement for the function to set the parameter. When you use out
, it’s not required that the parameter be initialized before the function is called but it is required that the function set the parameter.
In C++, there’s no difference between out
and ref
. There is simply a single pointer type that you get by adding an asterisk onto a parameter type: int*
. The caller uses an ampersand to get a pointer to their parameter:&myParam
. The compiler doesn’t require that the parameter be initialized before the function call or set by the function.
Here’s a comparison table of the three types of parameters:
Parameter Type | Must be initialized before function call? | Must be initialized by function? | Passed as pointer? |
---|---|---|---|
C# out |
No | Yes | Yes |
C# ref |
Yes | No | Yes |
C++ pointer | No | No | Yes |
So how will we implement this for our C++ plugins? For value types like structs and int
, the choice is clear: C++ simply adds a *
to the parameter type. Imagine a C# class like this:
// C# // Exposed API static class CsharpClass { public static void IntFunc( out int outParam, ref int refParam) { outParam = refParam; } } // Wrapper function for calling that API public static void CsharpClassIntFunc( out int outParam, ref int refParam);
C++ can call this C# class with the normal binding code :
// C++ // Binding function pointer to call into the C# delegate void (*CsharpClassIntFunc)( int32_t* outParam, int32_t* refParam); // API for game code struct CsharpClass { static void IntFunc( int32_t* outParam, int32_t* refParam) { CsharpClassIntFunc( outParam, refParam); } }; // Example game code void Foo() { // It's OK to not initialize outParam because C# // guarantees it'll be set. Feel free to initialize // it for safety if you want. int32_t outParam; int32_t refParam = 5; CsharpClass::IntFunc( &outParam, &refParam); // Use the variables as normal Vector3 position( (float)outParam, (float)refParam, 0.0f); GameObject go; Transform transform = go.GetTransform(); transform.SetPosition( position); }
That’s about all there is to out
and ref
parameters for structs. Reference types are a whole other story.
C# class instances (a.k.a. objects) are essentially a pointer to a struct. Unfortunately, C# doesn’t let us access either the pointer or the struct. This is why we needed to use “object handles” as a common way of describing the same object between C# and C++ back in the first part of this series. When we pass an object from C++ to C#, we actually just pass its int
handle. As a refresher, here’s how passing an object from C++ to C# works:
// C# // API we want to expose to C++ static class Logger { public static void Log( string message) { Debug.Log( message); } } // Wrapper function for calling that API public static void LoggerLog( int messageHandle) { // Get the object associated with the handle string message = (string)ObjectStore.Get( messageHandle); // Call the exposed function Logger.Log( message); }
// C++ // Function pointer for calling the C# wrapper function void (*LoggerLog)( int32_t messageHandle); // API for game code struct Logger { static void Log( String message) { // Pass the object handle instead of the object itself LoggerLog( message.Handle); } } // Example game code void Foo() { Logger::Log( String("test message")); }
As you can see above, we’re simply passing an int
as the object handle. The new complication with both out
and ref
parameters is that they can be assigned to by the function. So if the function were to assign a new object to an out
or ref
parameter, we’d need a way to get the new object handle back to C++.
We can do this by modifying the object handle type for out
and ref
parameters that are a reference type (i.e. class instance) so that it’s a ref int
parameter instead of just an int
parameter. That’ll let us write the new object handle back into C++ just like we did with IntFunc
.
On the C++ side, we need to deal with the object handle changing because we’re keeping reference counts on the C++ side. If the handle changes, we need to release our reference to the old handle and add a reference to the new handle. This can all be done in the binding layer without bothering the game code. The trick is to not pass a pointer to message.Handle
as we would have done above, but instead copy the handle and pass a pointer to that. This lets us compare handles after the function returns and only update reference counts if the handle has changed.
Here’s an example of how that works:
// C# // API to expose static class Network { public static void GetConfig( out string hostName, out int port) { hostName = "localhost"; port = 8080; } } // Binding function public static void NetworkGetConfig( ref int hostNameHandle, out int port) { // Get the object from the object handle string hostName = (string)ObjectStore.Get( hostNameHandle); // Call the exposed API Network.GetConfig(out hostName, out port); // Store the new object and write the handle back to C++ hostNameHandle = ObjectStore.Store(hostName); }
// C++ // Function pointer for calling the binding function void (*NetworkGetConfig)( int32_t* hostNameHandle, int32_t* port); // API for game code struct Network { static void GetConfig( String* hostName, int32_t* port) { // Make a copy of the object handle // Note: -> is like . for pointers int hostNameHandle = hostName->Handle; // Call the exposed function NetworkGetConfig( &hostNameHandle, &port); // Set the new object handle hostName->SetHandle(hostNameHandle); } }; // Example game code void Foo() { // Call the exposed API String hostName("old host name"); int32_t port; Network::GetConfig( &hostName, &port); // Use the out parameters Debug::Log(hostName); }
The SetHandle
function is new. Its job is to dereference the old handle and reference the new one to update what C# object a C++ object refers to. Here’s how it looks:
void Object::SetHandle(int32_t handle) { // Only dereference/reference if the handle has changed if (Handle != handle) { // If we had an existing handle, dereference it if (Handle) { Plugin::DereferenceManagedObject(Handle); } // Set the new handle Handle = handle; // If we have a new handle, reference it if (handle) { Plugin::ReferenceManagedObject(handle); } } }
There’s one last wrinkle to smooth out for this. Currently, we don’t have any concept of a null object in C++. Unlike in C#, only pointer types can be null in C++. C# class instances are essentially pointers, hence they can be null, but our C++ types are just structs and, like in C#, a struct can’t be null.
We do have a “null” object handle value though: zero. So one of these C++ structs could have an object handle value of zero and be considered null. There’s already a constructor for all of these struct types that takes a handle, but passing zero doesn’t clearly show the caller’s intent. It’d be better if we could pass C++’s version of null
, which is called nullptr
. It has the type std::nullptr_t
, so all we need to do is make a constructor that takes one of those:
struct String { String(std::nullptr_t n) : Object(0) // pass the "null" handle to the base class { } };
Now we don’t need the awkward (and garbage-allocating!) temporary string in the above example. Instead, we can just make a “null” instance:
// Instead of this... String hostName("old host name"); // Write this... String hostName(nullptr);
With that in place we can add some overloaded operators for convenience and to look a little more like C# or C++ pointers:
// Overload null-related operators struct Object { // Converts an Object to a bool operator bool() const { return Handle != 0; } // Equality operator with nullptr bool operator==(std::nullptr_t other) const { return Handle == 0; } // Inequality operator with nullptr bool operator!=(std::nullptr_t other) const { return Handle != 0; } }; // Example game code void Foo() { // Now we can assign nullptr String str = nullptr; // We can automatically convert to bool, like other pointers if (str) { Debug::Log(String("str is NOT null")); } // And we can compare explicitly with nullptr if (str == nullptr) { Debug::Log(String("str IS null")); } if (str != nullptr) { Debug::Log(String("str is NOT null")); } }
Now that we can create “null” objects on the C++ side, we can be a lot more efficient because we can skip the call into C# and the very expensive GC allocation to actually create a class instance. It’ll really help with out
parameters specifically since they’re often uninitialized before calling the function.
That’s all for this week’s installment of the series. Today we’ve gained access to the commonly-used out
and ref
parameters in C# so our C++ code can access even more C# APIs. The GitHub project has been updated with everything in this article, so feel free to have a look if you’re interested in all the nitty-gritty details of how this was implemented in the code generator.
#1 by kilik on September 17th, 2017 ·
hi we try get ref out in unity
https://forum.unity.com/threads/return-ref-out-variable.495164/
or return by delegate or sytem action
#2 by jackson on September 17th, 2017 ·
Thanks for the link. I’m sure there are language features like “ref return” that I’ll need to add support for if Unity ever allows for C# 7.0. For now,
ref
is just supported for parameters in both C# and this C++ Scripting project.