For our app CleanPresenter, we need to use some older Mac APIs. For example the function CGDisplayRegisterReconfigurationCalback, to register a callback when the display configuration changes. This function was introduced 20 years ago in Mac OS X Panther.
The function CGDisplayRegisterReconfigurationCalback does exactly what it promise, it takes a function pointer and calls back when needed. But this function pointer is annoying, we would like a more “Swifty” way of writing this code.
Perfect for app demos & presentations; Simply plug in an iPhone and it automatically shows up on your Mac.
Learn more →
From functions to closures
The C API uses function pointers to specify what function to callback. In Swift we don’t use function pointers, instead we use closures. The difference between a function pointer and a closure is that a closure also captures its surrounding variables (it “closes over the environment”).
In the automatic Swift-to-C bridging, we can write the familiar curly-braces syntax, that looks a lot like Swifts closures, but we cannot capture external variables.
This is an example of a closure:
let start = Int(Date.now.timeIntervalSinceReferenceDate)
let result = myArray.map { x in start + x }
Here, the map takes a closure argument, this isn’t just a function that returns a value based on the argument x
, but it also captures the start
value. If map instead used a function pointer instead, there would be no way to write the equivalent code. We couldn’t refer to the start
variable, we could try to inline the Date.now
call, but then it would be called multiple times, changing the behaviour from the original code.
So, with just the function pointer, we can write this to print each display reconfiguration:
CGDisplayRegisterReconfigurationCallback({ displayID, flags, userInfo in
print("Reconfiguration of display:", displayID)
}, nil)
Fortunately, the CGDisplayRegisterReconfigurationCallback function is not so limited that it only takes a function pointer. It also takes an optional second argument userInfo
, this userInfo can be anything (it is an UnsafeMutableRawPointer
) but that userInfo gets passed back into the callback! This is the standard method in C of getting the equivalent to a closure; pass an extra pointer around, that pointer can contain the environment needed.
With this, we can now make a helper object to store the closure.
class DisplayReconfigurationClosure {
let closure: (CGDirectDisplayID, CGDisplayChangeSummaryFlags) -> Void
init(_ closure: @escaping (CGDirectDisplayID, CGDisplayChangeSummaryFlags) -> Void) {
self.closure = closure
}
}
And use that closure, so that we can also print the external start
variable:
let start = Date.now
let closure = DisplayReconfigurationClosure { (displayID: CGDirectDisplayID, flags: CGDisplayChangeSummaryFlags) in
print("Reconfiguration, display:", displayID, start)
}
self.stored = closure
let unsafeMutableRawPointer = withUnsafeMutablePointer(to: &closure) { pointer in
UnsafeMutableRawPointer(pointer)
}
CGDisplayRegisterReconfigurationCallback({ displayID, flags, userInfo in
userInfo?.load(as: DisplayReconfigurationClosure.self).closure(displayID, flags)
}, unsafeMutableRawPointer)
This involves a whole lot of pointer casting, and it is very specific to the DisplayReconfiguration case, but it does work.
We can clean this up some more, and push most of the code into a generic Closure2
class.
class Closure2<T1, T2> {
public struct Container {
let closure: (T1, T2) -> Void
}
var container: Container
var pointer: UnsafeMutableRawPointer
init(_ closure: @escaping (T1, T2) -> Void) {
self.container = Container(closure: closure)
self.pointer = withUnsafeMutablePointer(to: &container) { pointer in
UnsafeMutableRawPointer(pointer)
}
}
static func invoke(_ arg1: T1, _ arg2: T2, _ unsafeMutableRawPointer: UnsafeMutableRawPointer?) {
unsafeMutableRawPointer?.load(as: Container.self).closure(arg1, arg2)
}
}
Which we can use like so:
let start = Date.now
let closure = Closure2 { (displayID: CGDirectDisplayID, flags: CGDisplayChangeSummaryFlags) in
print("Reconfiguration, display:", displayID, start)
}
self.stored = closure
CGDisplayRegisterReconfigurationCallback({ displayID, flags, userInfo in
Closure2.invoke(displayID, flags, userInfo)
}, closure.pointer)
Swift 5.9 parameter packs
With parameter packs in Swift 5.9, we are no longer restricted to using a specific number of generic arguments. We can create a class with a variable number of generic arguments. Note that this does require runtime support as Parameter packs in generic types are only available in macOS 14.0.0 or newer.
class Closure<each T> {
public struct Container {
let closure: (repeat each T) -> Void
}
var container: Container
var pointer: UnsafeMutableRawPointer
init(_ closure: @escaping (repeat each T) -> Void) {
self.container = Container(closure: closure)
self.pointer = withUnsafeMutablePointer(to: &container) { pointer in
UnsafeMutableRawPointer(pointer)
}
}
static func invoke(_ args: repeat each T, unsafeMutableRawPointer: UnsafeMutableRawPointer?) {
unsafeMutableRawPointer?.load(as: Container.self).closure(repeat (each args))
}
}
Conclusion
When first encountering older C functions with callback, they might seem difficult to work with. But those older function are often well designed taking an additional context parameter, and with a little helper code, they can wrapped to work with a normal Swift closure.
Perfect for app demos & presentations; Simply plug in an iPhone and it automatically shows up on your Mac.
Learn more →
References