Skip to content

Commit 2ff9c77

Browse files
authored
Merge pull request #10431 from jrose-apple/4.0-unstable-mangled-names
Runtime hook for logging on the use of unstable runtime names with NSCoding
2 parents b0e30dd + b3f680f commit 2ff9c77

File tree

7 files changed

+484
-0
lines changed

7 files changed

+484
-0
lines changed

include/swift/ABI/MetadataValues.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ enum class ClassFlags : uint32_t {
9292

9393
/// Does this class use Swift 1.0 refcounting?
9494
UsesSwift1Refcounting = 0x2,
95+
96+
/// Has this class a custom name, specified with the @objc attribute?
97+
HasCustomObjCName = 0x4
9598
};
9699
inline bool operator&(ClassFlags a, ClassFlags b) {
97100
return (uint32_t(a) & uint32_t(b)) != 0;

lib/IRGen/GenMeta.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include "swift/AST/CanTypeVisitor.h"
2020
#include "swift/AST/ExistentialLayout.h"
2121
#include "swift/AST/Decl.h"
22+
#include "swift/AST/Attr.h"
2223
#include "swift/AST/IRGenOptions.h"
2324
#include "swift/AST/SubstitutionMap.h"
2425
#include "swift/AST/Types.h"
@@ -3349,6 +3350,14 @@ namespace {
33493350
flags |= ClassFlags::UsesSwift1Refcounting;
33503351
}
33513352

3353+
DeclAttributes attrs = Target->getAttrs();
3354+
if (auto objc = attrs.getAttribute<ObjCAttr>()) {
3355+
if (objc->getName())
3356+
flags |= ClassFlags::HasCustomObjCName;
3357+
}
3358+
if (attrs.hasAttribute<ObjCRuntimeNameAttr>())
3359+
flags |= ClassFlags::HasCustomObjCName;
3360+
33523361
B.addInt32((uint32_t) flags);
33533362
}
33543363

