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
This commit is contained in:
Jordan Rose
2017-06-20 17:15:28 -07:00
committed by GitHub
parent 2b5710b437
commit 08c2a7a3ac
2 changed files with 350 additions and 7 deletions

View File

@@ -2,6 +2,7 @@
#include <objc/runtime.h>
#include "swift/Runtime/HeapObject.h"
#include "swift/Runtime/Metadata.h"
@interface NSKeyedUnarchiver (SwiftAdditions)
@@ -10,9 +11,116 @@
NS_SWIFT_NAME(_swift_checkClassAndWarnForKeyedArchiving(_:operation:));
@end
static bool isASCIIIdentifierChar(char c) {
if (c >= 'a' && c <= 'z') return true;
if (c >= 'A' && c <= 'Z') return true;
if (c >= '0' && c <= '9') return true;
if (c == '_') return true;
if (c == '$') return true;
return false;
}
static void logIfFirstOccurrence(Class objcClass, void (^log)(void)) {
static auto queue = dispatch_queue_create(
"SwiftFoundation._checkClassAndWarnForKeyedArchivingQueue",
DISPATCH_QUEUE_SERIAL);
static NSHashTable *seenClasses = nil;
dispatch_sync(queue, ^{
// Will be NO when seenClasses is still nil.
if ([seenClasses containsObject:objcClass])
return;
if (!seenClasses) {
NSPointerFunctionsOptions options = 0;
options |= NSPointerFunctionsOpaqueMemory;
options |= NSPointerFunctionsObjectPointerPersonality;
seenClasses = [[NSHashTable alloc] initWithOptions:options capacity:16];
}
[seenClasses addObject:objcClass];
// Synchronize logging so that multiple lines aren't interleaved.
log();
});
}
namespace {
class StringRefLite {
StringRefLite(const char *data, size_t len) : data(data), length(len) {}
public:
const char *data;
size_t length;
StringRefLite() : data(nullptr), length(0) {}
template <size_t N>
StringRefLite(const char (&staticStr)[N]) : data(staticStr), length(N) {}
StringRefLite(swift::TwoWordPair<const char *, uintptr_t> pair)
: data(pair.first), length(pair.second) {}
NS_RETURNS_RETAINED
NSString *newNSStringNoCopy() const {
return [[NSString alloc] initWithBytesNoCopy:const_cast<char *>(data)
length:length
encoding:NSUTF8StringEncoding
freeWhenDone:NO];
}
const char &operator[](size_t offset) const {
assert(offset < length);
return data[offset];
}
StringRefLite slice(size_t from, size_t to) const {
assert(from <= to);
assert(to <= length);
return {data + from, to - from};
}
const char *begin() const {
return data;
}
const char *end() const {
return data + length;
}
};
}
/// Assume that a non-generic demangled class name always ends in ".MyClass"
/// or ".(MyClass plus extra info)".
static StringRefLite findBaseName(StringRefLite demangledName) {
size_t end = demangledName.length;
size_t parenCount = 0;
for (size_t i = end; i != 0; --i) {
switch (demangledName[i - 1]) {
case '.':
if (parenCount == 0) {
if (i != end && demangledName[i] == '(')
++i;
return demangledName.slice(i, end);
}
break;
case ')':
parenCount += 1;
break;
case '(':
if (parenCount > 0)
parenCount -= 1;
break;
case ' ':
end = i - 1;
break;
default:
break;
}
}
return {};
}
@implementation NSKeyedUnarchiver (SwiftAdditions)
/// Checks if class \p cls is good for archiving.
/// Checks if class \p objcClass is good for archiving.
///
/// If not, a runtime warning is printed.
///
@@ -25,20 +133,21 @@
/// 2: a Swift non-generic class where adding @objc is valid
/// Future versions of this API will return nonzero values for additional cases
/// that mean the class shouldn't be archived.
+ (int)_swift_checkClassAndWarnForKeyedArchiving:(Class)cls
+ (int)_swift_checkClassAndWarnForKeyedArchiving:(Class)objcClass
operation:(int)operation {
const swift::ClassMetadata *theClass = (swift::ClassMetadata *)cls;
using namespace swift;
const ClassMetadata *theClass = (ClassMetadata *)objcClass;
// Is it a (real) swift class?
if (!theClass->isTypeMetadata() || theClass->isArtificialSubclass())
return 0;
// Does the class already have a custom name?
if (theClass->getFlags() & swift::ClassFlags::HasCustomObjCName)
if (theClass->getFlags() & ClassFlags::HasCustomObjCName)
return 0;
// Is it a mangled name?
const char *className = class_getName(cls);
const char *className = class_getName(objcClass);
if (!(className[0] == '_' && className[1] == 'T'))
return 0;
// Is it a name in the form <module>.<class>? Note: the module name could
@@ -48,13 +157,74 @@
// Is it a generic class?
if (theClass->getDescription()->GenericParams.isGeneric()) {
// TODO: print a warning
logIfFirstOccurrence(objcClass, ^{
// Use actual NSStrings to force UTF-8.
StringRefLite demangledName = swift_getTypeName(theClass,
/*qualified*/true);
NSString *demangledString = demangledName.newNSStringNoCopy();
NSString *mangledString = NSStringFromClass(objcClass);
switch (operation) {
case 1:
NSLog(@"Attempting to unarchive generic Swift class '%@' with mangled "
"runtime name '%@'. Runtime names for generic classes are "
"unstable and may change in the future, leading to "
"non-decodable data.", demangledString, mangledString);
break;
default:
NSLog(@"Attempting to archive generic Swift class '%@' with mangled "
"runtime name '%@'. Runtime names for generic classes are "
"unstable and may change in the future, leading to "
"non-decodable data.", demangledString, mangledString);
break;
}
NSLog(@"To avoid this failure, create a concrete subclass and register "
"it with NSKeyedUnarchiver.setClass(_:forClassName:) instead, "
"using the name \"%@\".", mangledString);
NSLog(@"If you need to produce archives compatible with older versions "
"of your program, use NSKeyedArchiver.setClassName(_:for:) "
"as well.");
[demangledString release];
});
return 1;
}
// It's a swift class with a (compiler generated) mangled name, which should
// be written into an NSArchive.
// TODO: print a warning
logIfFirstOccurrence(objcClass, ^{
// Use actual NSStrings to force UTF-8.
StringRefLite demangledName = swift_getTypeName(theClass,/*qualified*/true);
NSString *demangledString = demangledName.newNSStringNoCopy();
NSString *mangledString = NSStringFromClass(objcClass);
switch (operation) {
case 1:
NSLog(@"Attempting to unarchive Swift class '%@' with mangled runtime "
"name '%@'. The runtime name for this class is unstable and may "
"change in the future, leading to non-decodable data.",
demangledString, mangledString);
break;
default:
NSLog(@"Attempting to archive Swift class '%@' with mangled runtime "
"name '%@'. The runtime name for this class is unstable and may "
"change in the future, leading to non-decodable data.",
demangledString, mangledString);
break;
}
[demangledString release];
NSLog(@"You can use the 'objc' attribute to ensure that the name will not "
"change: \"@objc(%@)\"", mangledString);
StringRefLite baseName = findBaseName(demangledName);
// Offer a more generic message if the base name we found doesn't look like
// an ASCII identifier. This avoids printing names like "ABCモデル".
if (baseName.length == 0 ||
!std::all_of(baseName.begin(), baseName.end(), isASCIIIdentifierChar)) {
baseName = "MyModel";
}
NSLog(@"If there are no existing archives containing this class, you "
"should choose a unique, prefixed name instead: "
"\"@objc(ABC%1$.*2$s)\"", baseName.data, (int)baseName.length);
});
return 2;
}
@end

