The most common use-case for non-Escapable adoption is to wrap an unsafe pointer in an non-Escapable and/or non-Copyable type:
@safe
struct MutRef: ~Escapable & ~Copyable {
let _pointer: UnsafeRawPointer
}
The usual lifetime rules don't obviously apply to the trivial UnsafePointer value. Nonetheless, adopters immediately find themselves creating dependencies on the underlying pointer to implement basic primitives. They tend to have strong expectations of the exclusivity and lifetime behavior in certain situations. We've made an effort to add convenience, which "weakens" semantics, but there are tradeoffs to weaker semantics that haven't been considered. We should review these semantics before we end up with accidental source compatibility constraints. We may want to start with "stronger" semantics initially, allowing those to be relaxed in the future.
Currently, for convenience and consistency with Swift's core argument passing model, the dependency on the borrowed pointer is on an implicit local copy of the pointer rather than the underlying field. This makes it easy to violate exclusivity:
@_lifetime(a: copy a)
@_lifetime(b: copy b)
func access2(_ a: inout MutRef, _ b: inout MutRef) {}
func simultaneousAccess(ref1: inout MutRef) {
var ref2 = unsafe MutRef(_pointer: ref1._pointer)
access2(&ref1, &ref2) // Valid... but wrong!
}
If ref2 had exclusive access to ref1 rather than a local copy of ref1._pointer, then the compiler would raise a compile time error.
Similarly, borrowing an implicit copy makes it easy to reference memory after it is released:
struct Owner: ~Escapable & ~Copyable {
var _storage: AnyObject
var _pointer: UnsafeRawPointer // point to memory inside storage
}
func reassign(_ owner: inout Owner) {
owner._storage = ...
owner._pointer = ...
}
func access(_: inout MutRef) {}
func useAfterFree(owner: inout Owner) {
var ref = unsafe MutRef(_pointer: owner._pointer)
reassign(&owner)
access(&ref) // Valid... but wrong!
}
Currently, we special-case trivial inout arguments so that depending on the argument bypasses the local copy:
struct MutRef: ~Escapable & ~Copyable {
var _pointer: UnsafeRawPointer
init(pointer: inout UnsafeRawPointer) { self._pointer = pointer }
}
func checkedSimultaneousAccess(ref1: inout MutRef) {
var ref2 = unsafe MutRef(pointer: &ref1._pointer) // ERROR: overlapping access to 'ref1'
access2(&ref1, &ref2) // NOTE: conflicting access is here
}
This avoids the most surprising mistakes. Swift programmers generally expect an inout argument to require access to the original named variable.
The interesting question is whether non-inout arguments should also borrow the named variable rather than a local copy:
func simultaneousAccess(ref1: inout MutRef) {
var ref2 = MutRef(_pointer: ref1._pointer) ERROR: overlapping access to 'ref1'
access2(&ref1, &ref2) // NOTE: conflicting access is here
}
Adopters can run into all the same problems with stronger borrow semantics, but they need to be more explicit about pointer copies:
func simultaneousAccess(ref1: inout MutRef) {
let p = ref1._pointer
var ref2 = MutRef(_pointer: p)
access2(&ref1, &ref2) // Valid... but wrong!
}
To adopt stronger semantics, we would need to update MutableSpan.extracting:
mutating public func extracting(droppingLast k: Int) -> Self {
let newSpan = unsafe Self(_unchecked: _pointer, byteCount: ...)
return unsafe _overrideLifetime(newSpan, mutating: &self)
}
to
mutating public func extracting(droppingLast k: Int) -> Self {
let pointer = _pointer
let newSpan = unsafe Self(_unchecked: pointer, byteCount: ...)
return unsafe _overrideLifetime(newSpan, mutating: &self)
}
This is an example of how stronger semantics would be less convenient and sometimes require "strange" workarounds.