stdlib/public/SDK/Foundation/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ add_swift_library(swiftFoundation ${SWIFT_SDK_OVERLAY_LIBRARY_BUILD_TYPES} IS_SD
5454
URLComponents.swift
5555
URLRequest.swift
5656
UUID.swift
57+
CheckClass.mm
5758

5859
SWIFT_COMPILE_FLAGS "${SWIFT_RUNTIME_SWIFT_COMPILE_FLAGS}" "-Xllvm" "-sil-inline-generics" "-Xllvm" "-sil-partial-specialization"
5960
LINK_FLAGS "${SWIFT_RUNTIME_SWIFT_LINK_FLAGS}"
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
#import <Foundation/Foundation.h>
2+
3+
#include <objc/runtime.h>
4+
5+
#include "swift/Runtime/HeapObject.h"
6+
#include "swift/Runtime/Metadata.h"
7+
8+
@interface NSKeyedUnarchiver (SwiftAdditions)
9+
+ (int)_swift_checkClassAndWarnForKeyedArchiving:(Class)cls
10+
operation:(int)operation
11+
NS_SWIFT_NAME(_swift_checkClassAndWarnForKeyedArchiving(_:operation:));
12+
@end
13+
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>::Return rawValue)
60+
: data(swift::TwoWordPair<const char *, uintptr_t>(rawValue).first),
61+
length(swift::TwoWordPair<const char *, uintptr_t>(rawValue).second){}
62+
63+
NS_RETURNS_RETAINED
64+
NSString *newNSStringNoCopy() const {
65+
return [[NSString alloc] initWithBytesNoCopy:const_cast<char *>(data)
66+
length:length
67+
encoding:NSUTF8StringEncoding
68+
freeWhenDone:NO];
69+
}
70+
71+
const char &operator[](size_t offset) const {
72+
assert(offset < length);
73+
return data[offset];
74+
}
75+
76+
StringRefLite slice(size_t from, size_t to) const {
77+
assert(from <= to);
78+
assert(to <= length);
79+
return {data + from, to - from};
80+
}
81+
82+
const char *begin() const {
83+
return data;
84+
}
85+
const char *end() const {
86+
return data + length;
87+
}
88+
};
89+
}
90+
91+
/// Assume that a non-generic demangled class name always ends in ".MyClass"
92+
/// or ".(MyClass plus extra info)".
93+
static StringRefLite findBaseName(StringRefLite demangledName) {
94+
size_t end = demangledName.length;
95+
size_t parenCount = 0;
96+
for (size_t i = end; i != 0; --i) {
97+
switch (demangledName[i - 1]) {
98+
case '.':
99+
if (parenCount == 0) {
100+
if (i != end && demangledName[i] == '(')
101+
++i;
102+
return demangledName.slice(i, end);
103+
}
104+
break;
105+
case ')':
106+
parenCount += 1;
107+
break;
108+
case '(':
109+
if (parenCount > 0)
110+
parenCount -= 1;
111+
break;
112+
case ' ':
113+
end = i - 1;
114+
break;
115+
default:
116+
break;
117+
}
118+
}
119+
return {};
120+
}
121+
122+
@implementation NSKeyedUnarchiver (SwiftAdditions)
123+
124+
/// Checks if class \p objcClass is good for archiving.
125+
///
126+
/// If not, a runtime warning is printed.
127+
///
128+
/// \param operation Specifies the archiving operation. Valid operations are:
129+
/// 0: archiving
130+
/// 1: unarchiving
131+
/// \return Returns the status
132+
/// 0: not a problem class (either non-Swift or has an explicit name)
133+
/// 1: a Swift generic class
134+
/// 2: a Swift non-generic class where adding @objc is valid
135+
/// Future versions of this API will return nonzero values for additional cases
136+
/// that mean the class shouldn't be archived.
137+
+ (int)_swift_checkClassAndWarnForKeyedArchiving:(Class)objcClass
138+
operation:(int)operation {
139+
using namespace swift;
140+
const ClassMetadata *theClass = (ClassMetadata *)objcClass;
141+
142+
// Is it a (real) swift class?
143+
if (!theClass->isTypeMetadata() || theClass->isArtificialSubclass())
144+
return 0;
145+
146+
// Does the class already have a custom name?
147+
if (theClass->getFlags() & ClassFlags::HasCustomObjCName)
148+
return 0;
149+
150+
// Is it a mangled name?
151+
const char *className = class_getName(objcClass);
152+
if (!(className[0] == '_' && className[1] == 'T'))
153+
return 0;
154+
// Is it a name in the form <module>.<class>? Note: the module name could
155+
// start with "_T".
156+
if (strchr(className, '.'))
157+
return 0;
158+
159+
// Is it a generic class?
160+
if (theClass->getDescription()->GenericParams.isGeneric()) {
161+
logIfFirstOccurrence(objcClass, ^{
162+
// Use actual NSStrings to force UTF-8.
163+
StringRefLite demangledName = swift_getTypeName(theClass,
164+
/*qualified*/true);
165+
NSString *demangledString = demangledName.newNSStringNoCopy();
166+
NSString *mangledString = NSStringFromClass(objcClass);
167+
switch (operation) {
168+
case 1:
169+
NSLog(@"Attempting to unarchive generic Swift class '%@' with mangled "
170+
"runtime name '%@'. Runtime names for generic classes are "
171+
"unstable and may change in the future, leading to "
172+
"non-decodable data.", demangledString, mangledString);
173+
break;
174+
default:
175+
NSLog(@"Attempting to archive generic Swift class '%@' with mangled "
176+
"runtime name '%@'. Runtime names for generic classes are "
177+
"unstable and may change in the future, leading to "
178+
"non-decodable data.", demangledString, mangledString);
179+
break;
180+
}
181+
NSLog(@"To avoid this failure, create a concrete subclass and register "
182+
"it with NSKeyedUnarchiver.setClass(_:forClassName:) instead, "
183+
"using the name \"%@\".", mangledString);
184+
NSLog(@"If you need to produce archives compatible with older versions "
185+
"of your program, use NSKeyedArchiver.setClassName(_:for:) "
186+
"as well.");
187+
[demangledString release];
188+
});
189+
return 1;
190+
}
191+
192+
// It's a swift class with a (compiler generated) mangled name, which should
193+
// be written into an NSArchive.
194+
logIfFirstOccurrence(objcClass, ^{
195+
// Use actual NSStrings to force UTF-8.
196+
StringRefLite demangledName = swift_getTypeName(theClass,/*qualified*/true);
197+
NSString *demangledString = demangledName.newNSStringNoCopy();
198+
NSString *mangledString = NSStringFromClass(objcClass);
199+
switch (operation) {
200+
case 1:
201+
NSLog(@"Attempting to unarchive Swift class '%@' with mangled runtime "
202+
"name '%@'. The runtime name for this class is unstable and may "
203+
"change in the future, leading to non-decodable data.",
204+
demangledString, mangledString);
205+
break;
206+
default:
207+
NSLog(@"Attempting to archive Swift class '%@' with mangled runtime "
208+
"name '%@'. The runtime name for this class is unstable and may "
209+
"change in the future, leading to non-decodable data.",
210+
demangledString, mangledString);
211+
break;
212+
}
213+
[demangledString release];
214+
NSLog(@"You can use the 'objc' attribute to ensure that the name will not "
215+
"change: \"@objc(%@)\"", mangledString);
216+
217+
StringRefLite baseName = findBaseName(demangledName);
218+
// Offer a more generic message if the base name we found doesn't look like
219+
// an ASCII identifier. This avoids printing names like "ABCモデル".
220+
if (baseName.length == 0 ||
221+
!std::all_of(baseName.begin(), baseName.end(), isASCIIIdentifierChar)) {
222+
baseName = "MyModel";
223+
}
224+
225+
NSLog(@"If there are no existing archives containing this class, you "
226+
"should choose a unique, prefixed name instead: "
227+
"\"@objc(ABC%1$.*2$s)\"", baseName.data, (int)baseName.length);
228+
});
229+
return 2;
230+
}
231+
@end
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#import <Foundation/Foundation.h>
2+
3+
@interface NSKeyedUnarchiver (SwiftAdditions)
4+
+ (int)_swift_checkClassAndWarnForKeyedArchiving:(Class)cls operation:(int)operation
5+
NS_SWIFT_NAME(_swift_checkClassAndWarnForKeyedArchiving(_:operation:));
6+
@end
7+
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// RUN: rm -rf %t && mkdir -p %t
2+
// RUN: %target-build-swift %s -module-name=_Test -import-objc-header %S/Inputs/check_class_for_archiving.h -o %t/a.out
3+
// RUN: %target-run %t/a.out | %FileCheck %s
4+
5+
// REQUIRES: executable_test
6+
// REQUIRES: objc_interop
7+
8+
import Foundation
9+
10+
class SwiftClass {}
11+
12+
class ObjcClass : NSObject {}
13+
14+
private class PrivateClass : NSObject {}
15+
16+
@objc(named_class)
17+
private class NamedClass1 : NSObject {}
18+
19+
@objc(_T3nix11NamedClass2C)
20+
private class NamedClass2 : NSObject {}
21+
22+
class GenericClass<T> : NSObject {}
23+
24+
class DerivedClass : GenericClass<Int> {}
25+
26+
@objc(_T3nix20DerivedClassWithNameC)
27+
private class DerivedClassWithName : GenericClass<Int> {}
28+
29+
struct ABC {
30+
class InnerClass : NSObject {}
31+
}
32+
33+
struct DEF<T> {
34+
class InnerClass : NSObject {}
35+
}
36+
37+
let op: Int32 = 0 // archiving
38+
39+
// CHECK: SwiftClass: 0
40+
print("SwiftClass: \(NSKeyedUnarchiver._swift_checkClassAndWarnForKeyedArchiving(SwiftClass.self, operation: op))")
41+
// CHECK: ObjcClass: 0
42+
print("ObjcClass: \(NSKeyedUnarchiver._swift_checkClassAndWarnForKeyedArchiving(ObjcClass.self, operation: op))")
43+
// CHECK: PrivateClass: 2
44+
print("PrivateClass: \(NSKeyedUnarchiver._swift_checkClassAndWarnForKeyedArchiving(PrivateClass.self, operation: op))")
45+
// CHECK: NamedClass1: 0
46+
print("NamedClass1: \(NSKeyedUnarchiver._swift_checkClassAndWarnForKeyedArchiving(NamedClass1.self, operation: op))")
47+
// CHECK: NamedClass2: 0
48+
print("NamedClass2: \(NSKeyedUnarchiver._swift_checkClassAndWarnForKeyedArchiving(NamedClass2.self, operation: op))")
49+
// CHECK: GenericClass: 1
50+
print("GenericClass: \(NSKeyedUnarchiver._swift_checkClassAndWarnForKeyedArchiving(GenericClass<Int>.self, operation: op))")
51+
// CHECK: DerivedClass: 0
52+
print("DerivedClass: \(NSKeyedUnarchiver._swift_checkClassAndWarnForKeyedArchiving(DerivedClass.self, operation: op))")
53+
// CHECK: DerivedClassWithName: 0
54+
print("DerivedClassWithName: \(NSKeyedUnarchiver._swift_checkClassAndWarnForKeyedArchiving(DerivedClass.self, operation: op))")
55+
// CHECK: InnerClass: 2
56+
print("InnerClass: \(NSKeyedUnarchiver._swift_checkClassAndWarnForKeyedArchiving(ABC.InnerClass.self, operation: op))")
57+
// CHECK: InnerClass2: 1
58+
print("InnerClass2: \(NSKeyedUnarchiver._swift_checkClassAndWarnForKeyedArchiving(DEF<Int>.InnerClass.self, operation: op))")
59+
// CHECK: NSKeyedUnarchiver: 0
60+
print("NSKeyedUnarchiver: \(NSKeyedUnarchiver._swift_checkClassAndWarnForKeyedArchiving(NSKeyedUnarchiver.self, operation: op))")

0 commit comments

Comments
 (0)