Is it possible to get the retain count of a Closure in Swift?

In Swift, Closures are reference types. Which means (I think, please correct me if I'm wrong) that they have a lifetime managed by a retain count. I can get the retain count of an object using CFGetRetainCount, but this doesn't work for Closures because they can't conform to AnyObject (or any protocol for that matter).

I understand that manually retreiving or relying on the retain count for objects is to be avoided, It's purely for experimentation to prove to myself that this is how Closure lifetime is managed by the runtime.

The representation of a Swift function type is two pointers: a function pointer and a context pointer. The context pointer is referenced-counted using the Swift reference-counting system, but (unlike Swift classes) is not compatible with ObjC reference-counting and cannot be used as an AnyObject.

7 Likes

I guess you can do unsafe bitcasts and pointer arithmetic and get it out, but may I ask you why would you ever need this?

I don't need it, it's purely for experimentation to prove to myself how it works.

Thanks very much for the response @John_McCall !

I’ve been mulling over what you said and I just wanted to make sure I’m on the right path.

When you say context pointer, is it productive to think of the context as some sort of anonymous class which wraps all the variables and constants which are captured by the closure?

Using the example from the Swift documentation:

func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementer() -> Int {
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}

Would the context here resemble something like:

class IncrementerContext { 
    var runningTotal
}

(I'm just using Swift class syntax here for convenience, as far as I understand the context is stored in a block)

It seems to me that SIL functions with context (i.e. closures) are named "thick":

  • @convention(thin) indicates a "thin" function reference, which uses the Swift calling convention with no special "self" or "context" parameters.
  • @convention(thick) indicates a "thick" function reference, which uses the Swift calling convention and carries a reference-counted context object used to represent captures or other state required by the function. This attribute is implied by @callee_owned or @callee_guaranteed .

Most probably if you emit SIL with -emit-sil or IR with -emit-ir flags passed to swiftc you'll be able to see how contexts are manipulated and reference-counted.

Notice how similar (probably even dual) closures are to delegates. You can easily "emulate" closures this way:

protocol ClosureDelegate {
  associatedtype Input
  associatedtype Output

  func invoke(_ input: Input) -> Output
}

final class IncrementerClosure: ClosureDelegate {
  // context is embedded as instance properties
  var runningTotal: Int
  var amount: Int

  func invoke(()) -> Int {
    runningTotal += amount
    return runningTotal
  }
}

func makeIncrementer(forIncrement amount: Int) -> IncrementerClosure {
    return IncrementerClosure(runningTotal: 0, amount: amount)
}

In fact, this is probably the reason why so many Objective-C APIs are delegate-heavy, given that blocks (i.e. closures) were added to Objective-C relatively late in its lifetime. Also, C# delegates are basically closures, as far as I understand.

Important to note that if structs were used instead of classes to implement closure contexts in this "emulation", that wouldn't work as we expect. Closures capture their environment by reference by default, not by value (at least in Swift).

2 Likes

Closure contexts are themselves immutable, so whether they're value types or reference types is somewhat moot. When they capture mutable variables, they do so as if a struct contained a class reference—the struct itself has value semantics, but references mutable data elsewhere. In some ways, closures are more like value types, because the language does not guarantee that closures have a stable identity, unlike class objects.

4 Likes

Could you elaborate please? What does "stable identity" mean in this context (pardon the pun)?

1 Like

You can use === to observe whether two class references point to the same object or not. No such operation exists for closures. You can memcmp the bits of two function values, but there's no guarantee that will have any correspondence to any specific notion of equality, since the compiler might coalesce "different" functions that are identical at the machine code level, or put different thunks or wrappers around the "same" function.

4 Likes