Closures that capture local context aren't just function pointers (i.e. a memory address pointing to the first instruction of a block of compiled code). They're really more like objects.
Consider this code:
let localVariable1 = 5
let localVariable2 = "Hello"
callAClosure { [localVariable1, localVariable2] in
print("The local variables are: \(localVariable1), \(localVariable2)")
}
...
func callAClosure(_ closure: () -> Void) {
closure()
}
The closure passed to callAClosure
is sort of like an instance of an anonymous type created on the fly by the compiler with some sort of _call
or _invoke
method:
protocol Callable {
func _call()
}
...
let localVariable1 = 5
let localVariable2 = "Hello"
struct ClosureObject_vbxgxgedg: Callable { // Some random compiler-generated unique type name
let localVariable1: Int
let localVariable2: String
func _call() {
print("The local variables are: \(localVariable1), \(localVariable2)")
}
}
let closure = ClosureObject_vbxgxgedg(
localVariable1: localVariable1,
localVariable2: localVariable2
)
callAClosure(closure)
func callAClosure<C: Callable>(_ closure: C) {
closure._call()
}
Now imagine all that has to happen for the call to callAClosure
to work. ClosureObject_vbxgxgedg
is a struct, a value type, it has type info (which includes a function pointer for _call
), some size in memory (enough to hold those two capture variables), and it is copied when it is passed around as a function parameter. So when you call callAClosure
, the compiled code has to check the size of this struct, allocate enough memory to hold it, and perform a copy (the type itself will supply the copy operation) to initialize this memory. It will also need to deinitialize (destroy) it later. Function pointers for these copy and destroy operations probably live in the type info alongside the function pointer for _call
.
If callAClosure
stored the closure somewhere, the storage would have to be capable of holding any concrete type that conforms to the Callable
protocol. That's some sort of existential box that probably sticks the actual value in a pointer, along with a pointer to the type info structure that can be followed to find the copy and destroy function pointers for that type, and a witness table holding the function pointer for that type's _call
function. Copying this existential box involves making a new box, and doing a deep copy of the value pointed to by using the type's copy function (found by following the box's type info pointer).
This is all essential because different closures have different amounts of captured state. You have to be sure you copy all of it (will be different amounts of memory for each closure), and the values being copied may have non-trivial copy operations themselves. Of course you have to copy the captured variables if you store the closure because the variables now have to outlive the local scope they were originally created in.
This is all very different than if the closure were just a function pointer, i.e. UnsafeRawPointer
. That's just a simple integer (of whatever size the platform uses for pointers), it's trivially copyable (just copy the bits), and there's no issue of polymorphism (no different "types" of pointers, they're all just pointers and all work identically).
C only understands the latter. A function pointer in C is just a pointer, basically just a specially typed integer holding the address. There's nowhere to fit captured local state into a pointer. The memory is fully occupied by the address of the first instruction, and there's no extra space to stick captured values. Even if there was space, C would have no idea how to correctly copy that extra stuff if it needed to save it somewhere.
Thus, C APIs that use callbacks basically implement a closure-like capability, but the users have to work with it more explicitly. They give you that arbitrary "bag of whatever" to pass along with the function pointer. But you (as usual) need to manage the memory of it (decide when to malloc
it and when to free
it), and you need to deal with discovering the actual type of that bag and reinterpreting it accordingly (luckily you probably know statically and don't need to implement any kind of runtime polymorphism, which Swift has to do to solve the general case).
Perhaps what's surprising is that you can ever pass Swift "blocks" to C code. If you make sure not to capture anything, then Swift can compile the block to an anonymous global function, and that is just a function pointer.
Note: in the example I explicitly captured the two local variables, because implicit capture is more complicated (it's more like capturing the getters and setters for the local variables, which allows you to mutate those local variables directly).