Wouter01
(Wouter Hennen)
1
When working with older APIs, I oftentimes wrap them in an AsyncStream to create a nicer API when working with Swift Concurrency. I make use of AsyncStream.Continuation.onTermination to control when to stop the older API from sending updates. This mostly happens with two cases:
- The API has an explicit "stop" function which must be called.
- The API is some kind of reference type, which will automatically cancel when dereferenced.
For example, take this function
import AppKit
public enum Keyboard {
public static func updates() -> AsyncStream<NSEvent.ModifierFlags>? {
let (stream, continuation) = AsyncStream.makeStream(of: NSEvent.ModifierFlags.self)
let monitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in
continuation.yield(event.modifierFlags)
return event
}
guard let monitor else { return nil }
continuation.onTermination = { _ in
NSEvent.removeMonitor(monitor) // Capture of 'monitor' with non-sendable type 'Any' in a `@Sendable` closure
}
return stream
}
}
When trying to cancel the monitor, I will get a warning, because the onTermination closure is Sendable, and the Any type of monitor is not. I was hoping that this would be automagically fixed by enabling Region-based isolation, but this doesn't seem to have any effect.
What would be the recommended approach to tackle this issue?
Something I was wondering too: if the onTermination closure would be transferring (like described in this post), would this fix the warning? My assumption is that the current warning appears because the compiler cannot guarantee that the monitor would not be passed around, which transferring could enforce?
eskimo
(Quinn “The Eskimo!”)
2
I’m not going to wade into this in the general case, but I’m very concerned about the specific example you posted. Unless otherwise documented AppKit is a main-thread-only framework. AFAIK NSEvent monitors are not documented to be thread safe. Thus, all of that work would need to be done on the main actor.
Share and Enjoy
Quinn “The Eskimo!” @ DTS @ Apple
3 Likes
Wouter01
(Wouter Hennen)
3
Thank you for letting me know!
I cannot find anything about the thread safety of monitors, so I should indeed assume they should only be called on the main actor.
The onTermination function is nonisolated though. I am able to fix all warnings by using the following code
let stopMonitor: @MainActor @Sendable () -> Void = {
NSEvent.removeMonitor(monitor)
}
continuation.onTermination = { _ in
Task {
await stopMonitor()
}
}
However, this introduces a new Task which isn't ideal. Would there be a better way to achieve this?
kiel
(Kiel Gillard)
4
eskimo
(Quinn “The Eskimo!”)
5
However, this introduces a new Task which isn't ideal. Would
there be a better way to achieve this?
Given that:
I don’t see any way to avoid that task.
Share and Enjoy
Quinn “The Eskimo!” @ DTS @ Apple
1 Like
Wouter01
(Wouter Hennen)
6
I will look into this, thank you!