Skip to content

Commit 01b83ba

Browse files
committed
[AnyHashable] Handle comparisons/casting for wrappers around bridged types.
Swift value types are their bridged Objective-C classes can have different hash values. To address this, AnyHashable's responds to the _HasCustomAnyHashableRepresentation protocol, which bridge objects of those class types---NSString, NSNumber, etc---into their Swift counterparts. That way, we get consistent (Swift) hashing behavior across platforms. However, there are cases where multiple Swift value types map to the same Objective-C class type. In such cases, AnyHashable ends up converting the object of class type back to some canonical type. For example, an NS_STRING_ENUM (such as (NS)RunLoopMode) is a Swift wrapper around a String. If an (NS)RunLoopMode is placed into an AnyHashable, it maintains it's Swift type identity (which is correct behavior). If it is bridged to Objective-C, it becomes an NSString; if that NSString is placed into an AnyHashable, it produces a String. The hash values still line up, but equality of the AnyHashable values fails, which breaks when (for example) a dictionary with AnyHashable keys is used from Objective-C. See SR-2648 / rdar://problem/27992351 for a case where this breaks interoperability. To address this problem, make AnyHashable's casting and equality sensitive to the origin of the hashed value: if the AnyHashable was created through a _HasCustomAnyHashableRepresentation conformance, treat comparisons/casting from it as "fuzzy": * For equality, if one of the AnyHashable's comes from a custom representation (e.g., it originated with an Objective-C type like NSString) but the other did not, bridge the value of the *other* AnyHashable to Objective-C, re-wrap it in an AnyHashable, and compare that. This allows, e.g., an (NS)RunLoopMode created in Swift to compare to an NSString constant with the same string value. * For casting, if the AnyHashable we're casting from came from a custom representation and the cast would fail, bridge to Objective-C and then initiate the cast again. This allows an NSString to be casted to (NS)RunLoopMode. Fixes SR-2648 / rdar://problem/27992351. (cherry picked from commit 6f34118)
1 parent eb68737 commit 01b83ba

File tree

3 files changed

+147
-5
lines changed

3 files changed

+147
-5
lines changed

