Skip to content

Instantly share code, notes, and snippets.

@atrick
Last active April 23, 2024 15:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save atrick/82f045dcc52e8d6a6ddf2177aeacfd13 to your computer and use it in GitHub Desktop.
Save atrick/82f045dcc52e8d6a6ddf2177aeacfd13 to your computer and use it in GitHub Desktop.
Using the `@_unsafeNonescapableResult` function attribute

Using the @_unsafeNonescapableResult function attribute

Indeterminate lifetimes

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 the @_unsafeNonescapableResult function attribute:

extension Optional {
  @_unsafeNonescapableResult
  init(nilLiteral: ()) {
    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 @_unsafeNonescapableResult functions.

Depending on a BitwiseCopyable value

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. The safest way to accomplish this is by passing the object that owns the storage along with 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>(unsafePointer: UnsafePointer<T>, count: Int, dependingOn storageOwner: borrowing StorageOwner)
    -> dependsOn(storageOwner) Self { ... }
}

Although it is preferable to use a API that safely exposes the dependence, an unsafe alternative that takes only the UnsafePointer is convenient when the storage owner is not available in the current scope:

public struct BufferReference<T>: ~Escapable {
  ...
 
  // The client must ensure that `unsafePointer` is valid within entire scope of the caller.
  @_unsafeNonescapableResult
  internal init(unsafePointer: UnsafePointer<Element>, count: Int) { ... }
}

func decode(_ bufferRef: BufferReference<Int>) { /*...*/ }

The initializer that only takes UnsafePointer requires an @_unsafeNonescapableResult attribute. This means that the caller must use some additional lifetime management to guarantee that the BufferReference remains valid within the calling function that created it--the dependence will not be automatically enforced by the compiler. For example, creating a BufferReference directly from an UnsafeBufferPointer requires an unsafe nonescapable result because it is the caller that guarantees the lifetime of the storage:

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.
    // It is valid within `body` because `self` is valid within `body`.
    try body(BufferReference(unsafePointer: baseAddress!, count: count))
  }
}

func decodeArrayAsUBP(array: [Int]) {
  array.withUnsafeBufferPointer { buffer in
    buffer.withUnsafeBufferReference {
      decode($0)
    }
  }
}

By extending Array instead, we can bypass the unsafe initializer. 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)
  }
}

Future Directions

Depending on immutable global variables

Another place where indefinite 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 would be analogous to the 'static lifetime in Rust.

Reference Notes

Joe's Forum post on indefinite lifetimes

Forum post on BitwiseCopyable

Implementation Notes

We could choose to diagnose lifetime dependence on any static scope, even if the source of the dependence is BitwiseCopyable. But doing so would create a false dependence. Simply raising an error for code that violates the lifetime scope is insufficient for safety. Presumably, if the library author needs lifetime dependence over a syntactic scope, then they expect something that happens within that scope to be ordered with respect to something that may happen outside that scope. That expectation needs to be formalized and captured in the program representation at all levels, including SIL after ownership lowering and LLVM IR. We could support "safe" dependence on a borrow of a BitwiseCopyable value by injecting a scope marker in SIL and lowering that into a dummy runtime call that LLVM handles conservatively. But, this is not what programmers actually need or want. Instead, in the common case, they already have an UnsafePointer for which safety within a syntactic scope has been guaranteed using a different mechanism, like Builtin.fix_lifetime(owner) or Builtin.addressof(owner).

Note that we already have this problem with noncopyable types that don't declare a deinit and have only BitwiseCopyable members. We allow programmers to depend on the lifetime of the noncopyable value, but that does not provide any ordering guarantees once we lower out of ownership-SIL. We plan to increase the strength of diagnostics and prevent this case too.

Unsafe API Alternative Notes

Option 1: reuse argument dependence syntax

public struct Ref<T>: ~Escapable {
  private var address: UnsafePointer<T>
 
  @unsafeNonescapableResult
  private init(unsafePointer: UnsafePointer<T>) { ... }

  // The `dependsOn` qualifier creates a dependence from the result to `storageOwner` at the function boundary.
  public init<Owner>(unsafePointer: UnsafePointer<T>, dependingOn storageOwner: borrowing Owner)
    -> dependsOn(storageOwner) Ref<T> {
    Ref(unsafePointer)
  }

Option 2: add a free-standing unsafe dependence API

public func createDependence<Source: ~Escapable, Dest: ~Escapable>(_ source: Source, dependsOn: Dest) -> Source {
  /* Builtin.createDependence */
}

public struct Ref<T>: ~Escapable {
  private var address: UnsafePointer<T>
 
  @unsafeNonescapableResult
  private init(unsafePointer: UnsafePointer<T>) { ... }

  // The `dependsOn` qualifier creates a dependence from the result to `storageOwner` at the function boundary.
  public init<Owner>(unsafePointer: UnsafePointer<T>, dependingOn storageOwner: borrowing Owner)
    -> dependsOn(storageOwner) Ref<T> {
    let ref = Ref(unsafePointer)
    return createDependence(ref, dependsOn: storageOwner)
  }

This has the same effect has the dependsOn annotation by itself but is more explicit. It does not eliminate the need for @unsafeNonescapableResult.

Option 3: add a closure-taking unsafe dependence API

public func withDependence<Source, Dest, Result>(_ from: Source, dependsOn: Dest,
  _ body: (Source) throws -> Result) rethrows -> Result {
  body(Builtin.createDependence(from, dependsOn))
}

public struct Ref<T>: ~Escapable {
  private var address: UnsafePointer<T>
 
  @unsafeNonescapableResult
  private init(unsafePointer: UnsafePointer<T>) { ... }

  public static func withRef<Dest, Result>(unsafePointer: UnsafePointer<T>, dependsOn: Dest,
    _ body: (Ref<T>) throws -> Result) rethrows -> Result {
    let ref = Ref(unsafePointer)
    return withDependence(ref, dependsOn: dependsOn, body)
  }

The closure-taking API limits usability and doesn't appear to solve anything.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment