Use a C/C++ library with function pointer callbacks in Swift with async/await

Hi all!

Is there a way to get the following working:

So I have a C/C++ lib (actually a C++ lib with a C interface) which I want to use in a Swift package, I managed to wrap its API in an Objective-c++ class. The wrapper exposes for example a method as:

+ (void)auth: (BasedClientID)clientId withName:(NSString *)token andCallback: (void (*)(const char *))callback
NS_SWIFT_NAME(auth(clientId:token:callback:));

..which I can use in Swift as:

        BasedWrapper.auth(clientId: clientId, token: token) { chars in
        }

Ideally I would want then something like:

    func auth(clientId: BasedClientId, token: String) async -> String? {
        
        return await withCheckedContinuation { continuation in
            BasedWrapper.auth(clientId: clientId, token: token) { chars in
                let string = String(cString: chars, encoding: .utf8)
                continuation.resume(returning: string)
            }
        }
    }

But this gives me an error:
A C function pointer cannot be formed from a closure that captures context

Is there a way to get this working, so having the C callback with async await?

2 Likes

This might be difficult. If the callback has a field for an arbitrary data pointer, you might use something like this example to pass an instance of Swift class containing Swift closure. (It is ideal, to have something like destroy callback to release said Swift class.)

But would that help to get it also work with async/await?

If you can get Swift closure to work, then you can add additional layer for async/await.

However, the API you have posted is insufficient even for the simple Seift closure.

To put it simply, you need to have the ability to “pair” the non-blocking call and callback call.

Whether you can use arbitrary data pointer or number tag (or you can ensure that the callbacks are called in a particular order, like fifo for example) does not matter. But you need a guarantee that we can build on.

This is very doable, see enclosed.
// MARK: test.swift

func test() {
    Task {
        let result = await Auth.auth(1, withName: "2")
        print(result);
    }
}
test()
RunLoop.current.run(until: .distantFuture)

// MARK: Bridging header:

#include "objc.h"

// MARK: objc.h

#import <Foundation/Foundation.h>

typedef NSInteger BasedClientID;

@interface Auth: NSObject
+ (void)auth:(BasedClientID)clientId withName:(NSString *)token completion:(void (^)(NSString*))completion;
@end


// MARK: objc.m

#import "objc.h"

void ccall(void (*callback)(const char *)) {
    callback("Hello");
}

static void (^_completion)(NSString*);

static void ccallback(const char* s) {
    NSString* ns = [NSString stringWithUTF8String:s];
    _completion(ns);
}

@implementation Auth
+ (void)auth:(BasedClientID)clientId withName:(NSString *)token completion:(void (^)(NSString*))completion {
    _completion = completion;
    ccall(ccallback);
}
@end

Note that I had to use global variable to remember the passed block (swift closure) to use it from within C callback. Should your C call & callback be using a slightly different signature:

void ccall(void* userData, void (*callback)(void* userData, const char *));

then it would be possible passing swift's closure to userData parameter (with appropriate bridge casting) without resorting to global variable.

The global variable version has this obvious limitation: don't try to call "auth" second time before the first call completes.

So, in your first example, I did not know that an objc block is already available with await.. the second example seems more appropriate since a global var does not feel like a good solution. But the pointer userData how can you 'bridge' a Swift closure to having userData accepting it.. and then how would you use it in C land?

The example I've posted in my first post is doing exactly that. You can capture Swift closure in an instance of Swift class and then use Unmanaged to pass it around as an opaque pointer (or UnsafeRawPointer). I can rewrite it into a clearer example if you would like.

IMO means: if you can change the C++ library, you need to introduce an userData argument which does precisely what I wrote above: pass around an opaque pointer pointing to an instance of Swift class.

If you can not change the C++ API and the C++ API does not provide it already, then you're in no luck and you need to find other solution.

C function pointer is a pointer-wide variable, that contains pointer to executable memory, where the function is located.
Swift closure (in most cases) two pointer-wide variable. First pointer is a function pointer. Second pointer is (in most cases) reference-counted heap-allocated box, where the arguments (capture list) for the function pointer are stored.

If you can't change C library API - you are stuck with global variable approach. And if you need to support, say, up to 10 concurrent "auth" calls you'd need 10 different callbacks each referencing their own global variable, which sucks.

