Skip to content

Commit 07f5eaf

Browse files
committed
Take 2 of “[SDK] Fix multiple issues with Swift KVO”
This reproposes @lilyball’s fixes in swiftlang/swift#20103 while adding her fix to ensure observations are removed before observed objects in 32-bit testing.
1 parent cb9e0b7 commit 07f5eaf

File tree

3 files changed

+130
-84
lines changed

3 files changed

+130
-84
lines changed

Darwin/Foundation-swiftoverlay/NSObject.swift

Lines changed: 123 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -52,55 +52,79 @@ fileprivate extension NSObject {
5252
// must coexist, so it was renamed. The old name must not be used in the
5353
// new runtime.
5454
@objc private class __KVOKeyPathBridgeMachinery : NSObject {
55-
@nonobjc static var keyPathTable: [String : AnyKeyPath] = {
55+
@nonobjc static let swizzler: () = {
5656
/*
5757
Move all our methods into place. We want the following:
5858
__KVOKeyPathBridgeMachinery's automaticallyNotifiesObserversForKey:, and keyPathsForValuesAffectingValueForKey: methods replaces NSObject's versions of them
5959
NSObject's automaticallyNotifiesObserversForKey:, and keyPathsForValuesAffectingValueForKey: methods replace NSObject's __old_unswizzled_* methods
6060
NSObject's _old_unswizzled_* methods replace __KVOKeyPathBridgeMachinery's methods, and are never invoked
6161
*/
62+
threeWaySwizzle(#selector(NSObject.keyPathsForValuesAffectingValue(forKey:)), with: #selector(NSObject.__old_unswizzled_keyPathsForValuesAffectingValue(forKey:)))
63+
threeWaySwizzle(#selector(NSObject.automaticallyNotifiesObservers(forKey:)), with: #selector(NSObject.__old_unswizzled_automaticallyNotifiesObservers(forKey:)))
64+
}()
65+
66+
/// Performs a 3-way swizzle between `NSObject` and `__KVOKeyPathBridgeMachinery`.
67+
///
68+
/// The end result of this swizzle is the following:
69+
/// * `NSObject.selector` contains the IMP from `__KVOKeyPathBridgeMachinery.selector`
70+
/// * `NSObject.unswizzledSelector` contains the IMP from the original `NSObject.selector`.
71+
/// * __KVOKeyPathBridgeMachinery.selector` contains the (useless) IMP from `NSObject.unswizzledSelector`.
72+
///
73+
/// This swizzle is done in a manner that modifies `NSObject.selector` last, in order to ensure thread safety
74+
/// (by the time `NSObject.selector` is swizzled, `NSObject.unswizzledSelector` will contain the original IMP)
75+
@nonobjc private static func threeWaySwizzle(_ selector: Selector, with unswizzledSelector: Selector) {
6276
let rootClass: AnyClass = NSObject.self
6377
let bridgeClass: AnyClass = __KVOKeyPathBridgeMachinery.self
6478

65-
let dependentSel = #selector(NSObject.keyPathsForValuesAffectingValue(forKey:))
66-
let rootDependentImpl = class_getClassMethod(rootClass, dependentSel)!
67-
let bridgeDependentImpl = class_getClassMethod(bridgeClass, dependentSel)!
68-
method_exchangeImplementations(rootDependentImpl, bridgeDependentImpl) // NSObject <-> Us
69-
70-
let originalDependentImpl = class_getClassMethod(bridgeClass, dependentSel)! //we swizzled it onto this class, so this is actually NSObject's old implementation
71-
let originalDependentSel = #selector(NSObject.__old_unswizzled_keyPathsForValuesAffectingValue(forKey:))
72-
let dummyDependentImpl = class_getClassMethod(rootClass, originalDependentSel)!
73-
method_exchangeImplementations(originalDependentImpl, dummyDependentImpl) // NSObject's original version <-> NSObject's __old_unswizzled_ version
74-
75-
let autoSel = #selector(NSObject.automaticallyNotifiesObservers(forKey:))
76-
let rootAutoImpl = class_getClassMethod(rootClass, autoSel)!
77-
let bridgeAutoImpl = class_getClassMethod(bridgeClass, autoSel)!
78-
method_exchangeImplementations(rootAutoImpl, bridgeAutoImpl) // NSObject <-> Us
79-
80-
let originalAutoImpl = class_getClassMethod(bridgeClass, autoSel)! //we swizzled it onto this class, so this is actually NSObject's old implementation
81-
let originalAutoSel = #selector(NSObject.__old_unswizzled_automaticallyNotifiesObservers(forKey:))
82-
let dummyAutoImpl = class_getClassMethod(rootClass, originalAutoSel)!
83-
method_exchangeImplementations(originalAutoImpl, dummyAutoImpl) // NSObject's original version <-> NSObject's __old_unswizzled_ version
79+
// Swap bridge.selector <-> NSObject.unswizzledSelector
80+
let unswizzledMethod = class_getClassMethod(rootClass, unswizzledSelector)!
81+
let bridgeMethod = class_getClassMethod(bridgeClass, selector)!
82+
method_exchangeImplementations(unswizzledMethod, bridgeMethod)
8483

85-
return [:]
86-
}()
87-
88-
@nonobjc static var keyPathTableLock = NSLock()
89-
90-
@nonobjc fileprivate static func _bridgeKeyPath(_ keyPath: __owned AnyKeyPath) -> String {
91-
guard let keyPathString = keyPath._kvcKeyPathString else { fatalError("Could not extract a String from KeyPath \(keyPath)") }
92-
__KVOKeyPathBridgeMachinery.keyPathTableLock.lock()
93-
defer { __KVOKeyPathBridgeMachinery.keyPathTableLock.unlock() }
94-
__KVOKeyPathBridgeMachinery.keyPathTable[keyPathString] = keyPath
95-
return keyPathString
84+
// Swap NSObject.selector <-> NSObject.unswizzledSelector
85+
// NSObject.unswizzledSelector at this point contains the bridge IMP
86+
let rootMethod = class_getClassMethod(rootClass, selector)!
87+
method_exchangeImplementations(rootMethod, unswizzledMethod)
88+
}
89+
90+
private class BridgeKey : NSObject, NSCopying {
91+
let value: String
92+
93+
init(_ value: String) {
94+
self.value = value
95+
}
96+
97+
func copy(with zone: NSZone? = nil) -> Any {
98+
return self
99+
}
100+
101+
override func isEqual(_ object: Any?) -> Bool {
102+
return value == (object as? BridgeKey)?.value
103+
}
104+
105+
override var hash: Int {
106+
var hasher = Hasher()
107+
hasher.combine(ObjectIdentifier(BridgeKey.self))
108+
hasher.combine(value)
109+
return hasher.finalize()
110+
}
111+
}
112+
113+
/// Temporarily maps a `String` to an `AnyKeyPath` that can be retrieved with `_bridgeKeyPath(_:)`.
114+
///
115+
/// This uses a per-thread storage so key paths on other threads don't interfere.
116+
@nonobjc fileprivate static func _withBridgeableKeyPath(from keyPathString: String, to keyPath: AnyKeyPath, block: () -> Void) {
117+
_ = __KVOKeyPathBridgeMachinery.swizzler
118+
let key = BridgeKey(keyPathString)
119+
let oldValue = Thread.current.threadDictionary[key]
120+
Thread.current.threadDictionary[key] = keyPath
121+
defer { Thread.current.threadDictionary[key] = oldValue }
122+
block()
96123
}
97124

98125
@nonobjc fileprivate static func _bridgeKeyPath(_ keyPath:String?) -> AnyKeyPath? {
99126
guard let keyPath = keyPath else { return nil }
100-
__KVOKeyPathBridgeMachinery.keyPathTableLock.lock()
101-
defer { __KVOKeyPathBridgeMachinery.keyPathTableLock.unlock() }
102-
let path = __KVOKeyPathBridgeMachinery.keyPathTable[keyPath]
103-
return path
127+
return Thread.current.threadDictionary[BridgeKey(keyPath)] as? AnyKeyPath
104128
}
105129

106130
@objc override class func automaticallyNotifiesObservers(forKey key: String) -> Bool {
@@ -127,65 +151,85 @@ fileprivate extension NSObject {
127151
}
128152

129153
func _bridgeKeyPathToString(_ keyPath:AnyKeyPath) -> String {
130-
return __KVOKeyPathBridgeMachinery._bridgeKeyPath(keyPath)
131-
}
132-
133-
func _bridgeStringToKeyPath(_ keyPath:String) -> AnyKeyPath? {
134-
return __KVOKeyPathBridgeMachinery._bridgeKeyPath(keyPath)
154+
guard let keyPathString = keyPath._kvcKeyPathString else { fatalError("Could not extract a String from KeyPath \(keyPath)") }
155+
return keyPathString
135156
}
136157

137158
// NOTE: older overlays called this NSKeyValueObservation. We now use
138159
// that name in the source code, but add an underscore to the runtime
139160
// name to avoid conflicts when both are loaded into the same process.
140161
@objc(_NSKeyValueObservation)
141162
public class NSKeyValueObservation : NSObject {
142-
143-
@nonobjc weak var object : NSObject?
144-
@nonobjc let callback : (NSObject, NSKeyValueObservedChange<Any>) -> Void
145-
@nonobjc let path : String
146-
147-
//workaround for <rdar://problem/31640524> Erroneous (?) error when using bridging in the Foundation overlay
148-
@nonobjc static var swizzler : NSKeyValueObservation? = {
149-
let bridgeClass: AnyClass = NSKeyValueObservation.self
150-
let observeSel = #selector(NSObject.observeValue(forKeyPath:of:change:context:))
151-
let swapSel = #selector(NSKeyValueObservation._swizzle_me_observeValue(forKeyPath:of:change:context:))
152-
let rootObserveImpl = class_getInstanceMethod(bridgeClass, observeSel)!
153-
let swapObserveImpl = class_getInstanceMethod(bridgeClass, swapSel)!
154-
method_exchangeImplementations(rootObserveImpl, swapObserveImpl)
155-
return nil
156-
}()
157-
158-
fileprivate init(object: NSObject, keyPath: AnyKeyPath, callback: @escaping (NSObject, NSKeyValueObservedChange<Any>) -> Void) {
159-
path = _bridgeKeyPathToString(keyPath)
160-
let _ = NSKeyValueObservation.swizzler
161-
self.object = object
162-
self.callback = callback
163+
// We use a private helper class as the actual observer. This lets us attach the helper as an associated object
164+
// to the object we're observing, thus ensuring the helper will still be alive whenever a KVO change notification
165+
// is broadcast, even on a background thread.
166+
//
167+
// For the associated object, we use the Helper instance itself as its own key. This guarantees key uniqueness.
168+
private class Helper : NSObject {
169+
@nonobjc weak var object : NSObject?
170+
@nonobjc let path: String
171+
@nonobjc let callback : (NSObject, NSKeyValueObservedChange<Any>) -> Void
172+
173+
// workaround for <rdar://problem/31640524> Erroneous (?) error when using bridging in the Foundation overlay
174+
// specifically, overriding observeValue(forKeyPath:of:change:context:) complains that it's not Obj-C-compatible
175+
@nonobjc static let swizzler: () = {
176+
let bridgeClass: AnyClass = Helper.self
177+
let observeSel = #selector(NSObject.observeValue(forKeyPath:of:change:context:))
178+
let swapSel = #selector(Helper._swizzle_me_observeValue(forKeyPath:of:change:context:))
179+
let swapObserveMethod = class_getInstanceMethod(bridgeClass, swapSel)!
180+
class_addMethod(bridgeClass, observeSel, method_getImplementation(swapObserveMethod), method_getTypeEncoding(swapObserveMethod))
181+
}()
182+
183+
@nonobjc init(object: NSObject, keyPath: AnyKeyPath, options: NSKeyValueObservingOptions, callback: @escaping (NSObject, NSKeyValueObservedChange<Any>) -> Void) {
184+
_ = Helper.swizzler
185+
let path = _bridgeKeyPathToString(keyPath)
186+
self.object = object
187+
self.path = path
188+
self.callback = callback
189+
super.init()
190+
objc_setAssociatedObject(object, associationKey(), self, .OBJC_ASSOCIATION_RETAIN)
191+
__KVOKeyPathBridgeMachinery._withBridgeableKeyPath(from: path, to: keyPath) {
192+
object.addObserver(self, forKeyPath: path, options: options, context: nil)
193+
}
194+
}
195+
196+
@nonobjc func invalidate() {
197+
guard let object = self.object else { return }
198+
object.removeObserver(self, forKeyPath: path, context: nil)
199+
objc_setAssociatedObject(object, associationKey(), nil, .OBJC_ASSOCIATION_ASSIGN)
200+
self.object = nil
201+
}
202+
203+
@nonobjc private func associationKey() -> UnsafeRawPointer {
204+
return UnsafeRawPointer(Unmanaged.passUnretained(self).toOpaque())
205+
}
206+
207+
@objc private func _swizzle_me_observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSString : Any]?, context: UnsafeMutableRawPointer?) {
208+
guard let object = object as? NSObject, object === self.object, let change = change else { return }
209+
let rawKind:UInt = change[NSKeyValueChangeKey.kindKey.rawValue as NSString] as! UInt
210+
let kind = NSKeyValueChange(rawValue: rawKind)!
211+
let notification = NSKeyValueObservedChange(kind: kind,
212+
newValue: change[NSKeyValueChangeKey.newKey.rawValue as NSString],
213+
oldValue: change[NSKeyValueChangeKey.oldKey.rawValue as NSString],
214+
indexes: change[NSKeyValueChangeKey.indexesKey.rawValue as NSString] as! IndexSet?,
215+
isPrior: change[NSKeyValueChangeKey.notificationIsPriorKey.rawValue as NSString] as? Bool ?? false)
216+
callback(object, notification)
217+
}
163218
}
164219

165-
fileprivate func start(_ options: NSKeyValueObservingOptions) {
166-
object?.addObserver(self, forKeyPath: path, options: options, context: nil)
220+
@nonobjc private let helper: Helper
221+
222+
fileprivate init(object: NSObject, keyPath: AnyKeyPath, options: NSKeyValueObservingOptions, callback: @escaping (NSObject, NSKeyValueObservedChange<Any>) -> Void) {
223+
helper = Helper(object: object, keyPath: keyPath, options: options, callback: callback)
167224
}
168225

169226
///invalidate() will be called automatically when an NSKeyValueObservation is deinited
170227
@objc public func invalidate() {
171-
object?.removeObserver(self, forKeyPath: path, context: nil)
172-
object = nil
173-
}
174-
175-
@objc func _swizzle_me_observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSString : Any]?, context: UnsafeMutableRawPointer?) {
176-
guard let ourObject = self.object, object as? NSObject == ourObject, let change = change else { return }
177-
let rawKind:UInt = change[NSKeyValueChangeKey.kindKey.rawValue as NSString] as! UInt
178-
let kind = NSKeyValueChange(rawValue: rawKind)!
179-
let notification = NSKeyValueObservedChange(kind: kind,
180-
newValue: change[NSKeyValueChangeKey.newKey.rawValue as NSString],
181-
oldValue: change[NSKeyValueChangeKey.oldKey.rawValue as NSString],
182-
indexes: change[NSKeyValueChangeKey.indexesKey.rawValue as NSString] as! IndexSet?,
183-
isPrior: change[NSKeyValueChangeKey.notificationIsPriorKey.rawValue as NSString] as? Bool ?? false)
184-
callback(ourObject, notification)
228+
helper.invalidate()
185229
}
186230

187231
deinit {
188-
object?.removeObserver(self, forKeyPath: path, context: nil)
232+
invalidate()
189233
}
190234
}
191235

@@ -197,16 +241,14 @@ extension _KeyValueCodingAndObserving {
197241
options: NSKeyValueObservingOptions = [],
198242
changeHandler: @escaping (Self, NSKeyValueObservedChange<Value>) -> Void)
199243
-> NSKeyValueObservation {
200-
let result = NSKeyValueObservation(object: self as! NSObject, keyPath: keyPath) { (obj, change) in
244+
return NSKeyValueObservation(object: self as! NSObject, keyPath: keyPath, options: options) { (obj, change) in
201245
let notification = NSKeyValueObservedChange(kind: change.kind,
202246
newValue: change.newValue as? Value,
203247
oldValue: change.oldValue as? Value,
204248
indexes: change.indexes,
205249
isPrior: change.isPrior)
206250
changeHandler(obj as! Self, notification)
207251
}
208-
result.start(options)
209-
return result
210252
}
211253

212254
public func willChangeValue<Value>(for keyPath: __owned KeyPath<Self, Value>) {

Darwin/Foundation-swiftoverlay/NSSet.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ extension Set {
1717
///
1818
/// The provided `NSSet` will be copied to ensure that the copy can
1919
/// not be mutated by other code.
20-
fileprivate init(_cocoaSet: __shared AnyObject) {
20+
fileprivate init(_cocoaSet: __shared NSSet) {
2121
assert(_isBridgedVerbatimToObjectiveC(Element.self),
2222
"Set can be backed by NSSet _variantStorage only when the member type can be bridged verbatim to Objective-C")
2323
// FIXME: We would like to call CFSetCreateCopy() to avoid doing an

Darwin/Foundation-swiftoverlay/NSSortDescriptor.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,22 @@
1111
//===----------------------------------------------------------------------===//
1212

1313
@_exported import Foundation // Clang module
14+
import ObjectiveC
1415

1516
extension NSSortDescriptor {
1617
public convenience init<Root, Value>(keyPath: KeyPath<Root, Value>, ascending: Bool) {
1718
self.init(key: _bridgeKeyPathToString(keyPath), ascending: ascending)
19+
objc_setAssociatedObject(self, UnsafeRawPointer(&associationKey), keyPath, .OBJC_ASSOCIATION_RETAIN)
1820
}
1921

2022
public convenience init<Root, Value>(keyPath: KeyPath<Root, Value>, ascending: Bool, comparator cmptr: @escaping Foundation.Comparator) {
2123
self.init(key: _bridgeKeyPathToString(keyPath), ascending: ascending, comparator: cmptr)
24+
objc_setAssociatedObject(self, UnsafeRawPointer(&associationKey), keyPath, .OBJC_ASSOCIATION_RETAIN)
2225
}
2326

2427
public var keyPath: AnyKeyPath? {
25-
guard let key = self.key else { return nil }
26-
return _bridgeStringToKeyPath(key)
28+
return objc_getAssociatedObject(self, UnsafeRawPointer(&associationKey)) as? AnyKeyPath
2729
}
2830
}
31+
32+
private var associationKey: ()?

0 commit comments

Comments
 (0)