Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
83 views
in Technique[技术] by (71.8m points)

Swift function object wrapper in apple/swift

After reading:

I understood that Swift function pointer is wrapped by swift_func_wrapper and swift_func_object (according to the article in 2014).

I guess this still works in Swift 3, but I couldn't find which file in https://github.com/apple/swift best describes these structs.

Can anyone help me?

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

I believe these details are mainly part of the implementation of Swift's IRGen – I don't think you'll find any friendly structs in the source showing you the full structure of various Swift function values. Therefore if you want to do some digging into this, I would recommend examining the IR emitted by the compiler.

You can do this by running the command:

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

which will emit the IR (with demangled symbols) for a -Onone build. You can find the documentation for LLVM IR here.

The following is some interesting stuff that I've been able to learn from going through the IR myself in a Swift 3.1 build. Note that this is all subject to change in future Swift versions (at least until Swift is ABI stable). It goes without saying that the code examples given below are only for demonstration purposes; and shouldn't ever be used in actual production code.


Thick function values

At a very basic level, function values in Swift are simple things – they're defined in the IR as:

%swift.function = type { i8*, %swift.refcounted* }

which is the raw function pointer i8*, along with a pointer to its context %swift.refcounted*, where %swift.refcounted is defined as:

%swift.refcounted = type { %swift.type*, i32, i32 }

which is the structure of a simple reference-counted object, containing a pointer to the object's metadata, along with two 32 bit values.

These two 32 bit values are used for the reference count of the object. Together , they can either represent (as of Swift 4):

  • The strong and unowned reference count of the object + some flags, including whether the object uses native Swift reference counting (as opposed to Obj-C reference counting), and whether the object has a side table.

or

  • A pointer to a side table, which contains the above, plus the weak reference count of the object (on forming a weak reference to an object, if it doesn't already have a side table, one will be created).

For further reading on the internals of Swift reference counting, Mike Ash has a great blog post on the subject.

The context of a function usually adds extra values onto the end of this %swift.refcounted structure. These values are dynamic things that the function needs upon being called (such as any values that it has captured, or any parameters that it has been partially applied with). In quite a few cases, function values won't need a context, so the pointer to the context will simply be nil.

When the function comes to be called, Swift will simply pass in the context as the last parameter. If the function doesn't have a context parameter, the calling convention appears to allow it to be safely passed anyway.

The storing of the function pointer along with the context pointer is called a thick function value,
and is how Swift usually stores function values of known type (as opposed to a thin function value which is just the function pointer).

So, this explains why MemoryLayout<(Int) -> Int>.size returns 16 bytes – because it's made up of two pointers (each being a word in length, i.e 8 bytes on a 64 bit platform).

When thick function values are passed into function parameters (where those parameters are of non-generic type), Swift appears to pass the raw function pointer and context as separate parameters.


Capturing values

When a closure captures a value, this value will be put into a heap-allocated box (although the value itself can get stack-promoted in the case of a non-escaping closure – see later section). This box will be available to the function through the context object (the relevant IR).

For a closure that just captures a single value, Swift just makes the box itself the context of the function (no need for extra indirection). So you'll have a function value which looks like a ThickFunction<Box<T>> from the following structures:

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

In fact, we can actually verify this for ourselves by running the following:

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

We can see that by calling the function between printing out the value of the context object, we can observe the changing in value of the captured variable i.

For multiple captured values, we need extra indirection, as the boxes cannot be stored directly as the given function's context, and may be captured by other closures. This is done by adding pointers to the boxes to the end of a %swift.refcounted.

For example:

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)

Passing functions into parameters of generic type

You'll note that in the previous examples, we used a VoidVoidFunction wrapper for our function values. This is because otherwise, when being passed into a parameter of generic type (such as UnsafeMutablePointer's initialize(to:) method), Swift will put a function value through some reabstraction thunks in order to unify its calling convention to one where the arguments and return are passed by reference, rather than value (the relevant IR).

But now our function value has a pointer to the thunk, rather than the actual function we want to call. So how does the thunk know which function to call? The answer is simple – Swift puts the function that we want to the thunk to call in the context itself, which will therefore look like this:

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

The first thunk that we go through has 3 parameters:

  1. A pointer to where the return value should be stored
  2. A pointer to where the arguments for the function are located
  3. The context object which contains the actual thick function value to call (such as shown above)

This first thunk just extracts the function value from the context, and then calls a second thunk, with 4 parameters:

  1. A pointer to where the return value should be stored
  2. A pointer to where the arguments for the function are located
  3. The raw function pointer to call
  4. The pointer to the context of the function to call

This thunk now retrieves the arguments (if any) from the argument pointer, then calls the given function pointer with these arguments, along with its context. It then stores the return value (if any) at the address of the return pointer.

Like in the previous examples, we can test this like so:

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

Escaping vs. non-escaping capture

When the compiler can determine that the capture of a given local variable doesn't escape the lifetime of the function it's declared in, it can optimise by promoting the value of that variable from the heap-allocated box to the stack (this is a guaranteed optimisation, and occurs in even -Onone). Then, the function's context object need only store a p


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...