Skip to content

Commit 4e821f6

Browse files
authored
Merge pull request swiftlang#69464 from tbkka/tbkka-objc-equatable-hashable
Use Swift Equatable and Hashable conformances from ObjC
2 parents 1401b0f + d8d325f commit 4e821f6

File tree

4 files changed

+265
-14
lines changed

4 files changed

+265
-14
lines changed

stdlib/public/runtime/SwiftObject.mm

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,13 @@
3636
#include "swift/Runtime/ObjCBridge.h"
3737
#include "swift/Runtime/Portability.h"
3838
#include "swift/Strings.h"
39+
#include "swift/Threading/Mutex.h"
3940
#include "swift/shims/RuntimeShims.h"
4041
#include "swift/shims/AssertionReporting.h"
4142
#include "../CompatibilityOverride/CompatibilityOverride.h"
4243
#include "ErrorObject.h"
4344
#include "Private.h"
45+
#include "SwiftEquatableSupport.h"
4446
#include "SwiftObject.h"
4547
#include "SwiftValue.h"
4648
#include "WeakReference.h"
@@ -51,6 +53,7 @@
5153
#include <stdio.h>
5254
#include <stdlib.h>
5355
#include <unordered_map>
56+
#include <unordered_set>
5457
#if SWIFT_OBJC_INTEROP
5558
# import <CoreFoundation/CFBase.h> // for CFTypeID
5659
# import <Foundation/Foundation.h>
@@ -373,11 +376,86 @@ + (BOOL)conformsToProtocol:(Protocol*)proto {
373376
}
374377

375378
- (NSUInteger)hash {
379+
auto selfMetadata = _swift_getClassOfAllocated(self);
380+
381+
// If it's Hashable, use that
382+
auto hashableConformance =
383+
reinterpret_cast<const hashable_support::HashableWitnessTable *>(
384+
swift_conformsToProtocolCommon(
385+
selfMetadata, &hashable_support::HashableProtocolDescriptor));
386+
if (hashableConformance != NULL) {
387+
return _swift_stdlib_Hashable_hashValue_indirect(
388+
&self, selfMetadata, hashableConformance);
389+
}
390+
391+
// If a type is Equatable (but not Hashable), we
392+
// have to return something here that is compatible
393+
// with the `isEqual:` below.
394+
auto equatableConformance =
395+
reinterpret_cast<const equatable_support::EquatableWitnessTable *>(
396+
swift_conformsToProtocolCommon(
397+
selfMetadata, &equatable_support::EquatableProtocolDescriptor));
398+
if (equatableConformance != nullptr) {
399+
// Warn once per class about this
400+
auto selfClass = [self class];
401+
static Lazy<std::unordered_set<Class>> warned;
402+
static LazyMutex warnedLock;
403+
LazyMutex::ScopedLock guard(warnedLock);
404+
auto result = warned.get().insert(selfClass);
405+
auto inserted = std::get<1>(result);
406+
if (inserted) {
407+
const char *clsName = class_getName([self class]);
408+
warning(0,
409+
"Obj-C `-hash` method was invoked on a Swift object of type `%s` "
410+
"that is Equatable but not Hashable; "
411+
"this can lead to severe performance problems.\n",
412+
clsName);
413+
}
414+
// Constant value (yuck!) is the only choice here
415+
return (NSUInteger)1;
416+
}
417+
418+
// Legacy default for types that are neither Hashable nor Equatable.
376419
return (NSUInteger)self;
377420
}
378421

379-
- (BOOL)isEqual:(id)object {
380-
return self == object;
422+
- (BOOL)isEqual:(id)other {
423+
if (self == other) {
424+
return YES;
425+
}
426+
427+
// Get Swift type for self and other
428+
auto selfMetadata = _swift_getClassOfAllocated(self);
429+
430+
// We use Equatable conformance, which will also work for types that implement
431+
// Hashable. If the type implements Equatable but not Hashable, there is a
432+
// risk that `-hash` and `-isEqual:` might be incompatible. See notes above
433+
// for `-hash`
434+
auto equatableConformance =
435+
swift_conformsToProtocolCommon(
436+
selfMetadata, &equatable_support::EquatableProtocolDescriptor);
437+
if (equatableConformance == NULL) {
438+
return NO;
439+
}
440+
441+
// Is the other object a subclass of the parent that
442+
// actually defined this conformance?
443+
auto conformingParent =
444+
findConformingSuperclass(selfMetadata, equatableConformance->getDescription());
445+
auto otherMetadata = _swift_getClassOfAllocated(other);
446+
if (_swift_class_isSubclass(otherMetadata, conformingParent)) {
447+
// We now have an equatable conformance of a common parent
448+
// of both object types:
449+
return _swift_stdlib_Equatable_isEqual_indirect(
450+
&self,
451+
&other,
452+
conformingParent,
453+
reinterpret_cast<const equatable_support::EquatableWitnessTable *>(
454+
equatableConformance)
455+
);
456+
}
457+
458+
return NO;
381459
}
382460

383461
- (id)performSelector:(SEL)aSelector {

test/stdlib/BridgeIdAsAny.swift.gyb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ BridgeAnything.test("SwiftValue(mixed values)/Hashable") {
346346
}
347347
return lhs / 2 == rhs / 2
348348
}
349-
checkHashable(boxedXs, equalityOracle: equalityOracle)
349+
checkHashable(boxedXs, equalityOracle: equalityOracle, allowIncompleteHashing: true)
350350
}
351351

