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.