I‘ve recently began trying out lifetime dependencies (currently being pitched here: [Pitch #2] Lifetime dependencies for non-`Escapable` values) and non escapable types and I came across an interesting use case I did not see much discussion about. I am not 100% sure though, if this is something that is safe, so I wanted to ask about it here.
Escaping the pointer from withUnsafeMutablePointer is explicitely disallowed in the docs. However, the pointer should have the same lifetime of the inout variable, so theoretically the compiler should prevent access to the pointer in a scope where the inout variable is already gone, and the pointer pointing to invalid memory, right?
The only thing missing is, how someone could safely access the value. Since we already have a pointer, the easiest way would be to expose the pointer via the unsafeAddress and unsafeMutableAddress accessors I‘ve seen being used similarly in InlineArray and Spans.
var value: T {
unsafeAddress {
UnsafePointer(pointer)
}
@_lifetime(&self) // I think, …
unsafeMutableAddress {
pointer
}
}
The reason I even came to think about such a type was, that it allows to „store“ inout variables in other non escapable types, and allows them to be passed around as long as the lifetimes are correct.
Would someone mind, confirming or denying that this is something that is (or will be when lifetimes are accepted, or formalized) allowed and possible?
The pointer created through implicit bridging of an instance or of an array’s elements is only valid during the execution of the called function. Escaping the pointer to use after the execution of the function is undefined behavior. In particular, do not use implicit bridging when calling an UnsafeMutablePointer initializer.
Undefined behavior means that whatever behavior you are observing now is not guaranteed to be the same across different Swift versions or different platforms.
Like how Set/Dictionary uses a different seed for hashing every time your app is run (so iterating through keys/values will have different order), it is possible that there will be some intentional runtime check to avoid invalid uses of unsafe pointers. (To help with catching possible bugs related to passing pointers to C functions where the C function saves the pointer)
AFAIU I am escaping the pointer but not beyond the execution of the function by using the lifetime annotations so this should not result in undefined behavior. The compiler simply wouldn‘t allow that, at least this is what I think my example achieves.
The lifetime annotations by themselves don't grant withUnsafe*Pointer any additional abilities. It is still undefined behavior to escape the pointer from the closure body. We don't yet have public API for capturing references to individual values like this. You might be able to use InlineArray<1, T> and MutableSpan<T> as a stand-in in the meantime.
What Joe said. Being inout doesn’t actually give value a stable address, so even if you could safely escape the pointer from the closure (you can’t), there compiler would be free to ignore any updates that happen through it.
This is something that we intend to make easier; @Alejandro has done some early drafts of what API for this might look like. CollectionOfOne and/or InlineArray give a somewhat hacky but viable workaround in the short term.
You would put the referenced value inside of an InlineArray<1, T>, and use Span and MutableSpan to form references to the single value inside of the InlineArray, as in:
… and what bothered me was, that I could not do something like this:
struct FullJitterBackoffStrategy<Base: BackoffStrategy>: BackoffStrategy {
var generator: inout some RandomNumberGenerator
var base: Base
mutating func duration(_ attempt: Int) -> Base.Duration {
return .init(attoseconds: Int128.random(in: 0...base.duration(attempt).attoseconds, using: &generator)))
}
}
… instead, I had to kind of extract the randomness into its own protocol conformance, so the user can supply their RNG on their own, per call, like this:
The only other solution I could think of, is to just take a copy of a RandomNumberGenerator and mutate it internally. But this doesn‘t match the signatures in stdlib, which all take inout RNGs, AFAIK.