I am trying to use a C library for a robotic project which should run on both macOS and Linux. I am trying to call a Swift callback function inside the C function passed as a parameter to the library call.
I tried the solutions proposed here and here, but they do not work.
As suggested in those answers, I pass in the userData (or similar) object passed to the C function, an object which can call the Swift callback function.
But when I access the passed userData object I get a Thread 2: EXC_BAD_ACCESS (code=1, address=0x20) error on the second line of cHandler function. And I am not able to figure out why. I am currently testing on macOS.
Here the code:
public func subscribe(newMessageHandler: @escaping () -> Void) -> Result<Subscription> {
func cHandler(buffer: UnsafePointer<lcm_recv_buf_t>?, channel: UnsafePointer<Int8>?, userData: UnsafeMutableRawPointer?) {
guard let userData = userData else { return }
let subscribeUserData = Unmanaged<SubscribeUserData>.fromOpaque(userData).takeUnretainedValue()
subscribeUserData.handler()
}
let userData = SubscribeUserData(handler: newMessageHandler)
var userDataPointer = UnsafeRawPointer(Unmanaged.passUnretained(userData).toOpaque())
self.subscribeUserData = userData
self.subscribeUserDataPointer = userDataPointer
if let subscription = lcm_subscribe(context, "ExampleMessage", cHandler, &userDataPointer) {
return .success(subscription)
} else {
return .failure(nil)
}
}
Here is the definition of SubscribeUserData, the object that I pass in the C function:
The code from (1) instantiates a C struct from Swift, and the C struct from (2) will then call Swift functions as callbacks.
In (1), at line 111, a C struct (http_parser_settings) is instantiated in a Swift class, and the swift callback functions are assigned from lines 114 to 121. That structure (http_parser_settings) is defined in (2) at line 248. It's a collection of callback function pointers (defined in (2) at lines 84 and 85).
In (1), from lines 249, the swift function used as callbacks are defined.
public func subscribe(newMessageHandler: @escaping () -> Void) -> Result<Subscription> {
func cHandler(buffer: UnsafePointer<lcm_recv_buf_t>?, channel: UnsafePointer<Int8>?, userData: UnsafeMutableRawPointer?) {
guard let userData = userData else { return }
let subscribeUserData = Unmanaged<subscribe_user_data>.fromOpaque(userData).takeUnretainedValue()
subscribeUserData.handler()
}
var userData = subscribe_user_data()
userData.handler = newMessageHandler
if let subscription = lcm_subscribe(context, "ExampleMessage", cHandler, &userData) {
return .success(subscription)
} else {
return .failure(nil)
}
}
The new type is recognized, but only in the subscribe function scope. Not inside the cHandler function.
And also the new function type singature does not match the newMessageHandler one.
Annotate both cHandler and newMessageHandler with @convention(c) to let the Swift compiler know it shouldn't capture any outer-scope variables.
Also, with regards to the error with Unmanaged -- since you're declaring subscribe_user_data as a C struct, you no longer need to use the Unmanaged APIs. Call userData.assumingMemoryBound(to: subscribe_user_data.self) and you'll get an UnsafeMutablePointer<subscribe_user_data> that you can dereference.
The goal is to call a Swift function (newMessageHandler which captures the context) from a C function (cHandler which does not allow capturing the context).
Annotating newMessageHandler with @convention(c) just makes it into a C function, which prevents me from achieving my goal.
Thanks for the suggestion, but this is not solving my problem.
PS: the suggested way to load the subscribe_user_data struct inside the C function works. Thank you!
Ah! Well, if your goal is for newMessageHandler to capture context, then you will have to put it in a Swift type, rather than a C type. Go back to your old version with the UserData class and Unmanaged, and make sure to annotate cHandler with @convention(c).
Well not really. When I created the initial post I can add tags. I just added the tag Swift. I could have added the Swift 4 tag, but I felt it was not necessary.