352352
runAllTests()

test/stdlib/Inputs/SwiftObjectNSObject/SwiftObjectNSObject.m

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,48 @@ void HackSwiftObject()
8080
class_addMethod(cls, @selector(perform2::), (IMP)Perform2, "@@:@@");
8181
}
8282

83+
void TestSwiftObjectNSObjectAssertNoErrors(void)
84+
{
85+
printf("\nTotal: %d error%s\n",
86+
Errors, Errors == 1 ? "" : "s");
87+
if (Errors > 0) {
88+
exit(1);
89+
}
90+
}
91+
92+
93+
void TestSwiftObjectNSObjectEquals(id e1, id e2)
94+
{
95+
printf("NSObjectProtocol.isEqual: Expect %s == %s\n",
96+
[[e1 description] UTF8String],
97+
[[e2 description] UTF8String]);
98+
expectTrue([e1 isEqual:e2]);
99+
expectTrue([e2 isEqual:e1]);
100+
}
101+
102+
void TestSwiftObjectNSObjectNotEquals(id e1, id e2)
103+
{
104+
printf("NSObjectProtocol.isEqual: Expect %s != %s\n",
105+
[[e1 description] UTF8String],
106+
[[e2 description] UTF8String]);
107+
expectFalse([e1 isEqual:e2]);
108+
expectFalse([e2 isEqual:e1]);
109+
}
110+
111+
void TestSwiftObjectNSObjectHashValue(id e, NSUInteger hashValue)
112+
{
113+
printf("NSObjectProtocol.hash: Expect [%s hashValue] == %lu\n",
114+
[[e description] UTF8String],
115+
(unsigned long)hashValue);
116+
expectTrue([e hash] == hashValue);
117+
}
118+
119+
void TestSwiftObjectNSObjectDefaultHashValue(id e)
120+
{
121+
NSUInteger hashValue = (NSUInteger)e;
122+
TestSwiftObjectNSObjectHashValue(e, hashValue);
123+
}
124+
83125
void TestSwiftObjectNSObject(id c, id d)
84126
{
85127
printf("TestSwiftObjectNSObject\n");
@@ -159,13 +201,11 @@ void TestSwiftObjectNSObject(id c, id d)
159201
expectFalse([C_meta isEqual:D_meta]);
160202
expectFalse([S_meta isEqual:C_meta]);
161203

162-
163204
printf("NSObjectProtocol.hash\n");
164205

165206
expectTrue ([d hash] + [c hash] + [D hash] + [C hash] + [S hash] +
166207
[D_meta hash] + [C_meta hash] + [S_meta hash] != 0);
167208

168-
169209
printf("NSObjectProtocol.self\n");
170210

171211
expectTrue ([d self] == d);
@@ -798,9 +838,4 @@ void TestSwiftObjectNSObject(id c, id d)
798838
expectTrue ([S_meta instanceMethodForSelector:@selector(DESSLOK)] == fwd);
799839
expectTrue ([C_meta instanceMethodForSelector:@selector(DESSLOK)] == fwd);
800840
expectTrue ([D_meta instanceMethodForSelector:@selector(DESSLOK)] == fwd);
801-
802-
803-
printf("TestSwiftObjectNSObject: %d error%s\n",
804-
Errors, Errors == 1 ? "" : "s");
805-
exit(Errors ? 1 : 0);
806841
}

test/stdlib/SwiftObjectNSObject.swift

