Posted on 1 Comment

Using those pointers

In my last post I outlined a way to manage native object lifecycle from C# in Unity. But I skipped one of the conveniences of calling native functions. You can call a function without matching the declared type of the pointer parameters. That means you don’t need to use CFTypeRef everywhere.

The pointer you have in C# is untyped, it is void *. That’s the same as CFTypeRef. But you can declare and call native functions which are your type. Let’s take a look at one of the classes we could build from the last post.

// nativeClass.h
@interface NativeClass
- (instancetype) init;
@end
// nativeClass.m
#import "nativeClass.h"
@implementation NativeClass
- (instancetype) init
{
  // omitted for brevity
}
@end

#if __cplusplus
extern "C" {
#endif
CFTypeRef _createNativeClass()
{
  return CFBridgingRetain([[NativeClass alloc] init]);
}

void _destroyNativeClass(CFTypeRef nativeClassInstance)
{
  CFRelease(nativeClassInstance);
}
#if __cplusplus
}
#endif
// NativeClass.cs
using System;
using System.Runtime.InteropServices;
public class NativeClass : IDisposable
{
  [DllImport("__Internal")]
  static extern IntPtr _createNativeClass();

  [DllImport("__Internal")]
  static extern void _destroyNativeClass(IntPtr nativeClassInstance);

  IntPtr m_Instance;

  public NativeClass()
  {
    m_Instance = _createNativeClass();
  }

  public void Dispose()
  {
    _destroyNativeClass(m_Instance);
  }
}

This is a very basic native class which has its lifecycle controlled from C#. But we can take things a step further with our own functions. Let’s add a method to our objective-c interface.

// nativeClass.h
@interface NativeClass
- (instancetype) init;
- (void)doSomeStuff;
@end

Now we can add an implementation, and provide a C function wrapper. Remember, C# can call C functions, but not objective-c. So we need to write a simple C wrapper for any methods we want to call from C#.

// nativeClass.m
#import "nativeClass.h"
@implementation NativeClass
- (instancetype) init
{
  // omitted for brevity
}

- (void)doSomeStuff
{
  // Only the best algorithm
}
@end

#if __cplusplus
extern "C" {
#endif
CFTypeRef _createNativeClass()
{
  return CFBridgingRetain([[NativeClass alloc] init]);
}

void _destroyNativeClass(CFTypeRef nativeClassInstance)
{
  CFRelease(nativeClassInstance);
}

void _doSomeStuffNativeClass(const NativeClass * instance)
{
  [instance doSomeStuff];
}
#if __cplusplus
}
#endif

Notice the declared pointer type in the function parameters. It is NativeClass * instead of CFTypeRef which was used last time. Yes, this actually works. Finally, let’s add the C# code for this method.

// NativeClass.cs
using System;
using System.Runtime.InteropServices;
public class NativeClass : IDisposable
{
  [DllImport("__Internal")]
  static extern IntPtr _createNativeClass();

  [DllImport("__Internal")]
  static extern void _destroyNativeClass(IntPtr nativeClassInstance);

  [DllImport("__Internal")]
  static extern void _doSomeStuffNativeClass(IntPtr instance);

  IntPtr m_Instance;

  public NativeClass()
  {
    m_Instance = _createNativeClass();
  }

  public void Dispose()
  {
    _destroyNativeClass(m_Instance);
  }

  void DoSomeStuff()
  {
    _doSomeStuffNativeClass(m_Instance);
  }
}

In C#, the pointer remains an IntPtr which is essentially a void * or CFTypeRef. But we can call functions with the proper type declared in the function parameters. That means no casting is needed when we marshal our pointer into native objective-c.