Unity iOS Native Pointer Pattern

One common situation I find myself in when developing for Unity on iOS is having to integrate with native APIs. Integrating native features is a nice touch for your apps and makes them feel at home running in iOS. It keeps your users familiar with platform conventions instead of having to learn some UI or workflow specific to your app.

When doing this, you inevitably want to use an API which requires you to hold a reference to a pointer. Let’s say you want to use iCloud ubiquity containers in your app. You need to subscribe to changes in the cloud. This requires a persistent object on the native side to receive notifications from the operating system. The information then needs sent up to Unity and processed by your app. You may start with something like this on the native side.

@interface iCloudMonitor : NSObject

@end
@implementation iCloudMonitor {
	NSMetadataQuery * documentStoreMonitor;
}

-(instancetype)init
{
	if (!(self = [super init])) {
		return nil;
	}
	
	[self startSearch];
	return self;
}

-(void)queryDidUpdate:(NSNotification *)sender
{
	// Do what you need to when iCloud updates, this is just an example
	UnitySendMessage("iCloudMonitor", "DocumentsUpdated", "");
}

-(void)startSearch
{
	documentStoreMonitor = [NSMetadataQuery new];
	documentStoreMonitor.searchScopes = [NSArray arrayWithObjects:NSMetadataQueryUbiquitousDataScope,nil];
	[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(queryDidUpdate:) name:NSMetadataQueryDidUpdateNotification object:documentStoreMonitor];
	[documentStoreMonitor startQuery];
}

@end

extern "C" {
	void _createICloudMonitor()
	{
		[[iCloudMonitor alloc] init];
	}
}

This lets you call _createICloudWrapper() from Unity to start monitoring for iCloud changes. But there is a problem. iOS uses a system called Automatic Reference Counting, or ARC, to keep track of when to deallocate objects from memory. I don’t want to go into great depth about ARC here as there are many articles covering it in great detail such as https://www.raywenderlich.com/2992-beginning-arc-in-ios-5-tutorial-part-1. To summarize and simplify, when an object has zero strong references it get deallocated.

The call inside _createICloudMonitor() creates an object, but doesn’t hold a reference to it. As soon as the function returns, ARC will see the object has zero references and deallocate it. You need to keep a reference to the object so it stays around. One option is to make a global variable to keep ARC happy with a reference. This brings all the issues of global variables with it. Another option is to use a singleton, which is slightly better than a global. It keeps ARC happy with a reference, but still has many of the issues of a global variable. A third option is to use the -fno-objc-arc compiler flag to disable ARC on the entire file. But that means we can’t use ARC any where in the file at all. The best option is to transfer the variable out of ARC entirely. This is done through bridging.

Bridging allows us to give the ARC system more information than it can gather on its own. Namely, whether or not the system should deallocate an object. There are two things you can tell the system. One, you can tell it to stop monitoring an object which it previously was. And two, you can tell it to start monitoring an object it wasn’t. Basically you can move ownership of objects between Objective-C land with references and reference counting, and plain old C land with pointers. The function CFBridgingRetain(id) takes a reference and converts it to a CFTypeRef, which is just an alias for void *, and adds one to the retain count. Lets see how to change our code to use this.

	CFTypeRef _createICloudMonitor()
	{
		return CFBridgingRetain([[iCloudMonitor alloc] init]);
	}

Since we have stopped ARC from deallocating this object we need to do it ourselves. Just like in the time before ARC existed, every call to retain required a corresponding call to release. Luckily That function is provided as well. Lets make a function to access it.

	void _destroyICloudMonitor(CFTypeRef monitor)
	{
		CFRelease( monitor );
	}

And that’s everything we need in objective-c land. Now we can make a C# class to use this code. Here is an example of using MonoBehaviour to control the native object. When the MonoBehaviour instance is first loaded it will call into C land to make the native object. And when it is destroyed, it will call to C land again to destroy the native object. This means the native object’s lifecycle follows the MonoBehaviour instance.

using System;
using System.Runtime.InteropServices;
using UnityEngine;

public class iCloudMonitorBehaviour : MonoBehaviour
{
	[DllImport("__Internal")]
	private static extern IntPtr _createICloudMonitor();
	
	[DllImport("__Internal")]
	private static extern void _destroyICloudMonitor();
	
	private IntPtr NativeObject;
	
	void Awake()
	{
		NativeObject = _createICloudMonitor();
	}
	
	void OnDestroy()
	{
		_destroyICloudMonitor(NativeObject);
	}
}

All of this lets us use persistent native objects whose lifecycle is controlled by an object in Unity all without needing extra variables in native land to store the variables. The basics are as follows:

  1. Have a create function to make the object instance we need which bridges that variable out of ARC and returns the pointer to it.
  2. A corresponding destroy function which releases the variable sent to it.
  3. In the C# class have an IntPtr to hold the reference to the object.
  4. Inside Awake call the create function and assign the value to our pointer field.
  5. Inside OnDestroy call the destroy function and send the pointer.

This is applicable anywhere you need to use a persistent native object. Besides that, you can add all the other functions you want.

Alternate C# class

If you want something a bit more flexible than a MonoBehaviour to control the native object, implementing the IDisposable interface works really well too.

using System;
using System.Runtime.InteropServices;

public class iCloudMonitor : IDisposable
{
	[DllImport("__Internal")]
	private static extern IntPtr _createICloudMonitor();
	
	[DllImport("__Internal")]
	private static extern void _destroyICloudMonitor();
	
	private IntPtr NativeObject;
	
	public iCloudMonitor()
	{
		NativeObject = _createICloudMonitor();
	}
	
	public void Dispose()
	{
		_destroyICloudMonitor(NativeObject);
	}
}

Leave a Reply