Swift mangling regret #2: there are two mangling schemes and we may never be rid of the first (on Apple platforms).— Jordan Rose (@UINT_MIN) November 5, 2021
What is this treachery? I thought mangling was part of the ABI, and there wasn’t a stable ABI until Swift 5! The explanation is a chain of decisions and features…
Part of the Swift Regrets series.
TLDR: Swift has two mangling schemes and we may never be rid of the first (on Apple platforms).
What is this treachery? I thought mangling was part of the ABI, and there wasn’t a stable ABI until Swift 5! The explanation involves quite a chain of decisions and features…
One feature that Objective-C has that’s still unstable in Swift is looking up a class by its name. Several parts of Cocoa rely on this, especially in xib files, so we had to support it in Swift 1.0. In particular, we wanted to make sure
NSClassFromString(NSStringFromClass(someClass)) gave you the same class back. In retrospect I’m not sure anyone relies on that specifically, but, uh, it’s a nice property and a way to test that everything’s working.
From the start we knew that Swift was going to put its classes in modules, to sidestep any chance of “class ‘Foo’ is defined in two different places; gonna pick one randomly!”. So the “name” of a class from Objective-C’s perspective has to include the module: “FooKit.Foo”. But that’s not the only place you can define classes in Swift. You can nest classes in other types, or even inside functions. (Maybe we shouldn’t have allowed that?) So to keep things uniform, the Objective-C name of a Swift class is always its mangled name—again, a unique identifier. We taught the runtime specifically to translate “FooKit.Foo” for top-level, non-private classes and protocols, and left everything else to mangled names. Which were using Swift 1’s mangling scheme, of course. Never intended to be permanent.
Unfortunately, there’s one use case for NSClassFromString we didn’t account for: archiving. The NSCoding protocol provides a standard serialization mechanism for object graphs, much like Codable does for nested structs. Unlike Codable, however, it’s a typed format. The way you deserialize from an NSCoding archive is to look at an entry, look up its class by name, and then instantiate it. And we didn’t put any restrictions on the use of NSCoding from Swift. That meant that there were archives in the wild that had mangled names in them.
During the first few years of Swift we were willing to break a lot of things for developers if it made the language better. (I wish we had called it a beta, honestly.) But we drew the line at user data. Putting mangled names into archives was fundamentally broken in many cases, the most obvious being if your class was
private, in which case the mangling depended on the name of the source file. (I’ll talk more about that one next time.) But users don’t know or care, and they shouldn’t have to. So, developers got a log whenever they un/archived a non-top-level, non-public Swift class, explaining that the name was unstable but they could specify it explicitly if they had to…and the Objective-C names of Swift classes were forever locked to the “old” mangling.
If only we’d put the restriction in from the start. We couldn’t protect against all uses of NSClassFromString but it might have been enough.
Bonus: if a class is generic, it also has problems, because
Foo<Int> couldn’t be looked up by name if no one had used
Foo<Int> yet. This often meant test code worked but a real app didn’t. Eventually the Swift runtime provided a way to instantiate generics on demand.