In some cases, a nonescapable value must be constructed without any object that can stand in as the source of a dependence. Consider extending the standard library Optional
or Result
types to be conditionally escapable:
enum Optional<Wrapped: ~Escapable>: ~Escapable {
case none, some(Wrapped)
}
extension Optional: Escapable where Wrapped: Escapable {}
enum Result<Success: ~Escapable, Failure: Error>: ~Escapable {
case failure(Failure), success(Success)
}
extension Result: Escapable where Success: Escapable {}
When constructing an Optional<NotEscapable>.none
or Result<NotEscapable>.failure(error)
case, there's no lifetime to assign to the constructed value in isolation, and it wouldn't necessarily need one for safety purposes, because the given instance of the value doesn't store any state with a lifetime dependency. Instead, the initializer for cases like this can be annotated with dependsOn(immortal)
:
extension Optional {
init(nilLiteral: ()) dependsOn(immortal) {
self = .none
}
}
Once the escapable instance is constructed, it is limited in scope to the caller's function body since the caller only sees the static nonescapable type. If a dynamically escapable value needs to be returned further up the stack, that can be done by chaining multiple dependsOn(immortal)
functions.
Another place where immortal lifetimes might come up is with dependencies on global variables. When a value has a scoped dependency on a global let constant, that constant lives for the duration of the process and is effectively perpetually borrowed, so one could say that values dependent on such a constant have an effectively infinite lifetime as well. This will allow returning a value that depends on a global by declaring the function's return type with dependsOn(immortal)
:
let staticBuffer = ...
func getStaticallyAllocated() -> dependsOn(immortal) BufferReference {
staticBuffer.bufferReference()
}
Occasionally, library authors need to derive a nonescapable value from a BitwiseCopyable
value. This most often happens when converting an UnsafePointer
into a safe reference type. One way to do this is by passing both the object that owns the storage and the pointer into that storage into the safe reference's initializer:
struct BufferReference<T>: ~Escapable {
private var base: UnsafePointer<T>
private var count: Int
init<StorageOwner>(unsafeBaseAddress: UnsafePointer<T>, count: Int, dependingOn storageOwner: borrowing StorageOwner)
-> dependsOn(storageOwner) Self { ... }
}
Passing the storage owner along with the pointer makes the original dependence more explicit, but sometimes the owner is unavailable or inconvenient to pass. The library author may instead declare a dependence directly on the UnsafePointer
:
public struct BufferReference<T>: ~Escapable {
...
// The client must ensure that `unsafeBaseAddress` is valid within entire scope of the caller.
internal init(unsafeBaseAddress: UnsafePointer<Element>, count: Int) dependsOn(unsafeBaseAddress) { ... }
}
A BitwiseCopyable
value has no ownership or lifetime, so the compiler has no way to enforce this dependence. This puts responsibility on the programmer to ensure that the calling function keeps the pointer valid over all uses of the nonescapable return value. This is usually done by calling a closure-taking API like withUnsafePointer
or withUnsafeBufferPointer
:
func decode(_ bufferRef: BufferReference<Int>) { /*...*/ }
extension UnsafeBufferPointer {
// The client must ensure the lifetime of the buffer across the invocation of `body`.
// The client must ensure that no code modifies the buffer during the invocation of `body`.
func withUnsafeBufferReference<Result>(_ body: (BufferReference<Element>) throws -> Result) rethrows -> Result {
// Construct BufferReference using its internal, unsafe API.
try body(BufferReference(unsafePointer: baseAddress!, count: count))
}
}
func decodeArrayAsUBP(array: [Int]) {
array.withUnsafeBufferPointer { buffer in
buffer.withUnsafeBufferReference {
decode($0)
}
}
}
Interfaces that depend on BitwiseCopyable
do not make good public APIs because of the responsibility placed on the calling function. As an alternative, Array
could have been extended to avoid the responsibility of managing an UnsafeBufferPointer
. Here, the compiler will enforce the dependence between BufferReference
and the exclusive access scope of the array, identified by self
:
extension Array {
func withBufferReference<Result>(_ body: (BufferReference<Element>) throws -> Result) rethrows -> Result {
try withUnsafeBufferPointer {
return try body(BufferReference(unsafePointer: $0.baseAddress!, count: $0.count, dependsOn: self))
}
}
}
func decodeArray(array: [Int]) {
array.withBufferReference {
decode($0)
}
}
When a nonescapable return value depends on a BitwiseCopyable
source, that dependence is not enforced in the caller.
A BitwiseCopyable
value has no ownership or lifetime--the compiler is free to create temporary copies as needed and keep those temporary copies around as long as it likes. It follows that there's no way for the compiler to enforce a dependence on a BitwiseCopyable
value, and ignoring that dependence is always valid. This might surprise some programmers:
func decodeBuffer() {
var bufferRef: BufferReference?
do {
let (baseAddress, count) = ...
bufferRef = BufferReference(baseAddress: baseAddress, count: count)
}
decode(bufferRef!)
}
In this example, the programmer creates a nonescapable BufferReference
that depends on baseAddress
, but uses the bufferRef
outside of the syntactic scope of the baseAddress
variable. This is valid code as long as the pointer value that baseAddress
contained when bufferRef
was created is still valid when bufferRef
is used.
Dependencies on BitwiseCopyable
values may be ignored regardless of their static type because of type substitution:
func f1<T>(arg: borrowing T) -> dependsOn(scoped arg) NonescapableType
func f2() {
let i: Int = ...
// Programmer must ensure that the value in `i` is valid over all uses of `ne`.
let ne = f1(arg: i)
//...
use(ne)
}
It follows that the interaction between BitwiseCopyable
and conditionally escapable types leads to conditionally ignored dependence annotations. Conditionally escapable types are introduced in Non-Escapable Types:
struct Box<T: ~Escapable>: ~Escapable {
var t: T
}
// Box gains the ability to escape whenever its
// generic argument is Escapable
extension Box: Escapable where T: Escapable { }
Returning an escapable or conditionally escapable type requires lifetime dependence:
func transfer<T: ~Escapable>(arg: Box<T>) -> dependsOn(arg) Box<T> // 'dependsOn' may be inferred
The compiler ignores this lifetime dependence when it applies to an escapable type:
func escapingInt() -> Box<Int> {
let box = Box(t: 3)
return transfer(arg: box) // OK: Box<Int> is escapable
}
Joe's Forum post on indefinite lifetimes
The @_unsafeNonescapableResult
function attribute can be applied to any function that returns a nonescapable type. This attribute selectively disables lifetime dependence diagnostics on that function, allowing it to return a nonescapable value without a dependence on one of the function's arguments:
func f1(arg: borrowing ArgType) -> dependsOn(scoped arg) NonescapableType
@_unsafeNonescapableResult
func f2() -> NonescapableType { f1(ArgType()) }
func f3() {
let nonescapableValue = f2()
// ...
}
The nonescapable return value can be used within the body of the calling function, but it cannot otherwise escape or be returned up the call stack. This is identical to a return value declared as dependsOn(immortal)
. Unlike dependsOn(immortal)
, however, diagnostics on the return value are suppressed within the function's implementation. For example, f2
can return the nonescapable result of a call to f1
because f2
is annotated with @_unsafeNonescapableResult
.