Effective Two-Way Communication with a Native Unity Plugin

Sep 18, 2015

Unity has an incredibly powerful native plugin interface which allows almost any native library to be consumed by, and used within, the engine. The engine is also very dynamic and constantly changing, and the majority of information I found online about just how to do this was out of date and no longer applicable or just plain wrong. There are lots of tutorials that show how to call native code from Unity, but very few indeed show how to effectively call Unity code from a native library.  This can be a big problem if the library you're trying to incorporate has asynchronous calls. The only method documented in the official docs, UnitySendMessage, is very clunky and involves costly string parsing. This tutorial will demonstrate how to create C# delegates which actually compile to native code and can be called directly from C. It does involve a little bit of setup code, but it ends up working very nicely with asynchronous code libraries because asynchronous code often involves registering for notifications by design. Since the setup involved in enabling native to managed communication involves registering managed functions, your plugin can enable developers to register their C# code for native callbacks directly.

This tutorial is going to be using OSX and XCode, but the concepts apply to every platform. The Example app created will demonstrate how to pass data from Unity C# to native code and how to invoke Unity script from the native code. Calling the native code from Unity is pretty straightforward, but the other direction takes a little more setup. We are going to cover the creation of the native bundle first, then show how to access it from Unity. Depending on the library you want to work with, it may already be available as a bundle with an external C interface. If you're not so fortunate, or you're creating a library yourself, you'll need to create a bundle to expose your native code in a manner that Unity can consume.

Create a C file and corresponding header to expose your interface. We'll start by taking a look at the header.

#ifndef Example_h
#define Example_h

// Here we forward declare the C equivalent of the C# function we want to call
typedef void (*NativeDebugPrintDelegate)( const char* nativeMessage );

// This function will pass a reference to a C# delegate to the C runtime.
void RegisterPrintDelegate( NativeDebugPrintDelegate printDelegate );

//  This simple function demonstrates how to pass a string from managed to native code
void StringExampleFunction( const char* managedMessage );

#endif

We'll also take a look at the implementation file before we dive too far into this.

// stddef.h includes useful macros such as NULL
#include "stddef.h"
#include "Example.h"

// This allows us to store a reference to the delegate passed in from C#
NativeDebugPrintDelegate managedPrint = NULL;

// Store a reference to the C# delegate
void RegisterPrintDelegate( NativeDebugPrintDelegate printDelegate )
{
    managedPrint = printDelegate;
}

// Pass the message back to the C# using the delegate we registered
void StringExampleFunction( const char* managedMessage )
{
    // Make sure that the print delegate has been registered
    if( managedPrint )
    {
        managedPrint( managedMessage );
    }
}

The functions we've defined in this plugin are intended to be called in order. The C# would first register its callback functions, then use the C API. Build the project, then right click on the bundle product in the explorer, and select show in finder to get the .bundle that you've just created. Next we'll look at the C# and Unity side of the equation.

Create a Plugins folder in your Unity project, and drag the bundle that you created in xcode into the plugins folder.

Then create a C# script, and we'll start accessing the plugin that we've just created.

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

public class InteropTest : MonoBehaviour 
{
	//Declare the delegate for our callback
	delegate void DebugPrintDelegate( string message );

	// To enable 2 way communication, we use Mono's Ahead Of Time compiler functionality to compile a delegate to native code.
	[MonoPInvokeCallback(typeof( DebugPrintDelegate ))]
	void nativeDebugPrint( string message )
	{
		// For testing, we'll just print the message to the console
		Debug.Log( message );
	}

	// Here we import the functions that we created in our C plugin.
	[DllImport ("Example")]
	static extern void RegisterPrintDelegate( DebugPrintDelegate callback );

	[DllImport ("Example")]
	static extern void StringExampleFunction(  string managedMessage );
	
	// Now that our setup is done, test our plugin
	void Start () 
	{
		// pass our native-compiled delegate to the C library
		RegisterPrintDelegate( nativeDebugPrint );
		// Call the test function.  If it works, it will invoke our delegate
		StringExampleFunction( "Two way communication!" );
	}
}

Add the script to an empty game object, and you should be greeted with the log message "Two way communication!" when you hit run in the editor.

I hope this tutorial is helpful, but if you have any questions please feel free to ask at zach@voidstarsolutions.com.

A full, working version of this project can be found here: ExampleProjects.zip