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

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.