Swift 中的库演进
Swift 5.0 在 Apple 平台上引入了稳定的二进制接口。这意味着使用 Swift 5.0 编译器构建的应用程序可以使用操作系统内置的 Swift 运行时和标准库,并且现有应用程序将与未来操作系统版本中新的 Swift 运行时版本保持兼容。
Swift 5.1 发布了与二进制稳定性相关的两项新功能,这些功能支持可以分发和与他人共享的二进制框架
-
模块稳定性 允许在一个应用程序中一起使用使用不同编译器版本构建的 Swift 模块。
-
库演进支持 允许二进制框架的开发者对其框架的 API 进行附加更改,同时保持与先前版本的二进制兼容性。
模块稳定性目前需要库演进支持;通常,在构建用于分发的二进制框架时,您将同时启用这两个功能。
有关二进制稳定性、模块稳定性和库演进支持如何协同工作的更多详细信息,请参阅本博客上名为 ABI 稳定性和更多 的早期文章。
何时启用库演进支持
库演进支持默认是关闭的。始终一起构建和分发的框架,例如 Swift Package Manager 包或应用程序内部的二进制框架,不应使用库演进支持构建。
库演进支持仅应在框架与其客户端分开构建和更新时使用。在这种情况下,针对旧版本框架构建的客户端可以在不重新编译的情况下与新版本的框架一起运行。
如果您计划发布将以这种方式使用的框架,请务必至少从第一个版本开始启用库演进,或者最好在开发和测试周期的早期就启用。启用库演进支持会更改框架的性能特征,并引入与枚举的 switch
详尽性相关的源代码不兼容的语言更改。此外,为框架启用库演进支持本身就是一个二进制不兼容的更改,因为在没有库演进的情况下构建的框架不提供任何二进制兼容性保证。
启用库演进支持
Xcode
当使用 Xcode 为 Apple 平台开发时,在框架的目标中设置 BUILD_LIBRARY_FOR_DISTRIBUTION
构建设置。此设置同时启用库演进和模块稳定性。请确保在 Debug 和 Release 构建中都使用此设置。
BUILD_LIBRARY_FOR_DISTRIBUTION
Xcode 构建设置和相关的 .xcframework
支持在 WWDC 2019 的题为 Swift 中的二进制框架 的演讲中介绍。
直接调用编译器
如果您直接从命令行或其他构建系统调用 swiftc
,则可以传递 -enable-library-evolution
和 -emit-module-interface
标志。例如
$ swiftc Tack.swift Barn.swift Hay.swift \
-module-name Horse \
-emit-module -emit-library -emit-module-interface \
-enable-library-evolution
上述调用将生成一个名为 Horse.swiftinterface
的模块接口文件和一个共享库 libHorse.dylib
(macOS) 或 libHorse.so
(Linux)。
库演进模型
库演进允许您对框架进行某些更改,而不会破坏二进制兼容性。如果新版本保持与旧版本的源代码兼容和二进制兼容,我们称框架的更改是弹性的。
在我们详细说明哪些类型的更改是弹性更改之前,我们需要介绍 ABI 公共声明 的概念。这是一个可以从另一个 Swift 模块引用的声明。以下是一些示例
-
所有
public
声明都是 ABI 公共的。 -
使用
@usableFromInline
属性 注释的声明是 ABI 公共的,但在源代码语言中不是公共的;这意味着它们可以从@inlinable
代码中引用,但不能直接从源代码中引用。稍后将更详细地讨论此特殊属性。
如果我们需要明确关注非 ABI 公共声明的行为,则使用术语 ABI 私有。ABI 私有声明是那些声明为 private
、fileprivate
或 internal
且没有 @usableFromInline
属性的声明。
@frozen
属性 也与库演进相关联。此属性更改 ABI 公共结构体或枚举的二进制接口,以公开更多实现细节。通过限制未来哪些类型的更改可以是弹性的,可以牺牲一些灵活性以换取额外的性能。
介绍完这些之后,让我们继续描述框架作者可以引入的一些常见弹性更改,以及要避免的非弹性更改。
弹性更改的示例
-
一般原则是,可以随意添加、删除和更改 ABI 私有声明(根据上述定义)。只有显式声明为 ABI 公共的声明才成为框架二进制接口的一部分。
-
源代码文件中的顶层声明可以重新排序,并在同一框架中的源代码文件之间移动。类型或扩展中的成员可以重新排序,但声明为
@frozen
的结构体和枚举中的存储属性和枚举情况除外。例如,在下面的示例中,我们有一个顶层函数,后跟一个带有两个方法的类。函数和类可以以任何顺序出现,并且类中的两个方法可以重新排序,而不会破坏二进制兼容性
public func sum<T : Sequence>(_ seq: T) -> Int where T.Element == Int { return array.reduce(0, (+)) } open class NetworkHandle { open func open() {} open func close() {} }
这可以改为反转两个顶层声明来编写,而不会对框架的 ABI 产生任何影响
// The declarations of NetworkHandle and sum have been reordered. // This does NOT have any impact on the binary interface of // of the framework. open class NetworkHandle { open func open() {} open func close() {} } public func sum<T : Sequence>(_ seq: T) -> Int where T.Element == Int { return array.reduce(0, (+)) }
相反,在以下
@frozen
枚举定义中,两个case
声明不能重新排序,但两个方法可以。此外,方法和 case 的相对顺序可以更改@frozen public enum Shape { // These cases of an @frozen enum cannot be reordered. // The order of the cases with repect to each other // is part of the framework's binary interface. case rect(w: Int, h: Int) case circle(radius: Int) // The order that these methods are declared // can be reordered. Their ordering is NOT // part of the framework's binary interface. public func area() -> Int {...} public func circumference() -> Int {...} }
-
可以在源代码文件的顶层添加声明。
-
可以将成员添加到类、结构体和枚举类型,只要容器类型未声明为
@frozen
。如果类型为@frozen
,则不能添加存储属性或枚举情况。任何其他类型的成员都可以不受限制地添加。 -
不可变属性可以变为可变属性。属性的二进制接口是一组访问器函数,因此引入可变性等同于添加一个新声明——setter。
例如,假设我们有一个结构体定义了一个只读计算属性
fahrenheit
public struct Temperature { public var celsius: Int public var fahrenheit: Int { (celsius * 9) / 5 + 32 } }
库的新版本可以为
fahrenheit
添加 setterpublic struct Temperature { public var celsius: Int public var fahrenheit: Int { get { (celsius * 9) / 5 + 32 } set { celsius = ((newValue - 32) * 5) / 9 } } }
-
可以将新的协议要求添加到协议,只要新要求在协议扩展中定义了默认实现。
例如,假设我们有一个
PointLike
协议public protocol PointLike { var x: Int { get } var y: Int { get } }
库的新版本可以向协议添加新的属性要求
z
,并提供返回 0 的默认实现public protocol PointLike { var x: Int { get } var y: Int { get } var z: Int { get } } extension PointLike { public var z: Int { 0 } }
如果关联类型在协议本身中指定了默认值,则添加新的关联类型是二进制兼容的
public protocol PointLike { var x: Int { get } var y: Int { get } var z: Int { get } associatedtype Magnitude = Double var magnitude: Magnitude { get } }
这里有一个重要的注意事项。回想一下,Swift 允许所有协议用作泛型约束。此外,不定义关联类型或
Self
要求的协议可以用作类型。此限制仅存在于源代码语言中,并且不影响协议类型值的二进制接口。在上面的示例中,旧版本的
PointLike
可以用作类型,因为它没有任何关联类型或Self
要求。但是,新版本有一个关联类型。所以实际上,虽然此更改是二进制兼容的,但它不是源代码兼容的。因此,最好仅将新的关联类型或Self
要求添加到已经具有关联类型或Self
要求的协议。这样,您可以确保客户端没有协议作为类型的现有用法。 -
可以从源代码文件的顶层删除 ABI 私有声明。由于它们永远不能从框架外部直接引用,因此它们不影响框架的二进制接口。
-
可以从类、结构体和枚举类型中删除 ABI 私有成员,前提是容器类型不是
@frozen
。如果结构体或枚举是@frozen
,则不能删除存储属性或枚举情况。任何其他类型的成员都可以不受限制地删除。 -
私有和内部声明和成员可以变为
public
或@usableFromInline
。public
的类和类成员可以设为open
。 -
可以更改公共声明的实现,只要新实现与现有预期行为兼容。例如,函数的函数体可能会被更高效的算法替换,但产生相同的结果。或者,存储属性可以更改为计算属性,只要计算属性具有相同的观察行为。
例如,以下
Temperature
的实现与我们之前看到的实现是二进制兼容的public struct Temperature { public var celsius: Int { get { ((fahrenheit - 32) * 5) / 9 } set { fahrenheit = (newValue * 9) / 5 + 32 } } public var fahrenheit: Int }
但是,如果
Temperature
是@frozen
,则这不是二进制兼容的。 -
可以将新的协议一致性添加到类、结构体和枚举。(即使它们是
@frozen
。)例如,回想一下我们之前提到的冻结枚举
Shape
@frozen public enum Shape { case rect(w: Int, h: Int) case circle(radius: Int) }
我们可以使其符合标准库的
CustomStringConvertible
协议@frozen public enum Shape : CustomStringConvertible { case rect(w: Int, h: Int) case circle(radius: Int) public var description: String { ... } }
或者,我们可以使用扩展定义一致性,如下所示
extension Shape : CustomStringConvertible { public var description: String { ... } }
-
可以删除与 ABI 私有协议的一致性。
-
可以在两个现有类之间插入超类。例如,假设类
Widget
在版本 1 中继承自类Gadget
public class Gadget {} public class Widget : Gadget {}
我们可以在版本 2 中添加一个新类
Gizmo
,它继承自Gadget
,并同时将Widget
更改为继承自Gizmo
public class Gadget {} public class Gizmo : Gadget {} public class Widget : Gizmo {}
非弹性更改的示例
-
不允许删除 ABI 公共声明,因为现有的客户端代码可以引用这些声明;可以通过源代码或框架的内联函数(已发出到客户端)引用。例如,假设一个框架发布了以下代码
@usableFromInline func doInternalThing() { ... } @inlinable public func doPublicThing() { doInternalThing() }
函数
doInternalThing()
是 ABI 公共的,不能删除,因为现有的客户端应用程序可能已经内联了doPublicThing()
函数的主体,而doPublicThing()
函数是@inlinable
的。 -
可变的 ABI 公共属性不能变为不可变的。在二进制接口中,这将意味着删除 ABI 公共 setter 函数,这是不允许的。
-
从
@frozen
结构体添加或删除存储属性,即使该属性是私有的、文件私有的或内部的。 -
不允许在结构体或枚举上添加或删除
@frozen
属性。 -
不允许更改协议的精炼协议列表。
-
也不允许更改声明的接口。这包括以下内容
-
更改属性的类型
-
更改函数的返回类型或参数类型
-
向函数的参数列表添加参数(即使提供了默认值)
-
从函数的参数列表中删除参数
-
向泛型类型或函数的
where
子句添加或删除泛型约束
-
-
更改默认参数表达式技术上不会破坏二进制兼容性,但是由于默认参数表达式在调用站点内联,因此现有客户端将继续使用旧的默认参数值,直到重新编译。
有关哪些更改是弹性更改或非弹性更改的更详尽说明,请参阅 Swift 编译器源代码存储库中题为 LibraryEvolution.rst 的文档。
选择性退出库演进
现在,我们将详细讨论 @frozen
和 @inlinable
属性。
库演进通过在编译后的客户端代码和框架之间引入抽象级别,从而牺牲性能以换取灵活性。在大多数情况下,允许未来的灵活性是正确的默认设置。但是,有时您的框架会定义非常简单的数据类型,这些数据类型根本无法以任何合理的方式演进。
例如,一个用于二维图形的库可能会定义一个 struct
,用于表示二维空间中的点,表示为两个名为 x
和 y
的 Double
类型的存储属性。此结构体的存储属性布局在未来不太可能更改。
在这些情况下,对于开发者来说,向编译器传达声明在库的未来版本中不会演进可能是有利的。作为回报,当客户端与这些声明交互时,编译器可能会生成更高效的代码。
应谨慎使用这些属性。但是,它们在某些情况下仍然非常有价值,因此接下来我们将详细研究这些属性中的每一个。
内联函数
@inlinable
属性是库开发者承诺函数当前定义在使用库的未来版本时将保持正确的承诺。此承诺允许编译器在构建客户端代码时查看函数体。请注意,尽管名称如此,但并不保证会发生内联;编译器可以选择在客户端内部发出函数的专门的外部副本,或者继续调用在框架中找到的原始版本。
可能需要使用此属性的一个示例是完全根据协议要求实现的泛型算法。假设协议发布的不变量没有更改,则将泛型算法内联到客户端应用程序中始终是正确的。库的未来版本可能会用更高效的实现替换泛型算法,但已内联到客户端应用程序中的现有版本应继续工作。
编译器对 @inlinable
函数体强制执行了一个重要的限制;它们只能引用其他 ABI 公共声明。回想一下,ABI 公共声明是 public
或 @usableFromInline
的声明。@usableFromInline
属性的存在是为了可以为内联代码定义辅助函数,但这些辅助函数不能作为公共接口的一部分直接调用。要理解为什么存在此限制,请考虑如果 @inlinable
函数可以引用 private
函数或类型会发生什么。这些私有函数和类型现在将成为框架二进制接口的一部分,从而阻碍未来的演进。
从二进制兼容性的角度来看,@usableFromInline
声明实际上与 public 声明相同,这就是为什么我们总是谈论ABI 公开声明的概念,它涵盖了两者。一旦发布,@usableFromInline
声明绝不能被移除,或者对其接口进行任何不兼容的更改。
内联函数在 Swift Evolution 提案 SE-0193 跨模块内联和特化 中有更详细的描述。
冻结结构体
@frozen
属性可以应用于结构体,以向客户端公开其存储属性布局。@frozen
结构体的存储属性的添加、移除或重新排序都是二进制不兼容的更改。作为失去灵活性的回报,编译器能够跨模块边界对冻结结构体执行某些优化。
编译器对 @frozen
结构体施加了两个语言限制
-
虽然
@frozen
结构体的存储属性不必是 ABI 公开的,但这些存储属性的类型必须是 ABI 公开类型。这意味着 ABI 私有结构体和枚举永远不能成为框架的二进制接口的一部分,因为它们无法以递归方式包含在 ABI 公开的@frozen
类型中。因此,以下代码是合法的,因为
Widget.id
的类型是Int
,它是 ABI 公开的@frozen public struct Widget { private let id: Int }
但是,除了
id
属性具有自定义私有类型ID
的类似声明是不合法的@frozen public struct Widget { private let id: ID } fileprivate struct ID { private let id: Int }
为了使上面的代码能够编译,可以将
ID
的定义更改为public
或@usableFromInline
。 -
如果结构体中的任何存储属性具有初始值表达式,则这些初始值表达式的编译方式就像它们是
@inlinable
一样,这意味着初始值只能用对其他 ABI 公开声明的引用来表示。例如,以下代码是合法的,因为
doInternalThing()
是@usableFromInline
@usableFromInline func doInternalThing() -> Int { ... } public struct Widget { private let id: Int = doInternalThing() }
但是以下代码是不合法的
func doInternalThing() -> Int { ... } public struct Widget { private let id: Int = doInternalThing() }
请记住,@frozen
仅表示存储属性成员的集合不会更改。它没有对其他类型的结构体成员施加任何限制。添加和重新排序方法以及计算型属性是完全可以的。但是,不要将任何计算型属性更改为存储型属性,反之亦然;并请记住,属性包装器和 lazy
属性在底层实现为存储属性。
最后的注意事项是,实际上在结构体上添加或移除 @frozen
都是二进制不兼容的更改;结构体必须“天生冻结”,或者永远保持弹性!
关于冻结结构体的更多详细信息可以在 Swift Evolution 提案 SE-0260 用于稳定 ABI 的库演进 中找到。
冻结枚举
枚举也可以是 @frozen
的,这承诺不会添加、移除或重新排序枚举用例。(请注意,虽然“移除”在列表中,但从 ABI 公开的枚举中移除用例会破坏二进制兼容性,即使枚举不是 @frozen
的,因为所有用例都是 ABI 公开的。)
与冻结结构体一样,编译器可以跨模块边界更有效地操作冻结枚举值。在枚举上添加或移除 @frozen
是二进制不兼容的。
如果冻结枚举的所有用例都被 switch 语句覆盖,则认为对冻结枚举的 switch
是穷尽的,而对非冻结枚举的 switch 语句必须始终提供 default 或 @unknown
用例。这是启用库演进支持引入的唯一源码不兼容性。
switch 穷尽性的行为在 Swift Evolution 提案 SE-0192 非穷尽枚举 中有详细说明。
平台支持
目前,Swift 编译器仅保证在 Apple 平台上的不同编译器版本之间实现二进制兼容性。这意味着在 Linux 和其他平台上,使用不同版本的 Swift 编译器构建的应用程序和库不一定能在运行时正确链接或运行。
但是,稳定的模块接口和库演进可以在 Swift 支持的所有平台上使用。因此,在非 Apple 平台上,您仍然可以使用同一库的多个版本,而无需重新编译客户端应用程序,前提是所有二进制文件都是使用相同版本的 Swift 编译器构建的。
正如在 ABI 稳定性及更多 中提到的,随着 Swift 在 Linux、Windows 和其他平台上的开发日趋成熟,Swift 核心团队将评估在这些平台上稳定 ABI。这将解除对混合和匹配使用不同编译器版本构建的产物的限制。
Objective-C 互操作性
以下内容仅适用于 Apple 平台。
如果您的框架定义了一个 open
类,则客户端代码中的子类定义必须执行运行时初始化,以应对基类的弹性更改,例如添加新的存储属性或插入超类。此初始化由 Swift 运行时在幕后处理。
但是,如果一个类需要运行时初始化,则只有在新平台版本上运行时,它才对 Objective-C 运行时可见。这在实践中的结果是,在旧平台上,某些功能(例如构建在 NSClassFromString()
之上的功能)对于需要运行时初始化的类将无法按预期工作。此外,除非部署目标设置为足够新的平台版本,否则需要运行时初始化的类不会出现在 Swift 编译器生成的 Objective-C 头文件中。
以下操作系统版本中提供了必要的 Objective-C 运行时功能
- macOS 10.15
- iOS 13.0
- tvOS 13.0
- watchOS 6.0
除非您确定框架的类不会以前述方式与动态 Objective-C 功能结合使用,否则最安全的选择是将上述平台版本作为框架和客户端代码的最低部署目标。
与 -enable-testing 的交互
-enable-testing
编译器标志以特殊模式构建框架,允许其他模块使用 @testable
属性导入该框架。@testable import
使框架中所有 internal
声明对导入模块可见。这通常用于希望测试框架公共 API 之外的代码的单元测试。
-enable-library-evolution
编译器标志与 -enable-testing
结合使用时也受支持,实际上,构建用于测试的框架目标推荐的方式是同时传递这两个标志。但是,重要的是要注意,生成的框架仅在公共 API 的更改方面具有弹性。这意味着通常导入框架的客户端在二进制方面与为测试构建的新版本保持兼容。但是,实际使用 @testable import
的代码(例如框架自身的单元测试)绕过了访问控制,并且必然依赖于它所针对构建的特定框架版本的非弹性实现细节。因此,测试应始终与框架一起构建。
库演进的实现
对于本文的其余部分,我们将深入探讨编译器实现的细节。理解这些细节不是使用库演进功能的必要条件。这些内容仅对 Swift 编译器贡献者或任何对幕后工作原理感到好奇的人感兴趣。
弹性边界
对于给定的单个语言构造,Swift 编译器可能会根据上下文和可用的静态信息量生成不同的代码模式。使用支持库演进的框架与不使用支持库演进的框架之间的主要区别在于,对于某些语言构造,在使用库演进支持的情况下,编译器在生成代码时会更加保守。
一个重要的概念是弹性边界。在单个框架内部,编译器始终完全了解框架的类型和函数。框架内部没有弹性边界,因为框架的所有源文件都假定一起编译。
但是,在构建客户端应用程序时,编译器必须注意仅做出静态假设,以确保即使在框架的未来版本中这些假设仍然成立。跨弹性边界可用的编译时信息的范围受到有意限制,并且某些决策必须推迟到运行时,以便实现库演进支持提供的灵活性。
结构体和枚举
如果结构体或枚举未声明为 @frozen
,则其内存布局在弹性边界之外是不透明的。这包括值的大小和对齐方式,以及在移动、复制和销毁此类型的值时是否必须执行额外的工作(例如,更新引用计数)。
当生成跨弹性边界与弹性结构体或枚举交互的代码时,编译器将始终间接操作该值,传递类型元数据以描述该值的内存布局。这类似于未特化的泛型函数如何操作泛型参数类型的值,这是 2017 年 LLVM 开发者会议题为 实现 Swift 泛型 的演讲中详细讨论的主题。
实现的一个重要特性是,弹性结构体或枚举与非弹性结构体或枚举具有相同的内存布局;在值级别上没有 装箱 或间接寻址。相反,操作这些值的代码必须采取额外的步骤来计算字段偏移量或在函数之间传递值作为参数。这确保了虽然库演进支持可能会增加代码大小,但它不会影响数据的 缓存局部性。
属性
Swift 中的属性有许多不同的类型:存储属性、计算型属性、带有观察器的存储属性,以及一些更特殊的变体,例如 lazy
和 @NSManaged
。
回想一下,从库演进的角度来看,所有属性都公开了一个由访问器函数组成的统一接口。每个属性都有一个 getter 函数。如果属性是可变的,它还将具有 setter 和modify 协程。modify 协程允许在某些用法中生成更高效的代码,例如将属性作为 inout
参数传递。如今,它的存在是一个实现细节,但 向语言添加 modify 访问器的提案 目前正在通过 Swift 演进过程。
编译器通常总是使用访问器函数来跨弹性边界访问属性。这保证了对属性底层实现的更改是弹性的。
当然,例外情况是 @frozen
结构体中的存储属性。虽然访问器函数仍然生成,并且在某些上下文中(例如在发出协议见证表时)使用,但编译器能够在可能的情况下发出对存储属性的直接访问。
协议
当框架发布协议时,客户端代码可以声明符合此协议的类型。编译器生成一个称为协议见证表的函数指针表,以描述每个协议一致性。在泛型参数上调用协议需求需要从协议见证表中加载正确的函数指针。由于协议需求可以重新排序,并且可以添加具有默认实现的新协议需求,因此协议见证表的布局在弹性边界之外必须完全不透明。
这通过两个步骤完成。首先,对于每个协议需求,二进制框架导出一个名为分发桩的特殊函数。分发桩是框架本身的一部分,因此它可以直接硬编码协议需求在见证表中的偏移量。如果协议的声明被更改以重新排序需求,则见证表中条目的顺序会更改,但分发桩的符号名称保持不变。由于客户端代码通过分发桩调用所有协议方法,因此可以保持与框架未来版本的二进制兼容性。
最后,为了应对添加新的协议需求,协议见证表需要运行时实例化。编译器不是直接在客户端代码中发出见证表,而是发出一致性的符号描述。实例化过程将协议需求按正确的顺序放置,并填充缺失的条目以指向其默认实现,从而生成一个格式良好的见证表,该表可以传递给分发桩。
与结构体和枚举不同,协议没有定义选择退出机制来发布协议的确切布局并绕过调度 thunk 的使用。这是因为在实践中开销可以忽略不计。
如果您一直特别关注,您可能会(正确地)猜到,就像其他弹性功能一样,如果一致性是在与协议相同的框架中定义的,则编译器不会使用运行时实例化或调度 thunk。
类
Swift 中的类提供了大量功能,这主要是继承的结果。一个类可以继承自另一个 Swift 超类或 Objective-C 超类;当从 Swift 超类继承时,超类可能在同一模块中,或在另一个模块中,无论是否使用库演进支持构建。
类的方法可以动态调度,从而允许在子类中重写它们。从 Objective-C 类继承的 Swift 类也可以重写 Objective-C 方法。类可以通过将方法声明为 final
来选择退出动态调度。整个类也可以设为 final
。最后但并非最不重要的一点是,可以使用 @objc
属性将类的方法发布到 Objective-C。这里有很多内容,与弹性的相互作用可能很复杂。
这里的关键要点是,对弹性类上的 Swift 原生方法的方法调度是通过调用调度 thunk 来执行的;与协议一样,这允许重新排序类上的方法和添加新方法,而不会干扰调用者。这种机制还允许*超类*添加或删除方法,而不会干扰子类。
当然,@objc
方法使用完全不同的方法调度策略,涉及调用 Objective-C objc_msgSend()
运行时函数,该函数通过哈希表查找而具有弹性。
开发历史
库演进背后的大部分功能已经在编译器之前的版本中逐步测试和推出,从 Swift 3.0 版本开始。
在 Swift 4.0 之前,标准库以特殊模式构建,使用未公开的 -sil-serialize-all
编译器标志启用。此标志早于 @inlinable
属性的实现,并且本质上等同于将所有函数声明为可内联的。没有显式属性可以选择加入每个函数的行为;我们始终在标准库上启用该标志,并在其他任何地方禁用它。
Swift 4.0 引入了可内联函数的实验性实现,当时拼写为 @_inlineable
,并且删除了特殊的 -sil-serialize-all
标志。为了简化过渡,我们只是将所有标准库函数标记为 @_inlineable
,因此起初,这些更改几乎没有功能效果。
在 Swift 4.1 和 4.2 中,我们开始了对标准库的全面审核,以确定哪些应该和不应该成为 @_inlineable
。Swift 4.2 最终推出了 @inlinable
作为官方支持的属性,表明可内联函数的实现已达到所需的完善程度和正确性。
到 Swift 5.0 版本发布时,标准库审核已完成,可内联代码已精简到绝对最小值,确保标准库可以向未来演进。
我们还继续充实弹性结构体和枚举的实现,引入了另一个实验性属性 @_fixed_layout
,该属性后来变为 @frozen
。标准库现在是 ABI 稳定的,但实现这一目标所需的工具之一 @_fixed_layout
属性仍然不是官方语言功能。
Swift 5.1 最终引入了 @frozen
,作为实验性 @_fixed_layout
的替代品,同时保持与 Swift 5.0 标准库的 ABI 兼容性。随着 @frozen
的引入,库演进现在已准备好用于通用用途。
有问题吗?
请随时在 Swift 论坛上关于此帖子的相关主题中发布问题。
参考
以下列表收集了本文档前面找到的各种链接
- 博客文章: ABI 稳定性及更多
- WWDC 演讲: Swift 中的二进制框架
- 规范文档: LibraryEvolution.rst
- 演进提案: SE-0193 跨模块内联和特化
- 演进提案: SE-0260 用于稳定 ABI 的库演进
- 演进提案: SE-0192 非穷举枚举
- 演进建议: 修改访问器
- LLVM 开发者会议演讲: 实现 Swift 泛型