stdlib/public/core/AnyHashable.swift

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,11 @@ internal protocol _AnyHashableBox {
4141
var _typeID: ObjectIdentifier { get }
4242
func _unbox<T : Hashable>() -> T?
4343

44-
func _isEqual(to: _AnyHashableBox) -> Bool
44+
/// Determine whether values in the boxes are equivalent.
45+
///
46+
/// - Returns: `nil` to indicate that the boxes store different types, so
47+
/// no comparison is possible. Otherwise, contains the result of `==`.
48+
func _isEqual(to: _AnyHashableBox) -> Bool?
4549
var _hashValue: Int { get }
4650

4751
var _base: Any { get }
@@ -62,11 +66,11 @@ internal struct _ConcreteHashableBox<Base : Hashable> : _AnyHashableBox {
6266
return (self as _AnyHashableBox as? _ConcreteHashableBox<T>)?._baseHashable
6367
}
6468

65-
internal func _isEqual(to rhs: _AnyHashableBox) -> Bool {
69+
internal func _isEqual(to rhs: _AnyHashableBox) -> Bool? {
6670
if let rhs: Base = rhs._unbox() {
6771
return _baseHashable == rhs
6872
}
69-
return false
73+
return nil
7074
}
7175

7276
internal var _hashValue: Int {
@@ -85,6 +89,18 @@ internal struct _ConcreteHashableBox<Base : Hashable> : _AnyHashableBox {
8589
}
8690
}
8791

92+
#if _runtime(_ObjC)
93+
// Retrieve the custom AnyHashable representation of the value after it
94+
// has been bridged to Objective-C. This mapping to Objective-C and back
95+
// turns a non-custom representation into a custom one, which is used as
96+
// the lowest-common-denominator for comparisons.
97+
func _getBridgedCustomAnyHashable<T>(_ value: T) -> AnyHashable? {
98+
let bridgedValue = _bridgeAnythingToObjectiveC(value)
99+
return (bridgedValue as?
100+
_HasCustomAnyHashableRepresentation)?._toCustomAnyHashable()
101+
}
102+
#endif
103+
88104
/// A type-erased hashable value.
89105
///
90106
/// The `AnyHashable` type forwards equality comparisons and hashing operations
@@ -106,6 +122,7 @@ internal struct _ConcreteHashableBox<Base : Hashable> : _AnyHashableBox {
106122
/// print(descriptions[AnyHashable(Set(["a", "b"]))]!) // prints "a set of strings"
107123
public struct AnyHashable {
108124
internal var _box: _AnyHashableBox
125+
internal var _usedCustomRepresentation: Bool
109126

110127
/// Creates a type-erased hashable value that wraps the given instance.
111128
///
@@ -129,17 +146,20 @@ public struct AnyHashable {
129146
if let customRepresentation =
130147
(base as? _HasCustomAnyHashableRepresentation)?._toCustomAnyHashable() {
131148
self = customRepresentation
149+
self._usedCustomRepresentation = true
132150
return
133151
}
134152

135153
self._box = _ConcreteHashableBox(0 as Int)
154+
self._usedCustomRepresentation = false
136155
_stdlib_makeAnyHashableUpcastingToHashableBaseType(
137156
base,
138157
storingResultInto: &self)
139158
}
140159

141160
internal init<H : Hashable>(_usingDefaultRepresentationOf base: H) {
142161
self._box = _ConcreteHashableBox(base)
162+
self._usedCustomRepresentation = false
143163
}
144164

145165
/// The value wrapped by this instance.
@@ -162,7 +182,21 @@ public struct AnyHashable {
162182
/// a downcast on `base`.
163183
internal
164184
func _downCastConditional<T>(into result: UnsafeMutablePointer<T>) -> Bool {
165-
return _box._downCastConditional(into: result)
185+
// Attempt the downcast.
186+
if _box._downCastConditional(into: result) { return true }
187+
188+
#if _runtime(_ObjC)
189+
// If we used a custom representation, bridge to Objective-C and then
190+
// attempt the cast from there.
191+
if _usedCustomRepresentation {
192+
if let value = _bridgeAnythingToObjectiveC(_box._base) as? T {
193+
result.initialize(to: value)
194+
return true
195+
}
196+
}
197+
#endif
198+
199+
return false
166200
}
167201
}
168202

@@ -193,7 +227,34 @@ extension AnyHashable : Equatable {
193227
/// - lhs: A type-erased hashable value.
194228
/// - rhs: Another type-erased hashable value.
195229
public static func == (lhs: AnyHashable, rhs: AnyHashable) -> Bool {
196-
return lhs._box._isEqual(to: rhs._box)
230+
// If they're equal, we're done.
231+
if let result = lhs._box._isEqual(to: rhs._box) { return result }
232+
233+
#if _runtime(_ObjC)
234+
// If one used a custom representation but the other did not, bridge
235+
// the one that did *not* use the custom representation to Objective-C:
236+
// if the bridged result has a custom representation, compare those custom
237+
// custom representations.
238+
if lhs._usedCustomRepresentation != rhs._usedCustomRepresentation {
239+
// If the lhs used a custom representation, try comparing against the
240+
// custom representation of the bridged rhs (if there is one).
241+
if lhs._usedCustomRepresentation {
242+
if let customRHS = _getBridgedCustomAnyHashable(rhs._box._base) {
243+
return lhs._box._isEqual(to: customRHS._box) ?? false
244+
}
245+
return false
246+
}
247+
248+
// Otherwise, try comparing the rhs against the custom representation of
249+
// the bridged lhs (if there is one).
250+
if let customLHS = _getBridgedCustomAnyHashable(lhs._box._base) {
251+
return customLHS._box._isEqual(to: rhs._box) ?? false
252+
}
253+
return false
254+
}
255+
#endif
256+
257+
return false
197258
}
198259
}
199260

test/1_stdlib/AnyHashableCasts.swift.gyb

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99

1010
import StdlibUnittest
1111

12+
#if _runtime(_ObjC)
13+
import Foundation
14+
#endif
15+
1216
var AnyHashableCasts = TestSuite("AnyHashableCasts")
1317

1418
protocol Implemented {}
@@ -112,4 +116,41 @@ AnyHashableCasts.test("${valueExpr} as ${coercedType} as? ${castType}") {
112116
}
113117
% end
114118

119+
#if _runtime(_ObjC)
120+
// A wrapper type around a String that bridges to NSString.
121+
struct StringWrapper1 : _SwiftNewtypeWrapper, Hashable, _ObjectiveCBridgeable {
122+
let rawValue: String
123+
}
124+
125+
// A wrapper type around a String that bridges to NSString.
126+
struct StringWrapper2 : _SwiftNewtypeWrapper, Hashable, _ObjectiveCBridgeable {
127+
let rawValue: String
128+
}
129+
130+
AnyHashableCasts.test("Wrappers around bridged types") {
131+
let wrapper1Hello: AnyHashable = StringWrapper1(rawValue: "hello")
132+
let stringHello: AnyHashable = "hello" as String
133+
let nsStringHello: AnyHashable = "hello" as NSString
134+
135+
// Casting from Swift wrapper maintains type identity
136+
expectNotEmpty(wrapper1Hello as? StringWrapper1)
137+
expectEmpty(wrapper1Hello as? StringWrapper2)
138+
expectEmpty(wrapper1Hello as? String)
139+
expectNotEmpty(wrapper1Hello as? NSString)
140+
141+
// Casting from String maintains type identity
142+
expectEmpty(stringHello as? StringWrapper1)
143+
expectEmpty(stringHello as? StringWrapper2)
144+
expectNotEmpty(stringHello as? String)
145+
expectNotEmpty(stringHello as? NSString)
146+
147+
// Casting form NSString works with anything.
148+
expectNotEmpty(nsStringHello as? StringWrapper1)
149+
expectNotEmpty(nsStringHello as? StringWrapper2)
150+
expectNotEmpty(nsStringHello as? String)
151+
expectNotEmpty(nsStringHello as? NSString)
152+
}
153+
154+
#endif
155+
115156
runAllTests()

validation-test/stdlib/AnyHashable.swift.gyb

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -735,6 +735,46 @@ AnyHashableTests.test("AnyHashable(MinimalHashableRCSwiftError).base") {
735735
expectEqual(MinimalHashableRCSwiftError.self, type(of: ah.base))
736736
}
737737

738+
#if _runtime(_ObjC)
739+
// A wrapper type around a String that bridges to NSString.
740+
struct StringWrapper1 : _SwiftNewtypeWrapper, Hashable, _ObjectiveCBridgeable {
741+
let rawValue: String
742+
}
743+
744+
// A wrapper type around a String that bridges to NSString.
745+
struct StringWrapper2 : _SwiftNewtypeWrapper, Hashable, _ObjectiveCBridgeable {
746+
let rawValue: String
747+
}
748+
749+
AnyHashableTests.test("AnyHashable(Wrappers)/Hashable") {
750+
let values: [AnyHashable] = [
751+
StringWrapper1(rawValue: "hello"),
752+
StringWrapper2(rawValue: "hello"),
753+
"hello" as String,
754+
"hello" as NSString,
755+
StringWrapper1(rawValue: "world"),
756+
StringWrapper2(rawValue: "world"),
757+
"world" as String,
758+
"world" as NSString,
759+
]
760+
761+
func equalityOracle(_ lhs: Int, _ rhs: Int) -> Bool {
762+
// Elements in [0, 3] match 3.
763+
if lhs == 3 { return rhs >= 0 && rhs <= 3 }
764+
if rhs == 3 { return lhs >= 0 && lhs <= 3 }
765+
766+
// Elements in [4, 7] match 7.
767+
if lhs == 7 { return rhs >= 4 && rhs <= 7 }
768+
if rhs == 7 { return lhs >= 4 && lhs <= 7 }
769+
770+
return lhs == rhs
771+
}
772+
773+
checkHashable(values, equalityOracle: equalityOracle,
774+
allowBrokenTransitivity: true)
775+
}
776+
#endif
777+
738778
#if _runtime(_ObjC)
739779
AnyHashableTests.test("AnyHashable(_SwiftNativeNSError(MinimalHashablePODSwiftError))/Hashable") {
740780
let swiftErrors: [MinimalHashablePODSwiftError] = [

0 commit comments

Comments
 (0)