I want my C# program (A Unity app) to call into iOS native code so that I can communicate with, e.g., Apple Health. I’ve successfully got my program calling in to run Swift code, but I want to be able to provide callback functions for asynchronous action, and I think I’m missing some detail to allow these to work.
I’ve got two example Swift functions. returnTrue
returns a bool immediately, and sendTrue
takes in a callback to send the bool:
Example.swift
public struct Example {
static func returnTrue() -> Bool {
return true
}
static func sendTrue(callback: @escaping (Bool) -> Swift.Void) {
callback(true);
}
}
I’ve exposed these Swift functions to C using @_cedcl
:
Bridge.swift
import Foundation
@_cdecl("Example_returnTrue")
public func Example_returnTrue() -> Bool {
print("Example_returnTrue started");
return Example.returnTrue();
}
@_cdecl("Example_sendTrue")
public func Example_sendTrue(callback: @escaping (Bool) -> Swift.Void) {
print("Example_sendTrue started");
Example.sendTrue(callback: callback);
}
Then I’ve set up a simple C# script in my Unity project to call these:
iOSCommunication.cs
using System;
using UnityEngine;
using System.Runtime.InteropServices;
using AOT;
public class iOSCommunication : MonoBehaviour
{
[DllImport("__Internal")]
private static extern bool Example_returnTrue();
[DllImport("__Internal")]
private static extern void Example_sendTrue(Action<bool> callback);
public void Start()
{
Debug.Log($"returnTrue: {Example_returnTrue()}");
Example_sendTrue(ResultHandler);
}
[MonoPInvokeCallback(typeof(Action<bool>))]
private static void ResultHandler(bool result)
{
Debug.Log($"sendTrue: {result}");
}
}
When I run this, the simple returnTrue
function works but sendTrue
crashes. Here’s my brief log output:
Example_returnTrue started
returnTrue: True
Stacktrace is not supported on this platform.
(lldb)
The error given in Xcode is EXC_BAD_ACCESS
and it seems to occur early within the Example_sendTrue
function, but I can only see the machine code. Notably, we don’t print Example_sendTrue started
, so I think the problem is with initialisation of this function.
I’ve been advised to add (and have added) the decorator MonoPInvokeCallback
to the C# method I pass through so that it’s made visible to C, but I suspect I’m missing a declaration on the Swift side to describe what this is, or that I’m not correctly describing the callback’s signature to C and/or Swift.
2
Answers
Adding a
@convention(c)
attribute to the callback's signature in both the static function and C declaration defines the parameter as a C function pointer. This is then correctly addressable by Swift when a C function is passed in as the parameter, and can be invoked in the expected way.There’s no direct interoperability between C# and Swift, so any interoperability between them has to pass through a C interface, which has no native representation of Swift closures or C# Delegates/Actions.
The closest thing is to pass a function pointer, which must be
@convention(c)
.Here’s an excerpt from a thread I started on the Swift forums about something this: https://forums.swift.org/t/add-api-that-makes-it-easier-to-interoperate-with-c-callbacks/62718.
It’s more concerned with calling C APIs from Swift than having Swift APIs be called from C, but the function pointer+userInfo pointer idea is much the same.
C doesn’t have the luxury of closures, and by extension, neither does Swift code that tries to interoperate with C APIs. C functions that want to have a callback have to take a function pointer, which is
@convention(c)
by Swift terms. If you try to pass a capturing closure, you’d get a compilation error:Working around this is pretty tricky, and requires some pretty sophisticated knowledge. (There are Objective-C blocks which can be used in C code, but they’re not a standard part of C itself, and there are plenty of APIs that don’t use them, so I don’t see them as a solution to this problem.)
C APIs often simulate closures by using a pair parameters:
void *
pointer.When the callback is called, you’re passed back your context as an argument, which you can cast to whatever type you had given it, and unpack your contextual variables from there.
Here’s an example simulated C API:
Calling it from Swift is a little tricky, in part because you need to be pretty careful with specifying how userInfo’s memory should be managed.
Here’s an example of one way to call such an API with a userInfo parameter. This approach uses the
userInfo
value to smuggle in a closure which can be invoked. This lets you have the convenience of a normal Swift closure on the caller, while being compatible with the C API. It adds one extra layer of function indirection, but that’s usually acceptable.