我相信这些细节主要是Swift's IRGen 实现的一部分——我认为您不会在源代码中找到任何友好的结构,向您展示各种 Swift 函数值的完整结构.因此,如果您想对此进行深入研究,我建议您检查编译器发出的 IR。
您可以通过运行以下命令来做到这一点:
xcrun swiftc -emit-ir main.swift | xcrun swift-demangle > main.irgen
这将为 -Onone 构建发出 IR(带有去错符号)。你可以找到the documentation for LLVM IR here。
以下是我自己在 Swift 3.1 构建中通过 IR 学到的一些有趣的东西。请注意,这所有可能会在未来的 Swift 版本中发生变化(至少在 Swift ABI 稳定之前)。不用说,下面给出的代码示例仅用于演示目的;并且不应该在实际的生产代码中使用。
厚函数值
在非常基本的层面上,Swift 中的函数值是简单的东西——它们在 IR 中被定义为:
%swift.function = type { i8*, %swift.refcounted* }
这是原始函数指针i8*,以及指向其上下文的指针%swift.refcounted*,其中%swift.refcounted定义为:
%swift.refcounted = type { %swift.type*, i32, i32 }
这是一个简单的引用计数对象的结构,包含一个指向对象元数据的指针,以及两个 32 位值。
这两个 32 位值用于对象的引用计数。一起,它们可以代表(从 Swift 4 开始):
- 对象的强引用计数和无主引用计数 + 一些标志,包括对象是否使用原生 Swift 引用计数(相对于 Obj-C 引用计数),以及对象是否有边表。
或
- 一个指向边表的指针,其中包含上述内容,加上对象的弱引用计数(在形成对对象的弱引用时,如果它还没有边表,则会创建一个)。
如需进一步了解 Swift 引用计数的内部原理,Mike Ash 有一个great blog post on the subject。
函数的上下文通常会在%swift.refcounted 结构的末尾添加额外的值。这些值是函数在被调用时需要的动态事物(例如它已捕获的任何值,或已部分应用的任何参数)。在相当多的情况下,函数值不需要上下文,所以指向上下文的指针只是nil。
当函数被调用时,Swift 会简单地将上下文作为最后一个参数传入。如果函数没有上下文参数,调用约定似乎允许它安全地传递。
函数指针与上下文指针一起存储被称为 thick 函数值,
这也是 Swift 通常存储已知类型的函数值的方式(而不是 瘦 函数值,它只是函数指针)。
所以,这就解释了为什么MemoryLayout<(Int) -> Int>.size 返回 16 个字节——因为它由两个指针组成(每个指针的长度都是一个字,即 64 位平台上的 8 个字节)。
当厚函数值被传递给函数参数(这些参数是非泛型类型)时,Swift 似乎将原始函数指针和上下文作为单独的参数传递。
捕获值
当闭包捕获一个值时,该值将被放入堆分配的盒子中(尽管在非转义闭包的情况下,值本身可以被堆栈提升 - 请参阅后面的部分)。该框将通过上下文对象 (the relevant IR) 对函数可用。
对于只捕获单个值的闭包,Swift 只是让盒子本身成为函数的上下文(不需要额外的间接)。因此,您将拥有一个类似于 ThickFunction<Box<T>> 的函数值,来自以下结构:
// The structure of a %swift.function.
struct ThickFunction<Context> {
// the raw function pointer
var ptr: UnsafeRawPointer
// the context of the function value – can be nil to indicate
// that the function has no context.
var context: UnsafePointer<Context>?
}
// The structure of a %swift.refcounted.
struct RefCounted {
// pointer to the metadata of the object
var type: UnsafeRawPointer
// the reference counting bits.
var refCountingA: UInt32
var refCountingB: UInt32
}
// The structure of a %swift.refcounted, with a value tacked onto the end.
// This is what captured values get wrapped in (on the heap).
struct Box<T> {
var ref: RefCounted
var value: T
}
事实上,我们实际上可以通过运行以下命令来验证这一点:
// this wrapper is necessary so that the function doesn't get put through a reabstraction
// thunk when getting typed as a generic type T (such as with .initialize(to:))
struct VoidVoidFunction {
var f: () -> Void
}
func makeClosure() -> () -> Void {
var i = 5
return { i += 2 }
}
let f = VoidVoidFunction(f: makeClosure())
let ptr = UnsafeMutablePointer<VoidVoidFunction>.allocate(capacity: 1)
ptr.initialize(to: f)
let ctx = ptr.withMemoryRebound(to: ThickFunction<Box<Int>>.self, capacity: 1) {
$0.pointee.context! // force unwrap as we know the function has a context object.
}
print(ctx.pointee)
// Box<Int>(ref:
// RefCounted(type: 0x00000001002b86d0, refCountingA: 2, refCountingB: 2),
// value: 5
// )
f.f() // call the closure – increment the captured value.
print(ctx.pointee)
// Box<Int>(ref:
// RefCounted(type: 0x00000001002b86d0, refCountingA: 2, refCountingB: 2),
// value: 7
// )
ptr.deinitialize()
ptr.deallocate(capacity: 1)
我们可以看到,通过在打印出上下文对象的值之间调用函数,我们可以观察到捕获的变量i的值的变化。
对于多个捕获的值,我们需要额外的间接性,因为盒子不能直接存储为给定函数的上下文,并且可能被其他闭包捕获。这是通过将指向框的指针添加到 %swift.refcounted 的末尾来完成的。
例如:
struct TwoCaptureContext<T, U> {
// reference counting header
var ref: RefCounted
// pointers to boxes with captured values...
var first: UnsafePointer<Box<T>>
var second: UnsafePointer<Box<U>>
}
func makeClosure() -> () -> Void {
var i = 5
var j = "foo"
return { i += 2; j += "b" }
}
let f = VoidVoidFunction(f: makeClosure())
let ptr = UnsafeMutablePointer<VoidVoidFunction>.allocate(capacity: 1)
ptr.initialize(to: f)
let ctx = ptr.withMemoryRebound(to:
ThickFunction<TwoCaptureContext<Int, String>>.self, capacity: 1) {
$0.pointee.context!.pointee
}
print(ctx.first.pointee.value, ctx.second.pointee.value) // 5 foo
f.f() // call the closure – mutate the captured values.
print(ctx.first.pointee.value, ctx.second.pointee.value) // 7 foob
ptr.deinitialize()
ptr.deallocate(capacity: 1)
将函数传递给泛型类型的参数
您会注意到,在前面的示例中,我们为函数值使用了 VoidVoidFunction 包装器。这是因为否则,当被传递给泛型类型的参数时(例如UnsafeMutablePointer 的initialize(to:) 方法),Swift 将通过一些重抽象 thunk 来放置函数值,以便将其调用约定统一到参数所在的位置和 return 通过引用传递,而不是值 (the relevant IR)。
但是现在我们的函数值有一个指向 thunk 的指针,而不是我们想要调用的实际函数。那么 thunk 是如何知道调用哪个函数的呢?答案很简单——Swift 将我们希望 thunk 调用的函数放在 context 本身中,因此看起来像这样:
// the context object for a reabstraction thunk – contains an actual function to call.
struct ReabstractionThunkContext<Context> {
// the standard reference counting header
var ref: RefCounted
// the thick function value for the thunk to call
var function: ThickFunction<Context>
}
我们经历的第一个 thunk 有 3 个参数:
- 指向应存储返回值的位置的指针
- 指向函数参数所在位置的指针
- 包含要调用的实际厚函数值的上下文对象(如上所示)
第一个 thunk 只是从上下文中提取函数值,然后调用 second thunk,有 4 个参数:
- 指向应存储返回值的位置的指针
- 指向函数参数所在位置的指针
- 要调用的原始函数指针
- 指向要调用的函数上下文的指针
这个 thunk 现在从参数指针中检索参数(如果有的话),然后使用这些参数及其上下文调用给定的函数指针。然后它将返回值(如果有)存储在返回指针的地址中。
和前面的例子一样,我们可以这样测试:
func makeClosure() -> () -> Void {
var i = 5
return { i += 2 }
}
func printSingleCapturedValue<T>(t: T) {
let ptr = UnsafeMutablePointer<T>.allocate(capacity: 1)
ptr.initialize(to: t)
let ctx = ptr.withMemoryRebound(to:
ThickFunction<ReabstractionThunkContext<Box<Int>>>.self, capacity: 1) {
// get the context from the thunk function value, which we can
// then get the actual function value from, and therefore the actual
// context object.
$0.pointee.context!.pointee.function.context!
}
// print out captured value in the context object
print(ctx.pointee.value)
ptr.deinitialize()
ptr.deallocate(capacity: 1)
}
let closure = makeClosure()
printSingleCapturedValue(t: closure) // 5
closure()
printSingleCapturedValue(t: closure) // 7
转义与非转义捕获
当编译器可以确定给定局部变量的捕获不会逃逸声明它的函数的生命周期时,it can optimise by promoting 该变量的值从堆分配的盒子到堆栈(这是保证优化,并且发生在偶数 -Onone 中)。然后,函数的上下文对象只需要在堆栈上存储一个指向给定捕获值的指针,因为它保证在函数退出后不再需要。
因此,当已知捕获变量的闭包不会逃逸函数的生命周期时,可以这样做。
通常,转义闭包是:
- 存储在非局部变量中(包括从函数返回)。
- 被另一个逃逸的闭包捕获。
- 作为参数传递给函数,其中该参数要么标记为
@escaping,要么不是函数类型(注意这包括复合类型,例如可选函数类型)。
因此,以下是可以认为捕获给定变量不会逃逸函数生命周期的示例:
// the parameter is non-escaping, as is of function type and is not marked @escaping.
func nonEscaping(_ f: () -> Void) {
f()
}
func bar() -> String {
var str = ""
// c doesn't escape the lifetime of bar().
let c = {
str += "c called; "
}
c();
// immediately-evaluated closure obviously doesn't escape.
{ str += "immediately-evaluated closure called; " }()
// closure passed to non-escaping function parameter, so doesn't escape.
nonEscaping {
str += "closure passed to non-escaping parameter called."
}
return str
}
在这个例子中,因为 str 只被已知不会逃逸函数 bar() 生命周期的闭包捕获,编译器可以通过将 str 的值存储在堆栈上进行优化,使用仅存储指向它的指针的上下文对象 (the relevant IR)。
因此,每个闭包1 的上下文对象看起来像Box<UnsafePointer<String>>,并带有指向堆栈上字符串值的指针。虽然不幸的是,以类似薛定谔的方式,尝试通过分配和重新绑定指针(如以前)来观察这一点会触发编译器将给定的闭包视为转义 - 所以我们再次查看Box<String> for上下文。
为了处理上下文对象之间的差异,这些对象持有指向捕获值的指针,而不是将值保存在它们自己的堆分配框中——Swift 创建了闭包的特殊实现,这些闭包采用指向捕获值的指针作为论据。
然后,为每个简单地接受给定上下文对象的闭包创建一个 thunk,从中提取指向捕获值的指针,并将其传递给闭包的专门实现。现在,我们可以将一个指向这个 thunk 的指针与我们的上下文对象一起作为粗函数值。
对于多个未转义的捕获值,只需将附加指针添加到框的末尾,即
struct TwoNonEscapingCaptureContext<T, U> {
// reference counting header
var ref: RefCounted
// pointers to captured values (on the stack)...
var first: UnsafePointer<T>
var second: UnsafePointer<U>
}
这种将捕获的值从堆提升到堆栈的优化在这种情况下可能特别有益,因为我们不再需要为每个值分配单独的框 - 例如以前的情况。
此外,值得注意的是,许多具有非转义闭包捕获的情况可以在具有内联的 -O 构建中更积极地优化,这可能导致上下文对象被完全优化掉。
1.立即评估的闭包实际上不使用上下文对象,指向捕获值的指针只是在调用时直接传递给它。