Skip to content

Commit 08c2a7a

Browse files
authored
Finish off the log part of _swift_checkClassAndWarnForKeyedArchiving. (#10418)
Logs a warning the first time a problematic class is archived or unarchived. We expect people to actually fix these issues, so the performance of the warning isn't too important. Sample output: [timestamp] Attempting to archive Swift class '_Test.Outer.ArchivedThenUnarchived', which does not have a stable runtime name. [timestamp] Use the 'objc' attribute to ensure that the runtime name will not change: "@objc(_TtCC5_Test5Outer22ArchivedThenUnarchived)" [timestamp] If there are no existing archives containing this class, you can choose a unique, prefixed name instead: "@objc(ABCArchivedThenUnarchived)" Finishes rdar://problem/32414508
1 parent 2b5710b commit 08c2a7a

File tree

2 files changed

+350
-7
lines changed

2 files changed

+350
-7
lines changed

stdlib/public/SDK/Foundation/CheckClass.mm

Lines changed: 177 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
#include <objc/runtime.h>
44

5+
#include "swift/Runtime/HeapObject.h"
56
#include "swift/Runtime/Metadata.h"
67

78
@interface NSKeyedUnarchiver (SwiftAdditions)
@@ -10,9 +11,116 @@ + (int)_swift_checkClassAndWarnForKeyedArchiving:(Class)cls
1011
NS_SWIFT_NAME(_swift_checkClassAndWarnForKeyedArchiving(_:operation:));
1112
@end
1213

14+
static bool isASCIIIdentifierChar(char c) {
15+
if (c >= 'a' && c <= 'z') return true;
16+
if (c >= 'A' && c <= 'Z') return true;
17+
if (c >= '0' && c <= '9') return true;
18+
if (c == '_') return true;
19+
if (c == '$') return true;
20+
return false;
21+
}
22+
23+
static void logIfFirstOccurrence(Class objcClass, void (^log)(void)) {
24+
static auto queue = dispatch_queue_create(
25+
"SwiftFoundation._checkClassAndWarnForKeyedArchivingQueue",
26+
DISPATCH_QUEUE_SERIAL);
27+
static NSHashTable *seenClasses = nil;
28+
29+
dispatch_sync(queue, ^{
30+
// Will be NO when seenClasses is still nil.
31+
if ([seenClasses containsObject:objcClass])
32+
return;
33+
34+
if (!seenClasses) {
35+
NSPointerFunctionsOptions options = 0;
36+
options |= NSPointerFunctionsOpaqueMemory;
37+
options |= NSPointerFunctionsObjectPointerPersonality;
38+
seenClasses = [[NSHashTable alloc] initWithOptions:options capacity:16];
39+
}
40+
[seenClasses addObject:objcClass];
41+
42+
// Synchronize logging so that multiple lines aren't interleaved.
43+
log();
44+
});
45+
}
46+
47+
namespace {
48+
class StringRefLite {
49+
StringRefLite(const char *data, size_t len) : data(data), length(len) {}
50+
public:
51+
const char *data;
52+
size_t length;
53+
54+
StringRefLite() : data(nullptr), length(0) {}
55+
56+
template <size_t N>
57+
StringRefLite(const char (&staticStr)[N]) : data(staticStr), length(N) {}
58+
59+
StringRefLite(swift::TwoWordPair<const char *, uintptr_t> pair)
60+
: data(pair.first), length(pair.second) {}
61+
62+
NS_RETURNS_RETAINED
63+
NSString *newNSStringNoCopy() const {
64+
return [[NSString alloc] initWithBytesNoCopy:const_cast<char *>(data)
65+
length:length
66+
encoding:NSUTF8StringEncoding
67+
freeWhenDone:NO];
68+
}
69+
70+
const char &operator[](size_t offset) const {
71+
assert(offset < length);
72+
return data[offset];
73+
}
74+
75+
StringRefLite slice(size_t from, size_t to) const {
76+
assert(from <= to);
77+
assert(to <= length);
78+
return {data + from, to - from};
79+
}
80+
81+
const char *begin() const {
82+
return data;
83+
}
84+
const char *end() const {
85+
return data + length;
86+
}
87+
};
88+
}
89+
90+
/// Assume that a non-generic demangled class name always ends in ".MyClass"
91+
/// or ".(MyClass plus extra info)".
92+
static StringRefLite findBaseName(StringRefLite demangledName) {
93+
size_t end = demangledName.length;
94+
size_t parenCount = 0;
95+
for (size_t i = end; i != 0; --i) {
96+
switch (demangledName[i - 1]) {
97+
case '.':
98+
if (parenCount == 0) {
99+
if (i != end && demangledName[i] == '(')
100+
++i;
101+
return demangledName.slice(i, end);
102+
}
103+
break;
104+
case ')':
105+
parenCount += 1;
106+
break;
107+
case '(':
108+
if (parenCount > 0)
109+
parenCount -= 1;
110+
break;
111+
case ' ':
112+
end = i - 1;
113+
break;
114+
default:
115+
break;
116+
}
117+
}
118+
return {};
119+
}
120+
13121
@implementation NSKeyedUnarchiver (SwiftAdditions)
14122

15-
/// Checks if class \p cls is good for archiving.
123+
/// Checks if class \p objcClass is good for archiving.
16124
///
17125
/// If not, a runtime warning is printed.
18126
///
@@ -25,20 +133,21 @@ @implementation NSKeyedUnarchiver (SwiftAdditions)
25133
/// 2: a Swift non-generic class where adding @objc is valid
26134
/// Future versions of this API will return nonzero values for additional cases
27135
/// that mean the class shouldn't be archived.
28-
+ (int)_swift_checkClassAndWarnForKeyedArchiving:(Class)cls
136+
+ (int)_swift_checkClassAndWarnForKeyedArchiving:(Class)objcClass
29137
operation:(int)operation {
30-
const swift::ClassMetadata *theClass = (swift::ClassMetadata *)cls;
138+
using namespace swift;
139+
const ClassMetadata *theClass = (ClassMetadata *)objcClass;
31140

32141
// Is it a (real) swift class?
33142
if (!theClass->isTypeMetadata() || theClass->isArtificialSubclass())
34143
return 0;
35144

36145
// Does the class already have a custom name?
37-
if (theClass->getFlags() & swift::ClassFlags::HasCustomObjCName)
146+
if (theClass->getFlags() & ClassFlags::HasCustomObjCName)
38147
return 0;
39148

40149
// Is it a mangled name?
41-
const char *className = class_getName(cls);
150+
const char *className = class_getName(objcClass);
42151
if (!(className[0] == '_' && className[1] == 'T'))
43152
return 0;
44153
// Is it a name in the form <module>.<class>? Note: the module name could
@@ -48,13 +157,74 @@ + (int)_swift_checkClassAndWarnForKeyedArchiving:(Class)cls
48157

49158
// Is it a generic class?
50159
if (theClass->getDescription()->GenericParams.isGeneric()) {
51-
// TODO: print a warning
160+
logIfFirstOccurrence(objcClass, ^{
161+
// Use actual NSStrings to force UTF-8.
162+
StringRefLite demangledName = swift_getTypeName(theClass,
163+
/*qualified*/true);
164+
NSString *demangledString = demangledName.newNSStringNoCopy();
165+
NSString *mangledString = NSStringFromClass(objcClass);
166+
switch (operation) {
167+
case 1:
168+
NSLog(@"Attempting to unarchive generic Swift class '%@' with mangled "
169+
"runtime name '%@'. Runtime names for generic classes are "
170+
"unstable and may change in the future, leading to "
171+
"non-decodable data.", demangledString, mangledString);
172+
break;
173+
default:
174+
NSLog(@"Attempting to archive generic Swift class '%@' with mangled "
175+
"runtime name '%@'. Runtime names for generic classes are "
176+
"unstable and may change in the future, leading to "
177+
"non-decodable data.", demangledString, mangledString);
178+
break;
179+
}
180+
NSLog(@"To avoid this failure, create a concrete subclass and register "
181+
"it with NSKeyedUnarchiver.setClass(_:forClassName:) instead, "
182+
"using the name \"%@\".", mangledString);
183+
NSLog(@"If you need to produce archives compatible with older versions "
184+
"of your program, use NSKeyedArchiver.setClassName(_:for:) "
185+
"as well.");
186+
[demangledString release];
187+
});
52188
return 1;
53189
}
54190

55191
// It's a swift class with a (compiler generated) mangled name, which should
56192
// be written into an NSArchive.
57-
// TODO: print a warning
193+
logIfFirstOccurrence(objcClass, ^{
194+
// Use actual NSStrings to force UTF-8.
195+
StringRefLite demangledName = swift_getTypeName(theClass,/*qualified*/true);
196+
NSString *demangledString = demangledName.newNSStringNoCopy();
197+
NSString *mangledString = NSStringFromClass(objcClass);
198+
switch (operation) {
199+
case 1:
200+
NSLog(@"Attempting to unarchive Swift class '%@' with mangled runtime "
201+
"name '%@'. The runtime name for this class is unstable and may "
202+
"change in the future, leading to non-decodable data.",
203+
demangledString, mangledString);
204+
break;
205+
default:
206+
NSLog(@"Attempting to archive Swift class '%@' with mangled runtime "
207+
"name '%@'. The runtime name for this class is unstable and may "
208+
"change in the future, leading to non-decodable data.",
209+
demangledString, mangledString);
210+
break;
211+
}
212+
[demangledString release];
213+
NSLog(@"You can use the 'objc' attribute to ensure that the name will not "
214+
"change: \"@objc(%@)\"", mangledString);
215+
216+
StringRefLite baseName = findBaseName(demangledName);
217+
// Offer a more generic message if the base name we found doesn't look like
218+
// an ASCII identifier. This avoids printing names like "ABCモデル".
219+
if (baseName.length == 0 ||
220+
!std::all_of(baseName.begin(), baseName.end(), isASCIIIdentifierChar)) {
221+
baseName = "MyModel";
222+
}
223+
224+
NSLog(@"If there are no existing archives containing this class, you "
225+
"should choose a unique, prefixed name instead: "
226+
"\"@objc(ABC%1$.*2$s)\"", baseName.data, (int)baseName.length);
227+
});
58228
return 2;
59229
}
60230
@end

0 commit comments

Comments
 (0)