Swift regret: no mangling for library evolution— Jordan Rose (@UINT_MIN) November 3, 2021
“Name mangling” is the process of coming up with a single string that uniquely represents an entity in a program. Swift uses this for symbol names, identifiers within SourceKit, and even as a compact type description format.
Part of the Swift Regrets series.
“Name mangling” is the process of coming up with a single string that uniquely represents an entity in a program. Swift uses this for symbol names, identifiers within SourceKit, and even as a compact type description format. I’m going to focus on the “symbols” part, though, which is how a reference in one module gets resolved to a definition in another. If the library can be swapped out at run time, the mangling scheme also has to be consistent across library and compiler versions.
Mangling makes symbols more complicated, but it can also provide some stopgap safety. If a library changes a parameter from Bool to String, the function’s mangled name will change, and clients that were passing Bool will fail to launch rather than silently breaking type safety. This would be a reason to include types in mangled names even if Swift didn’t have type overloading. There are several tradeoffs, of course, but I hope you get the general point.
What other guarantees are encoded in a symbol name? Not much, it turns out. For example, small structs are passed “fieldwise” by default (i.e. one field per register). If you add a field to the struct, all uses of the struct as a parameter need to be recompiled. However, that’s not true when the struct is “non-frozen”. In that case, callers outside the module have to handle the struct by reference, including when passed as an argument. That means it’s safe to add fields.
The trouble is, the caller and the callee have to agree on the “argument-passing convention” here. If they don’t, the callee will get garbage, just like if a new field were added to a frozen struct passed in registers. This should never happen, but if someone turns on library evolution for their library for the first time (or off!), it could happen by mistake. Or if there’s a compiler bug. Or maybe if Swift adds a feature to treat a dependency as if it were frozen, because you’re bundling it with your app. If the mangling for a function included the parameter convention, we’d be saved by the linker. Linker errors aren’t a great user experience, but it’s better than shipping an app that violates type safety.
Swift 5 established a stable ABI on Apple platforms, and as mentioned that includes mangling. But that only has to apply to modules with library evolution enabled. We can, in theory, still change the mangling for other modules, and for non-public functions. (But there are some reasons why that’s tricky and I’m no longer paid to think through all of them, so I’ll let the current Swift team do that thinking instead, for whatever changes they deem important enough to be worth the trouble.)
I should also, however, mention an alternate approach by my former coworker David Ungar: make all functions use the indirect, non-frozen convention by default, and optimize later. This would not only sidestep this problem to some degree, but it would help with incremental builds too. But that has a whole host of implications, so I’m gonna save it for another time.