OTOH, if you can change the C library and add userData parameter in that call / callback - all good, use smth like this (showing the relevant pieces, everything else is as before):

void ccall(void* userData, void (*callback)(void* userData, const char *)) {
    callback(userData, "Hello");
}

static void ccallback(void* userData, const char* s) {
    NSString* ns = [NSString stringWithUTF8String:s];
    void (^completion)(NSString*) = (__bridge void (^)(NSString*))userData; // 🐞 see Edited below
    completion(ns);
}

@implementation Auth
+ (void)auth:(BasedClientID)clientId withName:(NSString *)token completion:(void (^)(NSString*))completion {
    ccall((__bridge void *)completion, ccallback); // 🐞 see Edited below
}
@end

Edited:
As per @jrose correction below these should be retain/release bridging conversions, e.g.:

void (^completion)(NSString*) = (void (^)(NSString*))CFBridgingRelease(userData);
...
ccall((void*)CFBridgingRetain(completion), ccallback);

Nitpick: those should be __bridge_retain and __bridge_transfer so the block isn’t immediately released before the callback happens!

Good to know. How do I reproduce the crash?

For some reason this works for me as written.
void ccall(void* userData, void (*callback)(void* userData, const char *)) {
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 3*1000000000LL), dispatch_get_main_queue(), ^{
        callback(userData, "Hello");
    });
}

static void ccallback(void* userData, const char* s) {
    NSString* ns = [NSString stringWithUTF8String:s];
    void (^completion)(NSString*) = (__bridge void (^)(NSString*))userData;
    completion(ns);
}

@implementation Auth
+ (void)auth:(BasedClientID)clientId withName:(NSString *)token completion:(void (^)(NSString*))completion {
    ccall((__bridge void *)completion, ccallback);
}
@end

(tried both debug & release builds)

Maybe using ASan? Or maybe the block you’re testing with doesn’t actually capture anything and therefore didn’t end up using a dynamic allocation.

1 Like

You are absolutely right – the block wasn't capturing anything hence the problem was hidden.

Is this as good as what you suggested above? This is what Xcode suggests for "fix-it":

    void (^completion)(NSString*) = (void (^)(NSString*))CFBridgingRelease(userData);
    ...
    ccall((void*)CFBridgingRetain(completion), ccallback);

Yep, that’s what I’d use! Note that this does bake in the assumption that the callback is called exactly once; if it’s never called, the block will be leaked, and if it’s called multiple times, it’s a use-after-free. In that case you’ll have to use some other mechanism to manage the lifetime of the block.

1 Like

ok, so one of the C API call has actually a callback which can be called as long as the callback lives.. a sort of observable. I guess then you need something else like you mention?

Then it should be +1 (CFBridgingRetain) on "enter", then a sequence of +0 (__bridge) callouts, and finally -1 (CFBridgingRelease) to release the block at the end.

1 Like

Right. To this I'd add that Obj-c blocks are also pointer sized:

printf("C function pointer size: %ld\n", sizeof(void (*)())); // 8
printf("Obj-C block size: %ld\n", sizeof(void (^)())); // 8
...
print("swift closure size: ", MemoryLayout<()->Void>.size) // 16

And when swift closure is getting passed through to Obj-C somehow the resulting block is fully functional and works properly. Quite magical.

1 Like

Thanks for the answer!

1 Like

Thanks!

  • So with the multiple calls you would need to know when 'listener' stops listening and so CFBridgingRelease is needed...
  • And so do I understand correctly that the block completion handlers are already usable with 'await' in Swift? I need the wrapping Swift API also to confirm to async/await.
  • I also saw examples where the closures were wrapped in in a class and then passed over in the closure with 'Unmanaged', however this seems cleaner but you need to change the C api to use the 'in between closure' right?
  1. yes
  2. yes, async version is generated automatically from a function with the trailing completion handler parameter.
  3. you can do this but as I understand this happens automatically (during conversion from swift closure to obj-c block) and it'd prohibit (2) while also making the caller side more complicated than necessary.

Also I think you would then need different 'proxy' callbacks in C space supporting more callback type., e.g. with more and different params.