avdwerff
(Alexander van der Werff)
1
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
stuchlej
(MikolĂĄĹĄ StuchlĂk)
2
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.)
avdwerff
(Alexander van der Werff)
3
But would that help to get it also work with async/await?
stuchlej
(MikolĂĄĹĄ StuchlĂk)
4
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.
tera
5
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.
avdwerff
(Alexander van der Werff)
6
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?
stuchlej
(MikolĂĄĹĄ StuchlĂk)
7
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.
tera
8
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);
jrose
(Jordan Rose)
9
Nitpick: those should be __bridge_retain and __bridge_transfer so the block isnât immediately released before the callback happens!
tera
10
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)
jrose
(Jordan Rose)
11
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
tera
12
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);
jrose
(Jordan Rose)
13
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
avdwerff
(Alexander van der Werff)
14
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?
tera
15
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
tera
16
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
avdwerff
(Alexander van der Werff)
20
Also I think you would then need different 'proxy' callbacks in C space supporting more callback type., e.g. with more and different params.