C doesn't have the luxury of closures, and by extension, neither does Swift code that tries to interoperate with imported C APIs. C functions that want to have a callback have to take a function pointer, which is @convention(c)
by Swift terms. If you try to pass a capturing closure, you'd get a compilation error:
a C function pointer cannot be formed from a closure that captures context
Working around this is pretty tricky, and requires some pretty sophisticated knowledge. (There are Objective-C blocks which can be used in C code, but they're not a standard part of C itself, and there are plenty of APIs that don't use them, so I don't see them as a solution to this problem.)
C APIs often simulate closures by using a pair parameters:
- A function pointer (for defining the behaviour)
- And an accompanying context pointer (for the contextual data). This is often called "userInfo" or "context" and is just a
void *
pointer.
When the callback is called, you're passed back your context as an argument, which you can cast to whatever type you had given it, and unpack your contextual variables from there.
Here's an example simulated C API:
import Foundation
import Dispatch
// Just a simple example, written in Swift so you don't need to set up a complex multi-lang project.
Pretend this was an imported C API.
func runCallback(
after delaySeconds: Int,
userInfo: UnsafeRawPointer?,
callback: @convention(c) (_ currentTime: UInt64, _ userInfo: UnsafeRawPointer?) -> Void
) {
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(delaySeconds) ) {
callback(
DispatchTime.now().uptimeNanoseconds, // Some values given to you by the C API
userInfo // Passes back your `userInfo` for you to access your context.
)
}
}
Calling it from Swift is a little tricky, in part because you need to be pretty careful with specifying how userInfo's memory should be managed.
Here's an example caller that uses a Swift object to store the context
func cAPICaller_userInfoObject() {
let i = 123 // Some local state we want to close over and use in our callback
class CallbackUserInfo {
let i: Int
init(i: Int) { self.i = i }
}
let userInfo = CallbackUserInfo(i: i) // Package up `i` into our own context object
runCallback(
after: 1,
userInfo: Unmanaged.passRetained(userInfo).toOpaque(),
callback: { userInfoP in
guard let userInfoP else { fatalError("The userInfo pointer was nil!") }
// Cast our raw pointer back to `CallbackUserInfo` and retain it.
let userInfo = Unmanaged<CallbackUserInfo>.fromOpaque(userInfoP).takeRetainedValue()
// Fish out whatever context we cared about
let i = userInfo.i
print("hello, world! \(i)")
}
)
}
In that example, we had to hand-write our own CallbackUserInfo
class. If we want to capture different values instead, we have to manually update that class' properties and initializer.
We can notice that this context object is really just a hand made closure capturing mechanism, which we already have in Swift. We can simplify this code by leveraging the userInfo
to pass a normal Swift closure, which can capture arbitrary context just like normal:
Example caller that uses a Swift closure to store the context
func cAPICaller_closure() {
let i = 123 // Some local state we want to close over and use in our callback
typealias ClosureType = (_ currentTimeNS: UInt64) -> Void
// This is just a simple Swift closure. It can capture variables like normal,
// and doesn't need to know/worry about the `userInfo` pointer
let closure: ClosureType = { currentTimeNS in
print("Hello, world! \(currentTimeNS) \(i)")
}
runCallback(
after: 1,
userInfo: Unmanaged.passRetained(closure as AnyObject).toOpaque(), // Needs `as AnyObject`? A bit odd, but sure.
callback: { currentTimeNS, closureP in
guard let closureP else { fatalError("The userInfo pointer was nil!") }
// Retain the pointer to get an object, and cast it to our Swift closure type
guard let closure = Unmanaged<AnyObject>.fromOpaque(closureP).takeRetainedValue() as? ClosureType else {
fatalError("The userInfo points to an object that wasn't our expected closure type.")
}
// Call our Swift closure, passing along the callback arguments, but not any `userInfo` pointer
closure(currentTimeNS)
}
)
}
I think this is an area where the compiler can help us. From what I understand, closures are already objects that have this two part combination of a function pointer (which contains the behaviour) and heap-allocated storage (which contains the captured context). It would be great if we could use these two values directly with our C API. By using the existing context-building capability of the compiler, we simplify the user's code, and also get to piggy-back off some optimizations for free. For example, if you're capturing only a single memory-managed object (commonly self
), and nothing else, then the pointer to that object can be used directly as the context pointer, without needing any other heap allocations.
I don't know how this would look syntactically. One idea might be to have a hypothetical API like createCCallback
which returns these values for us, and lets them pass them directly to our C API. It might look something like this:
func cAPICaller_proposedImprovement() {
let i = 123
// `createCCallback` takes a regular Swift function and return a `@convention(c)` function.
// It's special, like `withoutActuallyEscaping` (which takes an `@escaping` function and
// returns a non-escaping function), in that its type can't be expressed in the Swift type system.
//
// `@closureContext` is hypothetical syntax that indicates which arg is passes the closure context.
let (callback, userInfo) = createCCallback { currentTimeNS, @closureContext userInfo in
// This is just a proper Swift closure. It can capture variables like normal.
print("Hello, world! \(currentTimeNS) \(i)")
}
runCallback(
after: 1,
// Assumes that `userInfo` should always be passed retained. Is that right?
userInfo: userInfo,
// The `callback` already has the correct type, and knows how to unpack captured values
// from the `userInfo` object.
callback: callback
)
}
If we only have one argument, it's implied that it'll be the userInfo
pointer to use to store the context. It gets messier when you have a C callback has multiple arguments. It wouldn't obvious to the compiler which one of them should be the context pointer. One (clunky) is is to ask users to point it would with a marker annotation, like @closureContext
.
Do you guys think this is a problem worth solving? Is there any nicer way to simplify Swift code that calls C APIs?