Skip to content

Commit 659d1cf

Browse files
committed
Make ObjC isEqual: delegate to Equatable when Hashable isn't available
When a Swift struct gets bridged to Obj-C, we box it into an opaque `_SwiftValue` Obj-C object. This object previously supported the Obj-C `isEqual:` and `hash` selectors by dispatching to the Swift Hashable conformance, if present. This does not work if the Swift struct conforms to Equatable but does not conform to Hashable. This case seems to have been overlooked in PR #4124. This PR extends the earlier work to support `isEqual:` by first checking for a Hashable conformance, then falling back on an Equatable conformance if there is no Hashable conformance. Resolves rdar://114294889
1 parent 5a2465e commit 659d1cf

File tree

3 files changed

+138
-13
lines changed

3 files changed

+138
-13
lines changed

stdlib/public/core/Equatable.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,16 @@ extension Equatable {
196196
}
197197
}
198198

199+
// Called by the SwiftValue implementation.
200+
@_silgen_name("_swift_stdlib_Equatable_isEqual_indirect")
201+
internal func Equatable_isEqual_indirect<T: Equatable>(
202+
_ lhs: UnsafePointer<T>,
203+
_ rhs: UnsafePointer<T>
204+
) -> Bool {
205+
return lhs.pointee == rhs.pointee
206+
}
207+
208+
199209
//===----------------------------------------------------------------------===//
200210
// Reference comparison
201211
//===----------------------------------------------------------------------===//

stdlib/public/runtime/SwiftValue.mm

Lines changed: 81 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,14 @@ - (id)copyWithZone:(NSZone *)zone;
5757

5858
@end
5959

60+
struct EquatableWitnessTable;
61+
62+
/// Calls `Equatable.==` through an `Equatable` witness table
63+
SWIFT_CC(swift) SWIFT_RUNTIME_STDLIB_INTERNAL
64+
bool _swift_stdlib_Equatable_isEqual_indirect(
65+
const void *lhsValue, const void *rhsValue, const Metadata *type,
66+
const EquatableWitnessTable *wt);
67+
6068
/// The fixed-size ivars of `__SwiftValue`. The actual boxed value is
6169
/// tail-allocated.
6270
struct SwiftValueHeader {
@@ -80,10 +88,18 @@ - (id)copyWithZone:(NSZone *)zone;
8088
/// Returns NULL if the type does not conform.
8189
const Metadata *getHashableBaseType() const;
8290

91+
/// Get the base type that conforms to `Equatable`.
92+
/// Returns NULL if the type does not conform.
93+
const Metadata *getEquatableBaseType() const;
94+
8395
/// Get the `Hashable` protocol witness table for the contained type.
8496
/// Returns NULL if the type does not conform.
8597
const hashable_support::HashableWitnessTable *getHashableConformance() const;
8698

99+
/// Get the `Equatable` protocol witness table for the contained type.
100+
/// Returns NULL if the type does not conform.
101+
const EquatableWitnessTable *getEquatableConformance() const;
102+
87103
SwiftValueHeader()
88104
: hashableBaseType(nullptr), hashableConformance(nullptr) {}
89105
};
@@ -105,6 +121,27 @@ - (id)copyWithZone:(NSZone *)zone;
105121
return type;
106122
}
107123

