0

I am trying to wrap a C library in a Swift package. The library has a struct that holds some function pointers. To expose a nicer interface I would like users to be able to pass normal Swift functions (i.e. not handling all the C oddities) and then create a thin wrapper around those functions that handles the type conversions etc.

Here's my approach so far:

public struct SwiftifiedLib {
    public init(
        callback: (Int) -> Void,
        complexCallback: (Int, String, Int) -> Void
    ) {
        // cLibStruct is defined in the wrapped C library
        var structInstance = cLibStruct()
        structInstance.callback = { (fd: Int32) -> Void in
            callback(Int(fd))
        }
        structInstance.complexCallback = {
        (fd: Int32, msg: UnsafePointer<UInt8>?, size: UInt64, type: Int32) -> Void in
            let msgBuffer = UnsafeBufferPointer(start: msg, count: Int(size))
            let msgString = String(decoding: msgBuffer, as: UTF8.self)
            complexCallback(Int(fd), msgString, Int(type))
        }
    }
}

Compilation fails with

error: a C function pointer cannot be formed from a closure that captures context
        structInstance.callback = { (fd: Int32) in
                                  ^

I understand that this is expected, given the way I am trying to pass the callback functions. However, I feel like this is a common enough use case for a workaround to exist.

Is there a pattern or construct/type cast/etc that allows me to break that dependency on the context?

I have also tried to use an instance function to wrap the argument and then make the instance function passable as described here but realised that this was just shifting the problem.

Jan Hettenkofer
  • 139
  • 4
  • 13
  • What is the compile error? – Shadowrun Sep 24 '21 at 15:35
  • It's failing with `a C function pointer cannot be formed from a closure that captures context`. I updated the question to include this info. – Jan Hettenkofer Sep 24 '21 at 15:43
  • Your closures are capturing variables from the context, they capture the `callback` and `complexCallback` arguments, so they cannot be converted to C functions. – Cristik Sep 24 '21 at 18:27
  • Indeed, that's the thing I'm trying to figure out: to allow users to pass their own callbacks, I _need_ to capture context (AFAICT), but to pass the callbacks as function pointers I _cannot_ capture context. I feel like this should be a common enough use case for a workaround to exist. – Jan Hettenkofer Sep 27 '21 at 07:31
  • @JanHettenkofer take a look at [Closure Capture Context Swift](https://stackoverflow.com/q/35346955/1974224) to see how a C function should look like in order to be usable as you want. And note that you'll gonna need a reference type instead of a struct, as stack-allocated values become invalid when the function returns. – Cristik Sep 27 '21 at 07:54

1 Answers1

0

First things first, you need to use a class, not a struct. The reason is the C callbacks are executed at discrete times, so you'll want to make sure they execute on the same context.

Second, the C callbacks should have a context parameter, that can be used from the Swift side in order to inject the instance of that class. Unfortunately in your case there doesn't seem to be an argument like this, otherwise the solution(s) for this SO post would've work.

Without knowing mor about the C library callbacks, the solution so far is to create a Singleton class, which gets passed to the C library:

class CLibraryHandler {
    static let shared = CLibraryHandler()

    private var structInstance: cLibStruct()

    init() {
        structInstance = cLibStruct()
        structInstance.callback = { fd in
            CLibraryHandler.shared.simpleCallback(fd)
        }
        structInstance.complexCallback = { fd, msg, size, type in
            CLibraryHandler.shared.complexCallback(fd: fd, msg: msg, size: size, type: type)
        }
    }

    private func simpleCallback(fd: Int32) {
    }

    private func complexCallback(fd: Int32, msg: UnsafePointer<UInt8>?, size: UInt64, type: Int32) {
    }
}

What's left to do here is to allow multiple subscribers to receive events when the C library publishes them, this can be achieved via multiple means: delegates, array of callbacks, Combine, notifications (though I don't recommend it).

Cristik
  • 30,989
  • 25
  • 91
  • 127