当前位置: 首页 > 面试题库 >

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

丌官盛
2023-03-14
问题内容

看完之后:

  • 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_wrapper和包装swift_func_object(根据2014年的文章)。

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

谁能帮我?


问题答案:

相信
这些细节主要是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.立即求值的闭包实际上不使用上下文对象,指向捕获值的指针仅在调用时直接传递给它。



 类似资料:
  • 问题内容: 我想用一种快速的语言创建一个抽象函数。可能吗? 问题答案: Swift中没有抽象的概念(例如Objective-C),但是您可以这样做:

  • 问题内容: 我当前的工作流程涉及使用Applescript本质上界定Excel数据并将其格式化为纯文本文件。我们正在向全Swift环境推进,但是我还没有找到任何将我的Excel数据解析为Swift的工具包。 我唯一想到的就是使用C或其他东西并将其包装,但这并不理想。关于解析此数据以在Swift中使用的任何更好的建议? 目的是消除Applescript,但是我不确定在仍然与Excel文件交互时是否有

  • 函数是组合在一起执行特定任务的一组语句。 Swift 4函数可以像C语言函数一样简单,也可以像Objective C语言函数一样复杂。 它允许在函数调用中传递本地和全局参数值。 函数声明 - 告诉编译器函数的名称,返回类型和参数。 函数定义 - 它提供函数的实际实现的主体。 Swift 4函数包含参数类型及其返回类型。 函数定义 在Swift 4中,函数由关键字定义。 当一个函数被新定义时,它可能

  • Swift 函数用来完成特定任务的独立的代码块。 Swift使用一个统一的语法来表示简单的C语言风格的函数到复杂的Objective-C语言风格的方法。 函数声明: 告诉编译器函数的名字,返回类型及参数。 函数定义: 提供了函数的实体。 Swift 函数包含了参数类型及返回值类型: 函数定义 Swift 定义函数使用关键字 func。 定义函数的时候,可以指定一个或多个输入参数和一个返回值类型。

  • 本文向大家介绍详解Swift中的函数及函数闭包使用,包括了详解Swift中的函数及函数闭包使用的使用技巧和注意事项,需要的朋友参考一下 一、引言 函数是有特定功能的代码段,函数会有一个特定的名称调用时来使用。Swift提供了十分灵活的方式来创建与调用函数。事实上在Swift,每个函数都是一种类型,这种类型由参数和返回值来决定。Swift和Objective-C的一大区别就在于Swift中的函数可以

  • 在Kotlin中,可以从闭包的外部函数返回。 输出: 建立 在Swift中,return退出闭包的执行 输出: 建立 B C 完成 在Swift中有没有办法达到同样的效果?