124+
extern "C" const ProtocolDescriptor PROTOCOL_DESCR_SYM(SQ);
125+
static constexpr auto &EquatableProtocolDescriptor = PROTOCOL_DESCR_SYM(SQ);
126+
127+
const Metadata *SwiftValueHeader::getEquatableBaseType() const {
128+
auto witnessTable =
129+
swift_conformsToProtocolCommon(type, &EquatableProtocolDescriptor);
130+
if (!witnessTable) {
131+
return nullptr;
132+
}
133+
#if SWIFT_STDLIB_USE_RELATIVE_PROTOCOL_WITNESS_TABLES
134+
const auto *conformance = lookThroughOptionalConditionalWitnessTable(
135+
reinterpret_cast<const RelativeWitnessTable*>(witnessTable))
136+
->getDescription();
137+
#else
138+
const auto *conformance = witnessTable->getDescription();
139+
#endif
140+
const Metadata *baseTypeThatConformsToEquatable =
141+
findConformingSuperclass(type, conformance);
142+
return baseTypeThatConformsToEquatable;
143+
}
144+
108145
const hashable_support::HashableWitnessTable *
109146
SwiftValueHeader::getHashableConformance() const {
110147
if (auto wt = hashableConformance.load(std::memory_order_acquire)) {
@@ -124,6 +161,14 @@ - (id)copyWithZone:(NSZone *)zone;
124161
return wt;
125162
}
126163

164+
const EquatableWitnessTable *
165+
SwiftValueHeader::getEquatableConformance() const {
166+
const EquatableWitnessTable *wt =
167+
reinterpret_cast<const EquatableWitnessTable *>(
168+
swift_conformsToProtocolCommon(type, &EquatableProtocolDescriptor));
169+
return wt;
170+
}
171+
127172
static constexpr const size_t SwiftValueHeaderOffset
128173
= sizeof(Class); // isa pointer
129174
static constexpr const size_t SwiftValueMinAlignMask
@@ -300,30 +345,53 @@ - (BOOL)isEqual:(id)other {
300345
return NO;
301346
}
302347

348+
// `other` must also be a _SwiftValue box
303349
if (![other isKindOfClass:getSwiftValueClass()]) {
304350
return NO;
305351
}
306352

307353
auto selfHeader = getSwiftValueHeader(self);
308354
auto otherHeader = getSwiftValueHeader(other);
309355

310-
auto hashableBaseType = selfHeader->getHashableBaseType();
311-
if (!hashableBaseType ||
312-
otherHeader->getHashableBaseType() != hashableBaseType) {
313-
return NO;
356+
// TODO: getHashableBaseType seems to always succeed,
357+
// even if the type is in fact not Hashable. Why?
358+
// Maybe we should try getting the conformance first,
359+
// since that seems to actually fail?
360+
auto selfHashableBaseType = selfHeader->getHashableBaseType();
361+
if (selfHashableBaseType) {
362+
auto otherHashableBaseType = otherHeader->getHashableBaseType();
363+
if (selfHashableBaseType == otherHashableBaseType) {
364+
auto hashableConformance = selfHeader->getHashableConformance();
365+
if (hashableConformance) {
366+
return _swift_stdlib_Hashable_isEqual_indirect(
367+
getSwiftValuePayload(self,
368+
getSwiftValuePayloadAlignMask(selfHeader->type)),
369+
getSwiftValuePayload(other,
370+
getSwiftValuePayloadAlignMask(otherHeader->type)),
371+
selfHashableBaseType, hashableConformance);
372+
}
373+
}
314374
}
315375

316-
auto hashableConformance = selfHeader->getHashableConformance();
317-
if (!hashableConformance) {
318-
return NO;
376+
// TODO: As above, getEquatableBaseType seems to always succeed
377+
// even if the type is not in fact Equatable.
378+
auto selfEquatableBaseType = selfHeader->getEquatableBaseType();
379+
if (selfEquatableBaseType) {
380+
auto otherEquatableBaseType = otherHeader->getEquatableBaseType();
381+
if (selfEquatableBaseType == otherEquatableBaseType) {
382+
auto equatableConformance = selfHeader->getEquatableConformance();
383+
if (equatableConformance) {
384+
return _swift_stdlib_Equatable_isEqual_indirect(
385+
getSwiftValuePayload(self,
386+
getSwiftValuePayloadAlignMask(selfHeader->type)),
387+
getSwiftValuePayload(other,
388+
getSwiftValuePayloadAlignMask(otherHeader->type)),
389+
selfEquatableBaseType, equatableConformance);
390+
}
391+
}
319392
}
320393

321-
return _swift_stdlib_Hashable_isEqual_indirect(
322-
getSwiftValuePayload(self,
323-
getSwiftValuePayloadAlignMask(selfHeader->type)),
324-
getSwiftValuePayload(other,
325-
getSwiftValuePayloadAlignMask(otherHeader->type)),
326-
hashableBaseType, hashableConformance);
394+
return NO;
327395
}
328396

329397
- (NSUInteger)hash {
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// RUN: %empty-directory(%t)
2+
// RUN: %target-build-swift -g %s -o %t/a.out
3+
// RUN: %target-codesign %t/a.out
4+
// RUN: %target-run %t/a.out
5+
6+
// REQUIRES: executable_test
7+
// REQUIRES: objc_interop
8+
9+
import StdlibUnittest
10+
import Foundation
11+
12+
var BridgeEquatableToObjC = TestSuite("BridgeEquatableToObjC")
13+
14+
struct MyEquatableStruct: Equatable {
15+
var text: String
16+
}
17+
18+
struct MyNonEquatableStruct {
19+
var text: String
20+
}
21+
22+
BridgeEquatableToObjC.test("Bridge equatable struct") {
23+
let swiftA = MyEquatableStruct(text: "xABC")
24+
let swiftB = swiftA
25+
let swiftResult = swiftA == swiftB
26+
27+
let objcA = swiftA as AnyObject
28+
let objcB = swiftB as AnyObject
29+
let objcResult = objcA.isEqual(objcB)
30+
31+
expectEqual(swiftResult, true)
32+
expectEqual(objcResult, true)
33+
}
34+
35+
BridgeEquatableToObjC.test("Bridge non-equatable struct") {
36+
let swiftA = MyNonEquatableStruct(text: "xABC")
37+
let swiftB = swiftA
38+
39+
let objcA = swiftA as AnyObject
40+
let objcB = swiftB as AnyObject
41+
let objcResult = objcA.isEqual(objcB)
42+
43+
expectEqual(objcResult, false)
44+
}
45+
46+
47+
runAllTests()

0 commit comments

Comments
 (0)