Lines changed: 142 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@
1313
// RUN: %empty-directory(%t)
1414
//
1515
// RUN: %target-clang %S/Inputs/SwiftObjectNSObject/SwiftObjectNSObject.m -c -o %t/SwiftObjectNSObject.o -g
16-
// RUN: %target-build-swift %s -I %S/Inputs/SwiftObjectNSObject/ -Xlinker %t/SwiftObjectNSObject.o -o %t/SwiftObjectNSObject
16+
// RUN: %target-build-swift %s -g -I %S/Inputs/SwiftObjectNSObject/ -Xlinker %t/SwiftObjectNSObject.o -o %t/SwiftObjectNSObject
1717
// RUN: %target-codesign %t/SwiftObjectNSObject
1818
// RUN: %target-run %t/SwiftObjectNSObject 2> %t/log.txt
19+
// RUN: cat %t/log.txt 1>&2
1920
// RUN: %FileCheck %s < %t/log.txt
2021
// REQUIRES: executable_test
2122

@@ -39,22 +40,159 @@ class D : C {
3940
@objc override class func cClassOverride() -> Int { return 8 }
4041
}
4142

42-
@_silgen_name("TestSwiftObjectNSObject")
43+
class E : Equatable, CustomStringConvertible {
44+
var i : Int
45+
static func ==(lhs: E, rhs: E) -> Bool { lhs.i == rhs.i }
46+
init(i: Int) { self.i = i }
47+
var description: String { "\(type(of:self))(i:\(self.i))" }
48+
}
49+
50+
class E1: E {
51+
}
52+
53+
class E2: E {
54+
}
55+
56+
class F : CustomStringConvertible {
57+
var i : Int
58+
init(i: Int) { self.i = i }
59+
var description: String { "\(type(of:self))(i:\(self.i))" }
60+
}
61+
62+
class F1: F, Equatable {
63+
static func ==(lhs: F1, rhs: F1) -> Bool { lhs.i == rhs.i }
64+
}
65+
66+
class F2: F, Equatable {
67+
static func ==(lhs: F2, rhs: F2) -> Bool { lhs.i == rhs.i }
68+
}
69+
70+
class H : E, Hashable {
71+
static func ==(lhs: H, rhs: H) -> Bool { lhs.i == rhs.i }
72+
func hash(into hasher: inout Hasher) { hasher.combine(i + 17) }
73+
}
74+
75+
@_silgen_name("TestSwiftObjectNSObject")
4376
func TestSwiftObjectNSObject(_ c: C, _ d: D)
77+
@_silgen_name("TestSwiftObjectNSObjectEquals")
78+
func TestSwiftObjectNSObjectEquals(_: AnyObject, _: AnyObject)
79+
@_silgen_name("TestSwiftObjectNSObjectNotEquals")
80+
func TestSwiftObjectNSObjectNotEquals(_: AnyObject, _: AnyObject)
81+
@_silgen_name("TestSwiftObjectNSObjectHashValue")
82+
func TestSwiftObjectNSObjectHashValue(_: AnyObject, _: Int)
83+
@_silgen_name("TestSwiftObjectNSObjectDefaultHashValue")
84+
func TestSwiftObjectNSObjectDefaultHashValue(_: AnyObject)
85+
@_silgen_name("TestSwiftObjectNSObjectAssertNoErrors")
86+
func TestSwiftObjectNSObjectAssertNoErrors()
87+
88+
// Verify that Obj-C isEqual: provides same answer as Swift ==
89+
func TestEquatableEquals<T: Equatable & AnyObject>(_ e1: T, _ e2: T) {
90+
if e1 == e2 {
91+
TestSwiftObjectNSObjectEquals(e1, e2)
92+
} else {
93+
TestSwiftObjectNSObjectNotEquals(e1, e2)
94+
}
95+
}
96+
97+
func TestNonEquatableEquals(_ e1: AnyObject, _ e2: AnyObject) {
98+
TestSwiftObjectNSObjectNotEquals(e1, e2)
99+
}
100+
101+
// Verify that Obj-C hashValue matches Swift hashValue for Hashable types
102+
func TestHashable(_ h: H)
103+
{
104+
TestSwiftObjectNSObjectHashValue(h, h.hashValue)
105+
}
106+
107+
// Test Obj-C hashValue for Swift types that are Equatable but not Hashable
108+
func TestEquatableHash(_ e: AnyObject)
109+
{
110+
// These should have a constant hash value
111+
TestSwiftObjectNSObjectHashValue(e, 1)
112+
}
113+
114+
func TestNonEquatableHash(_ e: AnyObject)
115+
{
116+
TestSwiftObjectNSObjectDefaultHashValue(e)
117+
}
118+
119+
// Check NSLog() output from TestSwiftObjectNSObject().
44120
45-
// This check is for NSLog() output from TestSwiftObjectNSObject().
46121
// CHECK: c ##SwiftObjectNSObject.C##
47122
// CHECK-NEXT: d ##SwiftObjectNSObject.D##
48123
// CHECK-NEXT: S ##{{.*}}SwiftObject##
49124
125+
// Full message is longer, but this is the essential part...
126+
// CHECK-NEXT: Obj-C `-hash` {{.*}} type `SwiftObjectNSObject.E` {{.*}} Equatable but not Hashable
127+
// CHECK-NEXT: Obj-C `-hash` {{.*}} type `SwiftObjectNSObject.E1` {{.*}} Equatable but not Hashable
128+
// CHECK-NEXT: Obj-C `-hash` {{.*}} type `SwiftObjectNSObject.E2` {{.*}} Equatable but not Hashable
129+
50130
// Temporarily disable this test on older OSes until we have time to
51131
// look into why it's failing there. rdar://problem/47870743
52132
if #available(OSX 10.12, iOS 10.0, *) {
133+
// Test a large number of Obj-C APIs
53134
TestSwiftObjectNSObject(C(), D())
54-
// does not return
135+
136+
// ** Equatable types with an Equatable parent class
137+
// Same type and class
138+
TestEquatableEquals(E(i: 1), E(i: 1))
139+
TestEquatableEquals(E(i: 790), E(i: 790))
140+
TestEquatableEquals(E1(i: 1), E1(i: 1))
141+
TestEquatableEquals(E1(i: 18), E1(i: 18))
142+
TestEquatableEquals(E2(i: 1), E2(i: 1))
143+
TestEquatableEquals(E2(i: 2), E2(i: 2))
144+
// Different class
145+
TestEquatableEquals(E1(i: 1), E2(i: 1))
146+
TestEquatableEquals(E1(i: 1), E(i: 1))
147+
TestEquatableEquals(E2(i: 1), E(i: 1))
148+
// Different value
149+
TestEquatableEquals(E(i: 1), E(i: 2))
150+
TestEquatableEquals(E1(i: 1), E1(i: 2))
151+
TestEquatableEquals(E2(i: 1), E2(i: 2))
152+
153+
// ** Non-Equatable parent class
154+
// Same class and value
155+
TestEquatableEquals(F1(i: 1), F1(i: 1))
156+
TestEquatableEquals(F1(i: 1), F1(i: 2))
157+
TestEquatableEquals(F2(i: 1), F2(i: 1))
158+
TestEquatableEquals(F2(i: 1), F2(i: 2))
159+
160+
// Different class and/or value
161+
TestNonEquatableEquals(F(i: 1), F(i: 2))
162+
TestNonEquatableEquals(F(i: 1), F(i: 1))
163+
TestNonEquatableEquals(F1(i: 1), F2(i: 1))
164+
TestNonEquatableEquals(F1(i: 1), F(i: 1))
165+
166+
// Two equatable types with no common parent class
167+
TestNonEquatableEquals(F1(i: 1), E(i: 1))
168+
TestEquatableEquals(H(i:1), E(i:1))
169+
170+
// Equatable but not Hashable: alway have the same Obj-C hashValue
171+
TestEquatableHash(E(i: 1))
172+
TestEquatableHash(E1(i: 3))
173+
TestEquatableHash(E2(i: 8))
174+
175+
// Neither Equatable nor Hashable
176+
TestNonEquatableHash(C())
177+
TestNonEquatableHash(D())
178+
179+
// Hashable types are also Equatable
180+
TestEquatableEquals(H(i:1), H(i:1))
181+
TestEquatableEquals(H(i:1), H(i:2))
182+
TestEquatableEquals(H(i:2), H(i:1))
183+
184+
// Verify Obj-C hash value agrees with Swift
185+
TestHashable(H(i:1))
186+
TestHashable(H(i:2))
187+
TestHashable(H(i:18))
188+
189+
TestSwiftObjectNSObjectAssertNoErrors()
55190
} else {
56191
// Horrible hack to satisfy FileCheck
57192
fputs("c ##SwiftObjectNSObject.C##\n", stderr)
58193
fputs("d ##SwiftObjectNSObject.D##\n", stderr)
59194
fputs("S ##Swift._SwiftObject##\n", stderr)
195+
fputs("Obj-C `-hash` ... type `SwiftObjectNSObject.E` ... Equatable but not Hashable", stderr)
196+
fputs("Obj-C `-hash` ... type `SwiftObjectNSObject.E1` ... Equatable but not Hashable", stderr)
197+
fputs("Obj-C `-hash` ... type `SwiftObjectNSObject.E2` ... Equatable but not Hashable", stderr)
60198
}

0 commit comments

Comments
 (0)