苹果/ swift中的Swift函数对象包装器

ina*_*miy 11 swift

看完之后:

我明白Swift函数指针是由(swift_func_wrapperswift_func_object根据2014年的文章)包装的.

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

谁能帮我?

Ham*_*ish 14

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

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

xcrun swiftc -emit-ir main.swift | xcrun swift-demangle > main.irgen
Run Code Online (Sandbox Code Playgroud)

它将为-Onone构建发出IR(带有解码符号).您可以在此处找到LLVM IR的文档.

以下是我在Swift 3.1版本中通过IR自己学习的一些有趣的东西.请注意,这是所有可能在未来的版本雨燕改变(至少直到雨燕ABI稳定).不言而喻,下面给出的代码示例仅用于演示目的; 并且不应该在实际的生产代码中使用.


厚函数值

在一个非常基础的层面上,Swift中的函数值是简单的东西 - 它们在IR中被定义为:

%swift.function = type { i8*, %swift.refcounted* }
Run Code Online (Sandbox Code Playgroud)

这是原始函数指针i8*,以及指向其上下文 的指针%swift.refcounted*,其中%swift.refcounted定义为:

%swift.refcounted = type { %swift.type*, i32, i32 }
Run Code Online (Sandbox Code Playgroud)

这是一个简单的引用计数对象的结构,包含指向对象元数据的指针,以及两个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
}
Run Code Online (Sandbox Code Playgroud)

事实上,我们可以通过运行以下内容来实际验证这一点:

// 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)
Run Code Online (Sandbox Code Playgroud)

我们可以看到,通过在打印出上下文对象的值之间调用函数,我们可以观察到捕获变量的值的变化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)
Run Code Online (Sandbox Code Playgroud)

将函数传递给泛型类型的参数

您将注意到,在前面的示例中,我们使用了VoidVoidFunction函数值的包装器.这是因为否则,当传递给泛型类型的参数(例如UnsafeMutablePointer's initialize(to:)方法)时,Swift会通过一些reabstraction thunks放置一个函数值,以便将其调用约定统一到参数和返回值通过引用传递的值.而不是价值(相关的IR).

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

// 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>
}
Run Code Online (Sandbox Code Playgroud)

我们经历的第一个thunk有3个参数:

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

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

  1. 指向应存储返回值的位置的指针
  2. 指向函数参数所在位置的指针
  3. 要调用的原始函数指针
  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
Run Code Online (Sandbox Code Playgroud)

逃避与非逃避捕获

当编译器可以确定给定局部变量的捕获没有逃脱它声明的函数的生命周期时,它可以通过从堆分配的框中将该变量的值提升到堆栈来进行优化(这是有保证的)优化,甚至在-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
}
Run Code Online (Sandbox Code Playgroud)

在这个例子中,因为str只有被已知不会逃避函数生命周期的闭包捕获bar(),编译器才能通过存储str堆栈的值来优化,上下文对象只存储指向它的指针(相关的IR)).

因此,每个闭包1的上下文对象看起来都像Box<UnsafePointer<String>>,指向堆栈上的字符串值.虽然遗憾的是,在类似Schrödinger的方式中,尝试通过分配和重新绑定指针(如前所述)来观察这一点会触发编译器将给定闭包视为转义 - 因此我们再次查看Box<String>上下文.

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

然后,为每个闭包创建一个thunk,它只接受给定的上下文对象,从中提取指向捕获的值,并将其传递给闭包的专用实现.现在,我们可以将指向此thunk的指针与我们的上下文对象一起作为thick函数值.

对于不会转义的多个捕获值,将附加指针简单地添加到框的末尾,即

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>
}
Run Code Online (Sandbox Code Playgroud)

在这种情况下,这种将捕获的值从堆提升到堆栈的优化可能特别有用,因为我们不再需要为每个值分配单独的框 - 例如之前的情况.

此外,值得注意的是,许多具有非转义闭包捕获的情况可以在使用内联的-O构建中更加积极地进行优化,这可能导致上下文对象被完全优化.


1.立即评估的闭包实际上不使用上下文对象,捕获值的指针只是在调用时直接传递给它.