View File

@@ -0,0 +1,173 @@
// RUN: %empty-directory(%t)
// RUN: %target-build-swift %s -module-name=_Test -import-objc-header %S/Inputs/check_class_for_archiving.h -o %t/a.out
// RUN: %target-run %t/a.out 2>&1 | %FileCheck %s
// REQUIRES: executable_test
// REQUIRES: objc_interop
// This test doesn't use StdlibUnittest because it's primarily concerned with
// checking the presence and absence of output.
import Foundation
class SwiftClass {}
func checkArchiving(_ cls: AnyObject.Type) {
NSKeyedUnarchiver._swift_checkClassAndWarnForKeyedArchiving(cls, operation: 0)
}
func checkUnarchiving(_ cls: AnyObject.Type) {
NSKeyedUnarchiver._swift_checkClassAndWarnForKeyedArchiving(cls, operation: 1)
}
func mark(line: Int32 = #line) {
NSLog("--%d--", line)
}
mark() // CHECK: --[[@LINE]]--
checkArchiving(SwiftClass.self)
mark() // CHECK-NEXT: --[[@LINE]]--
private class ArchivedTwice {}
checkArchiving(ArchivedTwice.self)
// CHECK-NEXT: Attempting to archive Swift class '_Test.(ArchivedTwice in {{.+}})' with mangled runtime name '_TtC{{.+[0-9]+}}ArchivedTwice'
// CHECK-NEXT: @objc(_TtC{{.+[0-9]+}}ArchivedTwice)
// CHECK-NEXT: @objc(ABCArchivedTwice)
mark() // CHECK-NEXT: --[[@LINE]]--
checkArchiving(ArchivedTwice.self)
mark() // CHECK-NEXT: --[[@LINE]]--
private class UnarchivedTwice {}
checkUnarchiving(UnarchivedTwice.self)
// CHECK-NEXT: Attempting to unarchive Swift class '_Test.(UnarchivedTwice in {{.+}})' with mangled runtime name '_TtC{{.+[0-9]+}}UnarchivedTwice'
// CHECK-NEXT: @objc(_TtC{{.+[0-9]+}}UnarchivedTwice)
// CHECK-NEXT: @objc(ABCUnarchivedTwice)
mark() // CHECK-NEXT: --[[@LINE]]--
checkUnarchiving(UnarchivedTwice.self)
mark() // CHECK-NEXT: --[[@LINE]]--
private class ArchivedThenUnarchived {}
checkArchiving(ArchivedThenUnarchived.self)
// CHECK-NEXT: Attempting to archive Swift class '_Test.(ArchivedThenUnarchived in {{.+}})' with mangled runtime name '_TtC{{.+[0-9]+}}ArchivedThenUnarchived'
// CHECK-NEXT: @objc(_TtC{{.+[0-9]+}}ArchivedThenUnarchived)
// CHECK-NEXT: @objc(ABCArchivedThenUnarchived)
mark() // CHECK-NEXT: --[[@LINE]]--
checkUnarchiving(ArchivedThenUnarchived.self)
mark() // CHECK-NEXT: --[[@LINE]]--
private class UnarchivedThenArchived {}
checkUnarchiving(UnarchivedThenArchived.self)
// CHECK-NEXT: Attempting to unarchive Swift class '_Test.(UnarchivedThenArchived in {{.+}})' with mangled runtime name '_TtC{{.+[0-9]+}}UnarchivedThenArchived'
// CHECK-NEXT: @objc(_TtC{{.+[0-9]+}}UnarchivedThenArchived)
// CHECK-NEXT: @objc(ABCUnarchivedThenArchived)
mark() // CHECK-NEXT: --[[@LINE]]--
checkArchiving(UnarchivedThenArchived.self)
mark() // CHECK-NEXT: --[[@LINE]]--
class Outer {
class ArchivedTwice {}
class UnarchivedTwice {}
class ArchivedThenUnarchived {}
class UnarchivedThenArchived {}
}
checkArchiving(Outer.ArchivedTwice.self)
// CHECK-NEXT: Attempting to archive Swift class '_Test.Outer.ArchivedTwice'
// CHECK-NEXT: @objc(_TtC{{.+[0-9]+}}ArchivedTwice)
// CHECK-NEXT: @objc(ABCArchivedTwice)
mark() // CHECK-NEXT: --[[@LINE]]--
checkArchiving(Outer.ArchivedTwice.self)
mark() // CHECK-NEXT: --[[@LINE]]--
checkUnarchiving(Outer.UnarchivedTwice.self)
// CHECK-NEXT: Attempting to unarchive Swift class '_Test.Outer.UnarchivedTwice'
// CHECK-NEXT: @objc(_TtC{{.+[0-9]+}}UnarchivedTwice)
// CHECK-NEXT: @objc(ABCUnarchivedTwice)
mark() // CHECK-NEXT: --[[@LINE]]--
checkUnarchiving(Outer.UnarchivedTwice.self)
mark() // CHECK-NEXT: --[[@LINE]]--
checkArchiving(Outer.ArchivedThenUnarchived.self)
// CHECK-NEXT: Attempting to archive Swift class '_Test.Outer.ArchivedThenUnarchived'
// CHECK-NEXT: @objc(_TtC{{.+[0-9]+}}ArchivedThenUnarchived)
// CHECK-NEXT: @objc(ABCArchivedThenUnarchived)
mark() // CHECK-NEXT: --[[@LINE]]--
checkUnarchiving(Outer.ArchivedThenUnarchived.self)
mark() // CHECK-NEXT: --[[@LINE]]--
checkUnarchiving(Outer.UnarchivedThenArchived.self)
// CHECK-NEXT: Attempting to unarchive Swift class '_Test.Outer.UnarchivedThenArchived'
// CHECK-NEXT: @objc(_TtC{{.+[0-9]+}}UnarchivedThenArchived)
// CHECK-NEXT: @objc(ABCUnarchivedThenArchived)
mark() // CHECK-NEXT: --[[@LINE]]--
checkArchiving(Outer.UnarchivedThenArchived.self)
mark() // CHECK-NEXT: --[[@LINE]]--
private class {}
checkArchiving(.self)
// CHECK-NEXT: Attempting to archive Swift class '_Test.( in {{.+}})'
// CHECK-NEXT: @objc(_TtC{{.+[0-9]+}}9)
// CHECK-NEXT: @objc(ABCMyModel)
mark() // CHECK-NEXT: --[[@LINE]]--
checkArchiving(.self)
mark() // CHECK-NEXT: --[[@LINE]]--
class ArchivedTwiceGeneric<T> {}
checkArchiving(ArchivedTwiceGeneric<Int>.self)
// CHECK-NEXT: Attempting to archive generic Swift class '_Test.ArchivedTwiceGeneric<Swift.Int>' with mangled runtime name '_TtGC5_Test20ArchivedTwiceGenericSi_'
// CHECK-NEXT: NSKeyedUnarchiver.setClass(_:forClassName:)
// CHECK-SAME: _TtGC5_Test20ArchivedTwiceGenericSi_
// CHECK-NEXT: NSKeyedArchiver.setClassName(_:for:)
mark() // CHECK-NEXT: --[[@LINE]]--
checkArchiving(ArchivedTwiceGeneric<Int>.self)
mark() // CHECK-NEXT: --[[@LINE]]--
checkArchiving(ArchivedTwiceGeneric<NSObject>.self)
// CHECK-NEXT: Attempting to archive generic Swift class '_Test.ArchivedTwiceGeneric<__ObjC.NSObject>' with mangled runtime name '_TtGC5_Test20ArchivedTwiceGenericCSo8NSObject_'
// CHECK-NEXT: NSKeyedUnarchiver.setClass(_:forClassName:)
// CHECK-SAME: _TtGC5_Test20ArchivedTwiceGenericCSo8NSObject_
// CHECK-NEXT: NSKeyedArchiver.setClassName(_:for:)
mark() // CHECK-NEXT: --[[@LINE]]--
checkArchiving(ArchivedTwiceGeneric<NSObject>.self)
mark() // CHECK-NEXT: --[[@LINE]]--
class UnarchivedTwiceGeneric<T> {}
checkUnarchiving(UnarchivedTwiceGeneric<Int>.self)
// CHECK-NEXT: Attempting to unarchive generic Swift class '_Test.UnarchivedTwiceGeneric<Swift.Int>' with mangled runtime name '_TtGC5_Test22UnarchivedTwiceGenericSi_'
// CHECK-NEXT: NSKeyedUnarchiver.setClass(_:forClassName:)
// CHECK-SAME: _TtGC5_Test22UnarchivedTwiceGenericSi_
// CHECK-NEXT: NSKeyedArchiver.setClassName(_:for:)
mark() // CHECK-NEXT: --[[@LINE]]--
checkUnarchiving(UnarchivedTwiceGeneric<Int>.self)
mark() // CHECK-NEXT: --[[@LINE]]--
class ArchivedThenUnarchivedGeneric<T> {}
checkArchiving(ArchivedThenUnarchivedGeneric<Int>.self)
// CHECK-NEXT: Attempting to archive generic Swift class '_Test.ArchivedThenUnarchivedGeneric<Swift.Int>' with mangled runtime name '_TtGC5_Test29ArchivedThenUnarchivedGenericSi_'
// CHECK-NEXT: NSKeyedUnarchiver.setClass(_:forClassName:)
// CHECK-SAME: _TtGC5_Test29ArchivedThenUnarchivedGenericSi_
// CHECK-NEXT: NSKeyedArchiver.setClassName(_:for:)
mark() // CHECK-NEXT: --[[@LINE]]--
checkUnarchiving(ArchivedThenUnarchivedGeneric<Int>.self)
mark() // CHECK-NEXT: --[[@LINE]]--
class UnarchivedThenArchivedGeneric<T> {}
checkUnarchiving(UnarchivedThenArchivedGeneric<Int>.self)
// CHECK-NEXT: Attempting to unarchive generic Swift class '_Test.UnarchivedThenArchivedGeneric<Swift.Int>' with mangled runtime name '_TtGC5_Test29UnarchivedThenArchivedGenericSi_'
// CHECK-NEXT: NSKeyedUnarchiver.setClass(_:forClassName:)
// CHECK-SAME: _TtGC5_Test29UnarchivedThenArchivedGenericSi_
// CHECK-NEXT: NSKeyedArchiver.setClassName(_:for:)
mark() // CHECK-NEXT: --[[@LINE]]--
checkArchiving(UnarchivedThenArchivedGeneric<Int>.self)
mark() // CHECK-NEXT: --[[@LINE]]--