Hi, just upgraded to Swift 5.10 a day ago (on Linux) and encountered this issue when compiled with optimization flags on. Basically, in some cases, the lifetime of function parameters are no longer hold until the end, see this commit:
Question: is this become some inlining happened? Is it become there is never a semantics guarantee the behavior I thought it would be? Is it something strange about init in particular? What are the simple rules I can derive to make sure I don't break anything in the future when interop with C?
By default, local variables of automatic storage duration do not have precise lifetime semantics. Such objects are simply strong references which hold values of retainable object pointer type, and these values are still fully subject to the optimizations on values under local control.
The lifetimes in question are method arguments, which are local variables. So according to the ObjC ARC rules, which are more or less inherited by Swift, these variable lifetimes may be optimized.
Now, the Swift compiler is actually much more friendly, and only fully optimizes the lifetimes of copy-on-write "value types", Array, Set, Dictionary, and String. The rationale is that (1) such "value types" don't generally have well-defined deinitialization, regardless of the element type, and (2) it is critical for performance to optimize the lifetime of copy-on-write data structures.
Here's more background from an earlier post:
You notice this with initializers and setters because these methods take ownership their arguments by default, and are therefore responsible for destroying them (whenever the implementation of the method pleases).
The simple rule is to follow the formal specification fo ARC, and use withExtendedLifetime to explicitly indicate the end of a values lifetime whenever you unsafely hold a pointer to its storage.
For example, if you expect x to be destroy after a <side effect> that does not reference x, then do this:
{
var x = ...
withExtendedLifetime(x) {
use(x)
<side effect>
}
}
The somewhat more complicated Swift compiler rules are:
You can skip withExtendedLifetime if x is not a predefined "value type" and the "side effect" above is any of the following:
a use of a weak or unowned reference to an object kept alive by x
a use of an unsafe pointer into an object kept alive by x
an "externally visible" side effect, such as I/O, closing a file handle, or thread synchronization. (This only applies if deinitialization of x also has an externally visible side effect.)
These more complicated rules exist to handle patterns that appear with some regularity in Swift code.
Rather than breaking existing code by improving lifetime optimizations in these cases, we will allow programers to opt-into lifetime optimization in Swift when using explicit ownership modifiers like borrowing, and consuming. These modifiers are supported on parameter types, but not yet on other local variables, and the implementation is a work-in-progress.
So this is ownership change related and an alternative simple fix is to explicitly marking them as borrowing parameters?
BTW, for withExtendedLifetime, I cannot follow the idiomatic way of doing it because the following snippet would be not well-defined:
public convenience init(_ x: X) {
withExtendedLifetime(x) {
// The side-effect happens within the self.init call.
let rawPointer = x.rawPointer
self.init(rawPointer)
}
}
This is just another way of expressing the same thing, so both look correct:
public convenience init(_ x: X) {
withExtendedLifetime(x) {
// The side-effect happens within the self.init call.
let rawPointer = x.rawPointer
self.init(rawPointer)
}
}
You can change the ownership to borrowing, but could still run into problems after inlining because array lifetimes are canonicalized to their last use.
withExtandedLifetime forces its argument to be alive regardless of whether its block is empty. So your fix is safe. It’s an awkward API that way but does work and it’s the best we have.