一尘不染

Apple / Swift中的Swift函数对象包装器

swift

看完之后:

我了解到,Swift函数指针由swift_func_wrapper和包装swift_func_object(根据2014年的文章)。

我想这在Swift
3中仍然有效,但是我找不到https://github.com/apple/swift中哪个文件最能描述这些结构。

谁能帮我?


阅读 357

收藏
2020-07-07

共1个答案

一尘不染

相信
这些细节主要是Swift的IRGen实现的一部分
-我认为您不会在源代码中找到任何友好的结构来向您展示各种Swift函数值的完整结构。因此,如果您想对此进行深入研究,建议您检查编译器发出的IR。

您可以通过运行以下命令来执行此操作:

xcrun swiftc -emit-ir main.swift | xcrun swift-demangle > main.irgen

它将发出IR(带有去斜线的符号)以进行-Onone构建。您可以在此处找到LLVM
IR的文档

以下是一些有趣的东西,我可以通过自己在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撰写了一篇很棒的博客文章

函数的上下文通常会在此%swift.refcounted结构的末尾添加额外的值。这些值是调用函数时需要的动态事物(例如,它已捕获的任何值或已部分应用的任何参数)。在很多情况下,函数值不需要上下文,因此指向上下文的指针将只是nil

当调用该函数时,Swift会简单地将上下文作为最后一个参数传递。如果该函数没有上下文参数,则调用约定似乎允许无论如何安全地传递它。

函数指针与上下文指针的存储称为 稠密 函数值,这
是Swift通常存储已知类型的函数值的方式(与之相反, 函数值只是函数指针)。

因此,这解释了为什么MemoryLayout<(Int) -> Int>.size返回16个字节的原因-
因为它由两个指针组成(每个指针的长度,即在64位平台上为8个字节)。

当将厚函数值传递给函数参数(这些参数属于非泛型类型)时,Swift似乎会将原始函数指针和上下文作为单独的参数传递。


捕获值

当闭包捕获一个值时,该值将被放入一个堆分配的盒子中(尽管在非转义闭包的情况下,该值本身可以被堆栈提升(请参阅下一节)。该框将通过上下文对象(相关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对函数值使用了包装器。这是因为否则,在将其传递给泛型类型的参数(例如UnsafeMutablePointerinitialize(to:)方法)时,Swift会将函数值通过一些重新提取的thunk进行放置,以便将其调用约定统一为其中的参数和return通过引用传递的,而不是价值(相关IR)。

但是现在我们的函数值有一个指向thunk的指针,而不是我们要调用的实际函数。那么thunk如何知道要调用哪个函数?答案很简单–
Swift将我们想要的函数放到thunk中以在 上下文 本身中调用,因此它将看起来像这样:

// 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>
}

我们经历的第一个重击具有3个参数:

  1. 指向应该存储返回值的指针
  2. 指向函数参数位置的指针
  3. 包含要调用的实际胖函数值的上下文对象(如上所示)

第一个thunk只是从上下文中提取函数值,然后调用带有4个参数的 第二个 thunk:

  1. 指向应该存储返回值的指针
  2. 指向函数参数位置的指针
  3. 要调用的原始函数指针
  4. 指向要调用的函数的上下文的指针

现在,此重击从参数指针中检索参数(如果有),然后使用这些参数及其上下文调用给定的函数指针。然后,它将返回值(如果有)存储在返回指针的地址处。

像前面的示例一样,我们可以这样测试:

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

转义与非转义捕获

当编译器确定捕获给定局部变量不会逃避其声明的函数的生存期时,它可以通过将该变量的值从堆分配的框提升到堆栈来优化(这是有保证的)最佳化,甚至发生在-
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在堆栈上存储的值进行优化,而上下文对象仅存储指向该指针的指针(相关的IR)。

因此,每个闭包1的上下文对象看起来都像Box<UnsafePointer<String>>,带有指向堆栈上字符串值的指针。尽管不幸的是,以类似于Schrödinger的方式,尝试通过分配和重新绑定指针来观察到这一点(像以前一样),触发编译器将给定的闭包视为转义–因此,我们再次在Box<String>上下文中查找a

为了处理持有指向捕获值的指针而不是将值保存在自己的堆分配框中的上下文对象之间的差异,Swift创建了闭包的特殊实现,这些闭包将指向捕获值的指针作为参数。

然后,为每个闭包创建一个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.立即求值的闭包实际上不使用上下文对象,指向捕获值的指针仅在调用时直接传递给它。

2020-07-07