作者
原文链接
Gwynne Raskind
Friday Q&A 2014-08-08: Swift Name Mangling

译者注

1.「Name Mangling」在本文中将译作「名字修饰」,这主要是参考了 Wikipedia 中关于该项技术的翻译;

2.「Friday Q&A」中文意为「周五问与答」,但限于该名称是作者文章的系列名称,故保留原文不再翻译。

许久没有参与到 Friday Q&A 中,不过我带着全新主题的文章又回来了:Swift。在最近的几篇文章中,Mike 具体介绍了 Swift 的内部结构是什么样的,但他只是轻描淡写了当查看包含 Swift 的二进制文件时,链接器所看到的:被修饰的(Mangled)符号名。


在 C 之类的编程语言中,任何给定的名称(符号)都只能对应一个函数或者一条数据,那么名字修饰(Name Mangling)就不是必需的。尽管如此,如果我们查看典型的纯 C 二进制文件的符号表,仍然能发现每个函数名中都有一个 _(下划线)作为前缀。举个例子:

$ echo 'int main() { return 0; }' | xcrun clang -x c - -o ./test
$ xctest nm ./test # 译者注:xctest 命令可能无法直接找到,也可直接使用 nm ./test
0000000100000000 T __mh_execute_header
0000000100000f80 T _main
                 U dyld_stub_binder
$

这种简单的「修饰」没有什么作用,其存在主要是历史原因,由于兼容性与统一性而被保留。按照约定,C 中定义的名称将带有下划线,而纯汇编定义的全局符号则没有(尽管许多汇编语言的作者仍会为了统一性而使用下划线作为前缀)。

Obj-C 同样不会在符号名称间产生冲突;Obj-C 的方法实现总是形如 -[class selector],而且 Obj-C 不允许在同一个类中重载具有不同类型签名的同一选择器(Selectors)。

好了,让我们开始玩转名字修饰吧!

如果在编程语言中只有简单的命名而没有提供任何更多的信息,会导致事情变得更加复杂。想一想这个 C++ 中的例子:

$ cat | xcrun clang -x c++ - -o test # 译者注:输入完此行需要手动换行再粘贴下述语句
int foo(int a) { return a * 2; }
int foo(double a) { return a * 2.0; }
int main() { return foo(1) + foo(1.0); }
^D # 译者注:换行后按下键盘 control + D
$ xcrun nm -a test
0000000100000f30 T __Z3food
0000000100000f10 T __Z3fooi
0000000100000000 T __mh_execute_header
0000000100000f60 T _main
                 U dyld_stub_binder

由于 foo 指的是两个具有不同签名的不同函数,而这样的语法在 C++ 中是合法的,因而不可能简单地生成两个 _foo 符号;链接器将无法将它们对应起来。所以 C++ 编译器使用一组严格的编码规则来「修饰」符号。

与 C 和 Obj-C 不同,C++ 和 Swift 中的函数名本身不足以区分每个单独的函数实现。因此同名但参数类型不同的函数(例如 foo(int)foo(double))需要更多信息以区分。而使用代码中给定的完整签名(例如 foo(int))将导致链接器中产生许多额外的代码,而且当多个类型名称对应相同的底层类型时会造成混淆(比如 unsignedunsigned int)。取而代之的是,在 C++ 中使用了些许晦涩的类型提升(Promotion)和转换规则,使得结果被修饰为一种利于编译器和链接器使用且不造成混淆的形式。Swift 与之相似。

上述 foo 的示例简单分解如下:

  1. _ 是 C 样式的通用前缀。
  2. _Z 是标志该符号为被修饰的全局 C++ 名称的前缀。
  3. 数字定义了接下来的标识字符数;这里是 33foo 意味着「名字『foo』」。
  4. di 分别代表了内置的 doubleint 类型名;C++ 中的返回值并不作为函数签名的一部分,因此只有参数列表位于函数全名之后。

关于典型的 C++ 编译器如何处理修饰的名字,详见 Itanium C ++ ABI 文档

非常有趣,但作为一篇 Swift 的文章,我们花费了好久终于来到了!

Swift 的名字修饰与 C++ 有所不同。原则上讲,虽然 Swift 清晰地使用了一套基于 C++ 方案的编码,但在更成熟地类型系统中包含了更多信息,并表达概念。

