jamieQ
(Jamie)
1
I had a surprising issue come up recently that I don’t totally understand, which is possibly related to escaping/non-escaping closures. Consider the following code, which will ‘chain’ together a bunch of methods such that they get called sequentially.
import Foundation
final class Chainer {
var callees: [Callee]
init(callees: [Callee]) {
self.callees = callees
}
func performAndInvoke(_ invocation: () -> Void) {
// if you wrap this implementation in a call to
// `withoutActuallyEscaping(invocation) { invocation in ... }`
// then it won't crash at runtime. why?
var chainedInvocation = invocation
for callee in callees.reversed() {
let nextInvocation = chainedInvocation
chainedInvocation = {
callee.performAndInvoke(nextInvocation)
}
}
chainedInvocation()
}
}
final class Callee {
var value: String
init(value: String) {
self.value = value
}
func performAndInvoke(_ invocation: () -> Void) {
print("performing with value: \(value)")
invocation()
}
}
let c: [Callee] = [
Callee(value: "one"),
// commenting out the second element prevents a runtime crash. why?
Callee(value: "two"),
]
let ch = Chainer(callees: c)
ch.performAndInvoke({ print("final invocation") })
When I compile and run this snippet from the command line, I get a runtime crash with the error terminated by signal SIGBUS (Misaligned address error). If I compile with the undefined behavior sanitizer (swiftc -sanitize=undefined ... ) and run it, I get slightly more info, but it’s still not super-clear to me what the underlying problem is:
==22622==ERROR: UndefinedBehaviorSanitizer: BUS on unknown address (pc 0x000199ea9f5c bp 0x000199eea5e4 sp 0x00016b9a1cd0 T16579874)
==22622==The signal is caused by a UNKNOWN memory access.
==22622==Hint: this fault was caused by a dereference of a high value address (see register values below). Disassemble the provided pc to learn which register was used.
If I wrap the implementation within a call to withoutActuallyEscaping, it seems to fix the issue, but I’m not entirely sure why. It’s a bit surprising to me that this compiles successfully but seems to cause invalid memory access at runtime. Any ideas what the underlying issue is in this instance? Thanks in advance!
2 Likes
tera
2
It does look like a bug as if I step through the app - it works, and if I run it - it crashes. The bug disappears when I put "escaping" for the closure parameter (even if the call is not actually escaping).
Compiler bug aside: the app would crash on stack overflow if you pass it a long list of items (like 50K), so proceed with this approach with care.
Edit: The following simpler version seems to be doing the same as your version, just without recursion / call chaining, so it won't blow stack up when passed a long list of items:
func performAndInvoke(_ invocation: () -> Void) {
for callee in callees {
callee.performAndInvoke {}
}
invocation()
}
I'd optimise it making the closure parameter of "callee.performAndInvoke" optional.