gpt4 book ai didi

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

转载 作者:行者123 更新时间:2023-12-04 01:58:38 26 4
gpt4 key购买 nike

看完之后:

  • https://github.com/rodionovd/SWRoute/wiki/Function-hooking-in-Swift
  • https://github.com/rodionovd/SWRoute/blob/master/SWRoute/rd_get_func_impl.c

  • 我了解到Swift函数指针被 swift_func_wrapperswift_func_object包裹(根据2014年的文章)。

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

    谁能帮我?

    最佳答案

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

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

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

    它将为 -Onone 构建发出 IR(带有 demangled 符号)。你可以找到 the documentation for LLVM IR here

    以下是我自己在 Swift 3.1 版本中通过 IR 学习到的一些有趣的东西。请注意,这一切都可能在 future 的 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 将简单地将上下文作为最后一个参数传入。如果函数没有上下文参数,调用约定似乎允许它无论如何都可以安全地传递。

    函数指针和上下文指针的存储被称为厚函数值,这也是 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 包装器。这是因为否则,当传递给泛型类型的参数时(例如 UnsafeMutablePointerinitialize(to:) 方法),Swift 会通过一些重新抽象的 thunk 来放置一个函数值,以便将其调用约定统一为传递参数和返回值通过引用,而不是值( the relevant 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>
    }

    我们经过的第一个 thunk 有 3 个参数:
  • 指向应存储返回值的位置的指针
  • 指向函数参数所在位置的指针
  • 包含要调用的实际厚函数值的上下文对象(如上图所示)

  • 第一个 thunk 只是从上下文中提取函数值,然后调用第二个 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>> ,带有指向堆栈中字符串值的指针。尽管不幸的是,以类似于薛定谔的方式,尝试通过分配和重新绑定(bind)指针(像以前一样)来观察这一点会触发编译器将给定的闭包视为转义——因此我们再次查看上下文的 Box<String> .

    为了处理保存指向捕获值的指针而不是将值保存在它们自己的堆分配框中的上下文对象之间的差异 - 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. 立即评估的闭包实际上不使用上下文对象,指向捕获值的指针只是在调用时直接传递给它。

    关于apple/swift 中的 Swift 函数对象包装器,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/43171341/

    26 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com