我们来看一个复杂的例子。思考以下过度设计且完全无用的 Swift 代码:

$ xcrun swiftc -emit-library -o test -
struct e {
        enum f {
                case G, H, I
        }
}
class a {
        class b {
                class c {
                        func d(y: a, x w: b, v u: (x: Int) -> Int) -> e.f {
                                return e.f.G
                        }
                }
        }
}
^D
$ xcrun nm -g test
...
0000000000001c90 T __TFCCC4test1a1b1c1dfS2_FTS0_1xS1_1vFT1xSi_Si_OVS_1e1f
...
$

译者注:由于本文发布时间较为久远,因此可使用以下实测过可以运行的代码来验证:

$ xcrun swiftc -emit-library -o test -
// 声明为 public 才可以在全局符号中看到
public struct e {
  public enum f {
    case G, H, I
  }
}

public class a {
  public class b {
    public class c {
      // 新的语法规定「函数类型不能拥有参数标签」需要使用
      // func d(y: a, x w: b, v u: (_ x: Int) -> Int) -> e.f
      // 或 func d(y: a, x w: b, v u: (Int) -> Int)
      public func d(y: a, x w: b, v u: (Int) -> Int) -> e.f {
        return e.f.G
      }
    }
  }
}
^D
$ nm -g test
...
0000000000001190 T _$s4test1aC1bC1cC1d1y1x1vAA1eV1fOAC_AES2iXEtF
0000000000001dd8 S _$s4test1aC1bC1cC1d1y1x1vAA1eV1fOAC_AES2iXEtFTq
0000000000001230 T _$s4test1aC1bC1cCMa
00000000000024a8 D _$s4test1aC1bC1cCMm
...
$ xcrun swift-demangle --compact s4test1aC1bC1cC1d1y1x1vAA1eV1fOAC_AES2iXEtFTq
method descriptor for test.a.b.c.d(y: test.a, x: test.a.b, v: (Swift.Int) -> Swift.Int) -> test.e.f
$ xcrun swift-demangle --compact s4test1aC1bC1cC1d1y1x1vAA1eV1fOAC_AES2iXEtF
test.a.b.c.d(y: test.a, x: test.a.b, v: (Swift.Int) -> Swift.Int) -> test.e.f
$ xcrun swift-demangle --compact s4test1aC1bC1cCMa
type metadata accessor for test.a.b.c
$ xcrun swift-demangle --compact s4test1aC1bC1cCMm
metaclass for test.a.b.c

Swift 将生成数百个符号,我们将拆分其中一个复杂的修饰名 __TFCCC4test1a1b1c1dfS2_FTS0_1xS1_1vFT1xSi_Si_OVS_1e1f

按顺序:

  1. Swift 符号中仍存在 _ 前缀。
  2. _T 是 Swift 全局符号的标志。
  3. F 可以告知我们符号的类型是函数。
  4. C 代表「class」类型。这里嵌套了三个类,因此出现了 3 次。
  5. 4test 是指「模块名(Module Name)」,1a 是指它的类名,生成一个名为 test.a 的类。
  6. 此时,Swift 解析器将创建一个已解析名称的栈,并在已修饰的名称中查找第一个非名称的令牌。这里将找到 1d 后的 f。然后返回并由里及外展开嵌套类型的栈,生成 test.atest.a.b,和 test.a.b.c 作为类名。由于 1d 没有对应的嵌套类型(前面只有三个 C),因此其将成为符号名称 test.a.b.c.d 最里面的部分。
  7. 小写 f 标记该符号为「非柯里化函数(Uncurried Function)」。这里类方法的第一个参数采用了隐式绑定,即实例本身。
  8. 因为我们正在解析凡属类型,所以接下来是参数类型,然后是返回值类型。对于非柯里化函数类型,柯里化参数首当其冲。S2_ 是一个替换,意味着它将使用解析名称时第三个非替换类型(索引从零开始)。这里将是 test.a.b.c(第三个类的类型)。
  9. F 现在以新函数类型的形式,标记函数参数列表的开始。至此显而易见的是,名字修饰高度围绕着类型。
  10. T 标记着「元组」的开始,在此联系上下文为类型列表。

参考