Swift库二进制接口(ABI)兼容性研究
前言阿里云APP组件化过程中,我们拆分出了若干基础组件库和业务代码库,由于代码是采用Swift编写的,所以这些库都是动态库形式。在上一个正式版本,组件化达到了完全形态,主工程只剩下一个壳,所有代码都以pod组件的形式引入,最后交付的是一个主工程的壳二进制程序,和十几个动态库Framework。这些动态库都是运行时才链接到主程序中的。 开发中,我们在某个组件上工作时,其他组件也都是以动态库二进制形式引入的,节省Rebuild源码成本。但开发过程中我们遇到了一些问题。 遇到问题组件拆分出来以后,无可避免,组件之间会存在一些非扁平的依赖关系,如A依赖B依赖C,同时A依赖C。开发A过程中,可能有需要改动C的情况,于是我们将C更新、打包、发布,而B保持不变(只有A用到C加的特性),A依赖C的新版本继续开发。但我们发现,有一些改动会导致APP闪退,只有在B重新依赖C的新版本也编译、打包、发布新版本之后,才恢复正常。我们注意到,出现这种情况时,一般是我们改动了A中某些类的属性。显而易见,我们遇到的是二进制兼容性问题。 理论上ObjC使用静态库也会遇到这种问题的,但是ObjC2.0引入了non-fragile特性,同时runtime中采用消息转发实现方法调用,规避了大部分会引起ABI不兼容的情况。所以在ObjC中,只要接口兼容,底层组件改动一般是不需要自下往上重新编译链接的。 而Swift,在我们认识里,它没有ObjC那么“动态”,又不像C/C++那么直接(寻址是基地址+偏移量,内存布局改变引起ABI不兼容),那它会存在哪些能引起ABI不兼容的情况呢?上个版本我们没有弄明白,保险起见,底层改动后我们都把上层重新编译连接,非常蛋疼。所以,这两天我尝试把这个问题弄清楚。 复现实例我们准备三个代码文件: // Foo.swift public class SimpleClass { public var x: Int = 0 public var y: Int = 0 public init() { x = 1 y = 2 } public func sum() -> Int { return x + y } }
// Bar.swift import Foo public func bar() -> Int { return SimpleClass().sum() }
// main.swift import Foundation import Foo import Bar print("bar (bar())")
然后分别编译出动态库,链接到main,运行: $ swiftc -emit-module -emit-library Foo.swift $ swiftc -emit-module -emit-library Bar.swift -I. -L. -lFoo $ swiftc -I. -L. -lFoo -lBar main.swift $ ./main bar 3 // output 然后我们给 // Foo.swift public class SimpleClass { public var diff: Int? public var x: Int = 0 public var y: Int = 0 public init() { x = 1 y = 2 } public func sum() -> Int { return x + y } }
单独编译 $ swiftc -emit-module -emit-library Foo.swift $ ./main [1] 52482 segmentation fault ./main // output $ swiftc -I. -L. -lFoo -lBar main.swift [1] 52645 segmentation fault ./main // output 可以看到,无论是否重新编译main,只要不重新编译 排查我们没有挑选一些在C/C++里容易理解的ABI不兼容的场景,如直接引用发生变化的对象成员变量,继承关系下父类字段增删等。上述复现实例中,我们在 还是要从内存布局说起。 对象内存布局Swift对象的内存布局我也是这两天慢慢明白过来的,现在放在最前头,后边说起来就容易理解了。在上一次我为了实现
?
Paste_Image.png
从这个视频,我们知道,普通Swift对象,即使是纯Swift对象,其实instance的布局也是和ObjC保持一致,大概是这样:
?
Paste_Image.png
我们打开Swift ABI文档。在Class Metadata这一章节,我们看到,在ObjC中, 我们直接将 525 __TWoFC3Foo11SimpleClass3sumfT_Si: 526 .quad 152 527 528 .globl __TMLC3Foo11SimpleClass 529 .zerofill __DATA,__common,__TMLC3Foo11SimpleClass,8,3 530 .section __DATA,__data 531 .align 3 532 __TMfC3Foo11SimpleClass: 533 .quad __TFC3Foo11SimpleClassD 534 .quad __TWVBo 535 .quad __TMmC3Foo11SimpleClass // isa指向的位置 536 .quad _OBJC_CLASS_$_SwiftObject 537 .quad __objc_empty_cache 538 .quad 0 539 .quad l__DATA__TtC3Foo11SimpleClass+1 540 .long 3 541 .long 0 542 .long 32 543 .short 7 544 .short 0 545 .long 176 546 .long 16 547 .quad __TMnC3Foo11SimpleClass-(__TMfC3Foo11SimpleClass+80) 548 .quad 0 549 .quad __TFC3Foo11SimpleClassg1xSi // vTable起点 550 .quad __TFC3Foo11SimpleClasss1xSi 551 .quad __TFC3Foo11SimpleClassm1xSi 552 .quad __TFC3Foo11SimpleClassg1ySi 553 .quad __TFC3Foo11SimpleClasss1ySi 554 .quad __TFC3Foo11SimpleClassm1ySi 555 .quad __TFC3Foo11SimpleClasscfT_S0_ 556 .quad __TFC3Foo11SimpleClass3sumfT_Si // sum方法 557 .quad 16 558 .quad 24 559 560 .section __TEXT,__swift3_typeref,regular,no_dead_strip 561 .align 4 562 L___unnamed_7: 563 .asciz "C3Foo11SimpleClass"
然后 $ xcrun swift-demangle *
output:
_TMmC3Foo11SimpleClass ---> metaclass for Foo.SimpleClass _TMfC3Foo11SimpleClass ---> full type metadata for Foo.SimpleClass _TFC3Foo11SimpleClassD ---> Foo.SimpleClass.__deallocating_deinit _TMmC3Foo11SimpleClass ---> metaclass for Foo.SimpleClass _TMnC3Foo11SimpleClass ---> nominal type descriptor for Foo.SimpleClass _TMfC3Foo11SimpleClass ---> full type metadata for Foo.SimpleClass _TFC3Foo11SimpleClassg1xSi ---> Foo.SimpleClass.x.getter : Swift.Int _TFC3Foo11SimpleClasss1xSi ---> Foo.SimpleClass.x.setter : Swift.Int _TFC3Foo11SimpleClassm1xSi ---> Foo.SimpleClass.x.materializeForSet : Swift.Int _TFC3Foo11SimpleClass3sumfT_Si ---> Foo.SimpleClass.sum () -> Swift.Int
很棒,一切符合预期。 调用细节然后我们看一下 38 __TF3Bar3barFT_Si:
39 .cfi_startproc
40 pushq %rbp
41 Ltmp3:
42 .cfi_def_cfa_offset 16
43 Ltmp4:
44 .cfi_offset %rbp,-16
45 movq %rsp,%rbp
46 Ltmp5:
47 .cfi_def_cfa_register %rbp
48 subq $32,%rsp 49 callq __TMaC3Foo11SimpleClass 50 movq %rax,%rdi 51 callq __TFC3Foo11SimpleClassCfT_S0_ // 构造SimpleClass实例 52 movq (%rax),%rdi 53 movq %rdi,-8(%rbp) 54 movq %rax,%rdi 55 movq -8(%rbp),%rcx 56 movq %rax,-16(%rbp) 57 callq *136(%rcx) // 调用sum方法 58 movq -16(%rbp),%rdi 59 movq %rax,-24(%rbp) 60 callq _rt_swift_release 61 movq -24(%rbp),%rax 62 addq $32,%rsp 63 popq %rbp 64 retq 65 .cfi_endproc
跳过细节,我们直接看 引起不兼容的原因这时候我们就可以考虑一下,在复现实例中,我们加入 671 .quad __TMnC3Foo11SimpleClass-(__TMfC3Foo11SimpleClass+80) 672 .quad 0 673 .quad __TFC3Foo11SimpleClassg4diffGSqSi_ 674 .quad __TFC3Foo11SimpleClasss4diffGSqSi_ 675 .quad __TFC3Foo11SimpleClassm4diffGSqSi_ 676 .quad __TFC3Foo11SimpleClassg1xSi 677 .quad __TFC3Foo11SimpleClasss1xSi 678 .quad __TFC3Foo11SimpleClassm1xSi 679 .quad __TFC3Foo11SimpleClassg1ySi 680 .quad __TFC3Foo11SimpleClasss1ySi 681 .quad __TFC3Foo11SimpleClassm1ySi 682 .quad __TFC3Foo11SimpleClasscfT_S0_ 683 .quad __TFC3Foo11SimpleClass3sumfT_Si
也就是说,Swift为新增的字段添加了三个方法,插在 其他情况上述例子主要是展示了一种研究ABI兼容性的思路。据此,我们可以研究其他情况。 StructSwift中,Struct不允许继承,所以它的方法派发不必要依赖 // Foo.swift节选 public struct SimpleStruct { public var x: Int = 0 public var diff: Int = 0 public var y: Int = 0 ... }
// foo.s节选 63 __TFV3Foo12SimpleStruct3sumfT_Si: 64 .cfi_startproc 65 pushq %rbp 66 Ltmp6: 67 .cfi_def_cfa_offset 16 68 Ltmp7: 69 .cfi_offset %rbp,-16 70 movq %rsp,%rbp 71 Ltmp8: 72 .cfi_def_cfa_register %rbp 73 addq %rdx,%rdi // .x + .y 74 seto %al 75 movq %rsi,-8(%rbp) 76 movq %rdi,-16(%rbp) 77 movb %al,-17(%rbp) 78 jo LBB2_2 79 movq -16(%rbp),%rax 80 popq %rbp 81 retq 82 LBB2_2: 83 ud2 84 .cfi_endproc
// bar.s节选 46 Ltmp5: 47 .cfi_def_cfa_register %rbp 48 callq __TFV3Foo12SimpleStructCfT_S0_ 49 movq %rax,%rdi // .x 50 movq %rdx,%rsi 51 movq %rcx,%rdx // .y 52 callq __TFV3Foo12SimpleStruct3sumfT_Si 53 popq %rbp 54 retq 55 .cfi_endproc
可以看到,在 Protocol参考Swift进阶之内存模型和方法调度一文,Swift对于协议类型的采用如下的内存模型 - Existential Container:
?
Paste_Image.png
?
Paste_Image.png
那么,显然,如果属性增删导致属性存储区在栈、堆之间变化,或者类的方法(包括可见属性的getter、setter)增删引起 Extension我们常常在各个地方为类增加扩展,显然它是不会引起不兼容问题的。为 320 .globl __TFC3Foo11SimpleClass2f0fT_T_ 321 .align 4,0x90 322 __TFC3Foo11SimpleClass2f0fT_T_: 323 .cfi_startproc 324 pushq %rbp 325 Ltmp39: 326 .cfi_def_cfa_offset 16 327 Ltmp40: 328 .cfi_offset %rbp,-16 329 movq %rsp,%rbp 330 Ltmp41: 331 .cfi_def_cfa_register %rbp 332 movq %rdi,-8(%rbp) 333 popq %rbp 334 retq 335 .cfi_endproc
静态方法静态方法和扩展类似,只是增加了global方法定义,不会影响到类的metadata,所以不会引起不兼容问题。 总结Swift 3出来时,我们曾看到Chris Lattner的邮件:?回顾Swift3,展望Swift4。他提到Swift4阶段1的任务时说:
现在看起来,Swift的整个设计完全是静态的,"易碎的"(fragile),这使得 但回到我们工程,这个问题已经拦在前面了,我们只能尽量不要频繁在底层组件做非二进制兼容的改动,即使有必要,我们也应该找到一个方案,自动化地完成有时序依赖的构建过程。 更多的情况,我将继续研究,大家有不同的见解,欢迎交流~ Reference
(编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |