Skip to content

Instantly share code, notes, and snippets.

@atrick
Last active July 10, 2025 19:58
Show Gist options
  • Select an option

  • Save atrick/55966f0d12addf65935596877cb4c67d to your computer and use it in GitHub Desktop.

Select an option

Save atrick/55966f0d12addf65935596877cb4c67d to your computer and use it in GitHub Desktop.
UnsafePointer borrow semantics

UnsafePointer borrow semantics

Problem Statement

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.

Weaker borrow sementics allows implicit copies

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!
}

Stronger borrow sementics for inout pointers

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.

Stronger borrow sementics require explicit assignment into a local let/var

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!
}

Updating the current standard library source

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.

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