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?

1
  • Just so you know; I've added a more extensive example to my answer Commented Jan 8, 2017 at 14:02

1 Answer 1

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

Sign up to request clarification or add additional context in comments.

2 Comments

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?
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?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.