14

Has anyone managed to get this function to work in Swift?

Here is a reference SO post from last year: Using CGEventTapCreate Trouble with parameters in Swift

Apple Doc: https://developer.apple.com/library/prerelease/mac/documentation/Carbon/Reference/QuartzEventServicesRef/index.html#//apple_ref/c/func/CGEventTapCreate

Here is how the CGEventTapCallBack is defined:

typealias CGEventTapCallBack = CFunctionPointer<((CGEventTapProxy, CGEventType, CGEvent!, UnsafeMutablePointer<Void>) -> Unmanaged<CGEvent>!)>

Here is how I've written the block:

let eventTapCallBackBlock : @objc_block
(CGEventTapProxy, CGEventType, CGEventRef, UnsafeMutablePointer<Void>) -> CGEventRef =
{ (eventTapProxy: CGEventTapProxy, eventType: CGEventType, event: CGEventRef, refcon: UnsafeMutablePointer<Void>) in
  return event
}

Then I've called CGEventTapCreate with the callback parameter like unsafeBitCast(eventTapCallBackBlock, CGEventTapCallBack.self)

I get a valid CFMachPortRef back, but at run time I get an access violation exception on first event. It would "seem" I'm close to a solution in swift in its current release state.

Using Xcode Version 6.4

Community
  • 1
  • 1
chrisp
  • 2,181
  • 4
  • 27
  • 35

1 Answers1

38

The callback parameter of CGEventTapCreate() is a C function pointer, and in Swift 1.x it is not possible call it with a Swift function argument.

However, in Swift 2 (Xcode 7), C functions that take function pointer arguments can be called using closures or global functions (with the restriction that the closure must not capture any of its local context).

As an example, here is a complete translation of Receiving, Filtering, and Modifying Key Presses and Releases to Swift 3.

import Foundation
import CoreGraphics

func myCGEventCallback(proxy: CGEventTapProxy, type: CGEventType, event: CGEvent, refcon: UnsafeMutableRawPointer?) -> Unmanaged<CGEvent>? {
    
    if [.keyDown , .keyUp].contains(type) {
        var keyCode = event.getIntegerValueField(.keyboardEventKeycode)
        if keyCode == 0 {
            keyCode = 6
        } else if keyCode == 6 {
            keyCode = 0
        }
        event.setIntegerValueField(.keyboardEventKeycode, value: keyCode)
    }
    return Unmanaged.passUnretained(event)
}

let eventMask = (1 << CGEventType.keyDown.rawValue) | (1 << CGEventType.keyUp.rawValue)
guard let eventTap = CGEvent.tapCreate(tap: .cgSessionEventTap,
                                      place: .headInsertEventTap,
                                      options: .defaultTap,
                                      eventsOfInterest: CGEventMask(eventMask),
                                      callback: myCGEventCallback,
                                      userInfo: nil) else {
                                        print("failed to create event tap")
                                        exit(1)
}

let runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0)
CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, .commonModes)
CGEvent.tapEnable(tap: eventTap, enable: true)
CFRunLoopRun()
Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
  • Thanks for the response. Have had any experience with @objc_blocks to solve this situation? – chrisp Aug 08 '15 at 22:20
  • @jacobsd: CGEventTapCreate() takes a C function pointer as a parameter, not an Objective-C block. I don't think that you can solve that with a Swift 1.2 (Xcode 6.4) callback. – Martin R Aug 08 '15 at 23:09
  • 4
    Holy Heck, that's uglier than the C/Objective-C versions... sigh... at least it's *possible* – uchuugaka Nov 02 '15 at 01:16
  • 1
    Thanks for updating it for Swift 2! – uchuugaka Nov 02 '15 at 01:20
  • 1
    Any idea on how to pass a value for the refcon parameter? You're example is passing nil, and I haven't had any luck trying to find a way to pass in a value for a Swift type that I can then unwrap in the callback function and call a method on (e.g. a delegate object). – chinabuffet Nov 02 '15 at 11:58
  • @chinabuffet: Have a look at http://stackoverflow.com/questions/33294620/how-to-cast-self-to-unsafemutablepointervoid-type-in-swift. The same technique should work here as well. – Martin R Nov 02 '15 at 12:08
  • Ah dang, I had it pretty close at one point, just wasn't taking it back from the UnsafePointer to the original Type quite right... thanks! – chinabuffet Nov 03 '15 at 00:01
  • 3
    It may be worth pointing out (since it took me a few minutes to work out what the issue was) that the function callback must be an actual func, not a class method. – James Waldrop Jan 04 '16 at 07:52
  • 1
    @JamesWaldrop: Yes, the callback cannot be a instance or class method. I *can* be a closure (which captures no context). I'll update the answer later to make that more clear. Thank you for the feedback! – Martin R Jan 04 '16 at 09:04
  • Any good reason to check `if [.keyDown , .keyUp].contains(type)` when `eventMask = (1 << CGEventType.keyDown.rawValue) | (1 << CGEventType.keyUp.rawValue)` ? – aristidesfl Feb 28 '17 at 15:38
  • @aristidesfl: Good question. I adopted that from the linked-to C code. – Martin R Feb 28 '17 at 15:41
  • @MartinR do you want to take a shot at this question? https://stackoverflow.com/questions/17773225/cgeventpost-hold-a-key-shift – aristidesfl Mar 08 '17 at 13:07
  • This stopped working for me since macOS 12.6. No exceptions, but the callback doesn't get called anymore. Any idea of what the problem could be? Thanks! – aristidesfl Oct 20 '22 at 16:37
  • @aristidesfl: I have just tested it on my MacBook Air M2 with macOS 12.6, and it works as expected. – Martin R Oct 20 '22 at 17:40
  • 1
    This solution leaks memory: `Unmanaged.passRetained` should be `Unmanaged.passUnretained`. I verified this by copying the solution into a new project and monitoring the memory in Activity Monitor while I typed as well as by profiling in Instruments on my own app. Why? I don't fully understand it but `passRetained` leaves the refcount at +1 which means either we will call ARC's release on it or the calling code will call ARC's release on it. We don't release it and I'm guessing the calling API doesn't use ARC and thus never calls release, causing a leak. – mcomella Dec 20 '22 at 00:48
  • @mcomella: You are right, I can confirm your observations. It seems that I misinterpreted the [documentation](https://developer.apple.com/documentation/coregraphics/cgeventtapcallback?language) (“After the new event has been passed back to the event system, the new event will be released along with the original event.”) which made me think that the returned event must be +1 retained. Anyway, thanks for the notice, I have updated the code accordingly. – Martin R Dec 20 '22 at 06:12
  • Since a recent MacOS update, probably 12.6.3, only the .listenOnly option can be used (not .defaultTap). Otherwise tapCreate returns nil, even when debugging the process as root in xcode. Also the callback is never called. – Rip Mar 24 '23 at 21:10
  • @Rip: It still works for me (on macOS 13) if you acquire the privilege to use the accessibility functions, e.g. using the code here: https://stackoverflow.com/a/52160006/1187415. It also works if the process is running as root – Martin R Mar 25 '23 at 20:02