swifttypescallbackiokitcfrunloop

Create a CFRunLoopSourceRef using IOPSNotificationCreateRunLoopSource in Swift


I am trying to subscribe to changes in power state on macOS. I discovered there is a way using IOKit, though it is a bit convoluted. I need to import it using #import <IOKit/ps/IOPowerSources.h> in an ObjC Bridging header. Then I get access to the function IOPSNotificationCreateRunLoopSource, which has the signature:

IOPSNotificationCreateRunLoopSource(_ callback: IOPowerSourceCallbackType!, _ context: UnsafeMutablePointer<Void>!) -> Unmanaged<CFRunLoopSource>!

I got some help from the answer in Callback method to Apple run loop, but still doesn't manage to create a function of type IOPowerSourceCallbackType in Swift. What is the missing piece to have this compile?


Solution

  • The issue is that IOPowerSourceCallbackType is a C function.

    According to Apple's documentation these functions are available as closures:

    C function pointers are imported into Swift as closures with C function pointer calling convention

    https://developer.apple.com/library/content/documentation/Swift/Conceptual/BuildingCocoaApps/InteractingWithCAPIs.html#//apple_ref/doc/uid/TP40014216-CH8-ID148

    So the easiest way is to use a closure:

    IOPSNotificationCreateRunLoopSource({ (context: UnsafeMutableRawPointer?) in
        debugPrint("Power source changed")
    }, &context)
    

    A second option is to use a top-level function:

    func powerSourceChanged(arg: UnsafeMutableRawPointer?) {
        debugPrint("Power source changed")
    }
    IOPSNotificationCreateRunLoopSource(powerSourceChanged, &context)
    

    For reference the complete implementation of how I'm using this:

    class WindowController: NSWindowController {
        static var context = 0
    
        override func windowDidLoad() {
            super.windowDidLoad()
            let loop: CFRunLoopSource = IOPSNotificationCreateRunLoopSource({ (context: UnsafeMutableRawPointer?) in
                debugPrint("Power source changed")
            }, &WindowController.context).takeRetainedValue() as CFRunLoopSource
            CFRunLoopAddSource(CFRunLoopGetCurrent(), loop, CFRunLoopMode.defaultMode)
        }
    }
    

    UPDATE

    To let it interact with the instance the loop was setup from, you have to pass self as context, however self isn't a pointer.

    When you try to pass self as pointer by prepending it with & (&self), you'll get an error that self is immutable.

    To convert it a to an opaque pointer you can use the Unmanaged class:

    let opaque = Unmanaged.passRetained(self).toOpaque()
    

    Which then can be used as an UnsafeMutableRawPointer:

    let context = UnsafeMutableRawPointer(opaque)
    

    What we can use as the context for IOPSNotificationCreateRunLoopSource.

    And then in the callback, by using the Unmanaged class again, we can resolve this pointer back to its initiating instance:

    let opaque = Unmanaged<WindowController>.fromOpaque(context!)
    let _self = opaque.takeRetainedValue()
    

    Full example:

    func PowerSourceChanged(context: UnsafeMutableRawPointer?) {
        let opaque = Unmanaged<WindowController>.fromOpaque(context!)
        let _self = opaque.takeRetainedValue()
        _self.powerSourceChanged()
    }
    
    class WindowController: NSWindowController {
        override func windowDidLoad() {
            super.windowDidLoad()
            let opaque = Unmanaged.passRetained(self).toOpaque()
            let context = UnsafeMutableRawPointer(opaque)
            let loop: CFRunLoopSource = IOPSNotificationCreateRunLoopSource(
                PowerSourceChanged,
                context
            ).takeRetainedValue() as CFRunLoopSource
            CFRunLoopAddSource(CFRunLoopGetCurrent(), loop, CFRunLoopMode.defaultMode)
        }
    
        func powerSourceChanged() {
            debugLog("Power source changed")
        }
    }
    

    Bonus

    A related article about CFunction pointers