3

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.

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:

typealias NewMessageHandler = () -> Void

/// User data object passed in the subscribe C handler function. Needed to pass in a Swift handler function.
class SubscribeUserData {
    let handler: NewMessageHandler
    init(handler: @escaping NewMessageHandler) {
        self.handler = handler
    }
}
Cla
  • 1,810
  • 3
  • 22
  • 36
  • I am currently getting this error on macOS. – Cla Jan 31 '18 at 10:18
  • 1
    The solution at https://stackoverflow.com/a/33262376/1187415 assumes that the object (in your case: `userData`) is somehow kept alive by keeping a strong reference. That seems not be the case here, so I assume that the object is destroyed before the handler is called. – Either keep a strong reference, or create *retained* pointers (compare also https://stackoverflow.com/a/33310021). – Martin R Jan 31 '18 at 10:21
  • I tried already both solutions already with the same result. I used to have properties on the parent object where I store both `userData` and `userDataPointer`. The result was the same – Cla Jan 31 '18 at 11:35

2 Answers2

4

Thanks to Andy for giving me different advice which made me solve this issue.

One problem was that I was passing the UnsafeMutableRawPointer to the cHandler function prefixed with the & operator.

The second problem is that the object I pass inside the cHandler function was getting deallocated. So keep a reference to it is essential.

Here the working code:

public func subscribe(newMessageHandler: @escaping NewMessageHandler) -> 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()
    }

    self.subscribeUserData = SubscribeUserData(handler: newMessageHandler)
    let subscribeUserDataPointer = UnsafeMutableRawPointer(Unmanaged.passUnretained(subscribeUserData).toOpaque())

    if let subscription = lcm_subscribe(context, "ExampleMessage", cHandler, subscribeUserDataPointer) {
        return .success(subscription)
    } else {
        return .failure(nil)
    }
}

Thanks to everyone for the help!

Cla
  • 1,810
  • 3
  • 22
  • 36
0

Update

Looking closely at one of your examples you seem to be doing the same thing, which makes me wonder about your handler signature.

func cHandler(buffer: UnsafePointer<lcm_recv_buf_t>?, channel: UnsafePointer<Int8>?, userData: UnsafeMutableRawPointer?)

I found an lcm_subscribe

Which refers to an lscm_msg_handler_t

typedef void(* lcm_msg_handler_t) (const lcm_recv_buf_t *rbuf, const char *channel, void *user_data)

Looking closely at this signature made me notice two issues:

  1. You are using the addressOf operator & on your pointers. This is incorrect. Either use that operator on a variable, or create pointers the way you are doing it (so just remove the &).
  2. All your parameters to cHandler are declared as optionals. This too is wrong - optionals are a Swift concept. Your callback signature is publishing a function that C can understand.

func cHandler(buffer: UnsafePointer<lcm_recv_buf_t>, channel: UnsafePointer<Int8>, userData: UnsafeMutableRawPointer)


Bad first idea

I'm extremely wary of structs and lifespan here.

Instead of

let userData = SubscribeUserData(handler: newMessageHandler)
var userDataPointer = UnsafeRawPointer(Unmanaged.passUnretained(userData).toOpaque())

self.subscribeUserData = userData
self.subscribeUserDataPointer = userDataPointer

Get a pointer to your member and use the returned struct immediately.

self.subscribeUserData = SubscribeUserData(handler: newMessageHandler)
self.subscribeUserDataPointer = UnsafeRawPointer(Unmanaged.passUnretained(self.subscribeUserData).toOpaque())

In particular, I think the danger is using a local variable in passUnretained.

If you think of the internal model as get a pointer to a location containing a reference then it makes more sense - your original is getting a pointer to the local variable on the stack.

Andy Dent
  • 17,578
  • 6
  • 88
  • 115
  • I tried this now, but the result is the same result except the error is not on the transformation of the OpaquePointer to SubscribeUserData. Now it is when calling the handler on the retried SubscribeUserData: `subscribeUserData.handler()`. Here the updated code https://imgur.com/B6D426y – Cla Jan 31 '18 at 14:24
  • Thanks Andy for the suggestions. The 1st one (& operator to be removed) is correct. The 2nd is not valid. Swift bridges that signature automatically with the one that `cHandler` is using. It's not a signature that I have written. I also tried to remove all the optionals, like you suggested, but then the signatures do not match anymore and I get a compiler error. With the suggested change (remove the & operator) I still obtain the same runtime error... – Cla Jan 31 '18 at 16:55