skip to Main Content

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


  1. Chosen as BEST ANSWER

    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.

    @_cdecl("Example_sendTrue")
    public func Example_sendTrue(callback: @escaping @convention(c) (Bool) -> Swift.Void) {
        print("Example_sendTrue started");
        Example.sendTrue(callback: callback);
    }
    
    static func sendTrue(callback: @escaping @convention(c) (Bool) -> Swift.Void) {
        callback(true);
    

  2. 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:

    ❌ a C function pointer cannot be formed from a closure that captures context

    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:

    • A function pointer (for defining the behaviour)
    • And an accompanying context pointer (for the contextual data). This is often called "userInfo" or "context" and is just a 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:

    import Foundation
    import Dispatch
    
    // Just a simple example, written in Swift so you don't need to set up a complex multi-lang project.
     Pretend this was an imported C API.
    func runCallback(
        after delaySeconds: Int,
        userInfo: UnsafeRawPointer?,
        callback: @convention(c) (_ currentTime: UInt64, _ userInfo: UnsafeRawPointer?) -> Void
    ) {
        DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(delaySeconds) ) {
            callback(
                DispatchTime.now().uptimeNanoseconds, // Some values given to you by the C API
                userInfo // Passes back your `userInfo` for you to access your context.
            )
        }
    }
    
    

    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.

    func cAPICaller_closure() {
        let i = 123 // Some local state we want to close over and use in our callback
        
        typealias ClosureType = (_ currentTimeNS: UInt64) -> Void
        
        // This is just a simple Swift closure. It can capture variables like normal,
        // and doesn't need to know/worry about the `userInfo` pointer
        let closure: ClosureType = { currentTimeNS in
            print("Hello, world! (currentTimeNS) (i)")
        }
        
        runCallback(
            after: 1,
            userInfo: Unmanaged.passRetained(closure as AnyObject).toOpaque(), // Needs `as AnyObject`? A bit odd, but sure.
            callback: { currentTimeNS, closureP in
                guard let closureP else { fatalError("The userInfo pointer was nil!") }
                
                // Retain the pointer to get an object, and cast it to our Swift closure type
                guard let closure = Unmanaged<AnyObject>.fromOpaque(closureP).takeRetainedValue() as? ClosureType else {
                    fatalError("The userInfo points to an object that wasn't our expected closure type.")
                }
                
                // Call our Swift closure, passing along the callback arguments, but not any `userInfo` pointer
                closure(currentTimeNS)
            }
        )
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search