3

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?

Community
  • 1
  • 1
Henrik
  • 3,908
  • 27
  • 48

1 Answers1

5

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

Koen.
  • 25,449
  • 7
  • 83
  • 78
  • Hi, I have tried your solution, but there are some error `A C function pointer cannot be formed from a closure that captures context` which I cant solve. Can you help me? – rolyanos Mar 27 '18 at 16:27
  • Seems like the `takeRetainedValue()` in the callback is going to cause a crash, because it consumes an unbalanced retain each time the callback is invoked. Should be `takeUnretainedValue()` in the callback and then `takeRetainedValue()` if we were to remove the runloop later, no? – Bryan Jul 17 '22 at 08:38