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(); } }
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.