Contents

SE-0556: Cell, ConstCell, and Volatile

Summary of changes

Introduces a family of Cell types that provide looser requirements around exclusivity, making them suitable for shared memory, synchronization primitives, concurrent data structures, and interoperability with other languages.

Motivation

The Span / Ref family of types, including Span (SE-0447), MutableSpan (SE-0467), OutputSpan (SE-0485), and Ref (SE-0519), as well as their Raw.*Span counterparts, provide memory-safe access to contiguous memory. Span and ref types provide lifetime safety, ensuring that the memory they reference isn't freed while the instance is still accessible, as well as bounds safety for spans because all indexed accesses ensure that the indices are within bounds.

However, they can only reference memory that is non-aliased by other parts of the process or operating system. This is due to types in Swift not having the capability of interior mutability (besides classes).

Spans and the Law of Exclusivity

The Span and Ref family of types depends on Swift's so-called Law of Exclusivity, which states that if there are two accesses to the same value in memory, both of them must be reads. Therefore, any access that can change the value in memory is known to be the only place that will modify that memory, which unlocks important optimization opportunities while still maintaining memory safety for the values in the memory the reference. Fundamentally, maintaining the Law of Exclusivity requires reasoning about all potential aliases of a particular location in memory, i.e., places where there is a reference (or pointer) to that memory that could be used to access the value stored there. Swift maintains the Law of Exclusivity through a mix of language and runtime features. Non-copyable types (like UniqueArray, SE-527) establish unique ownership, whereas non-escapable types (like the Span family of types) limit the scope in which data can be accessed, all at compile time. Copy-on-write collections (such as Array) and dynamic exclusivity checking (e.g., for global variables) establish exclusivity at run-time for places where it isn't possible to reason about every potential alias. Most of this is invisible to the Swift developer, unless they encounter code that violates the Law of Exclusivity. For example, attempting to create two MutableSpan instances that reference into the same Array, or modify the Array while there is an actual Span referencing its storage, will produce a compile-time error about the "overlapping access" that violates exclusivity:

func f() {
  var array = [1, 2, 3, 4, 5]

  let data = array.span // note: conflicting access is here
  array.append(6) // error: overlapping accesses to 'array', but modification requires 
                  // exclusive access; consider copying to a local variable 
  print(data[0])
}

Scenarios involving runtime exclusivity checking are described in more detail in SE-0176.

The Span and Ref dependency on the Law of Exclusivity manifests in its API surface. The subscript for Span uses a borrow accessor (SE-0507), which provides direct access to the value without requiring the caller to make a copy, which is only safe when the underlying value cannot change. Similarly, MutableSpan exclusively references its underlying memory, and it's subscript provides a mutate accessor to directly reference that memory (including modifing it). The choices here are important semantically, because they make it possible to support spans over non-copyable types, and also for performance, because they avoid the need for extraneous copies of the underlying data.

Span without the Law of Exclusivity breaks memory safety

Using the Span types in places where we have lifetime safety but have not established the Law of Exclusivity can cause memory-safety problems. For example, consider the following code that introduces aliasing issues through Span by using unsafe pointers:

class MyClass {
  var counter = 0
  
  func doSomething(_ body: () -> Void) {
    body()
    counter += 1
  }
}

func aliasingSafetyProblem(buffer: UnsafeMutableBufferPointer<MyClass>) {
  let otherBuffer = buffer
  let span = unsafe buffer.span
  unsafe span[0].doSomething {        // call doesn't adjust reference count
    unsafe otherBuffer[0] = MyClass() // MyClass instance in the doSomething call could be freed here
  }
}

Much of the code is marked unsafe here, including deriving a span from an unsafe buffer, because the developer needs to reason about both the lifetime and exclusivity of the unsafe pointer. In the next selection, we explore places where it is possible to reason about lifetime but it is not possible or practical to reason about exclusivity.

Lifetime safety of buffers is useful without the Law of Exclusivity

There are scenarios where the lifetime- and bounds-safety properties of spans are desirable, but it is impossible or impractical to ensure that the memory they reference obeys the Law of Exclusivity. One concrete example is shared memory, where the same region of memory is also accessible by another process or by some other hardware in the system (e.g., a GPU or network controller with direct memory access). One can build an abstraction to ensure that the shared memory's lifetime is correctly managed (e.g., with noncopyable types or classes), but the Law of Exclusivity can never be applied to such memory, because there are, fundamentally, aliases outside of the view of the program that cannot be reasoned about.

Interoperability with the C family of languages is another area where lifetime can be established through conventions, sometimes backed by static analysis. The vision for optional strict memory safety in Swift outlines the use of C attributes to describe memory-safety properties for lifetimes and bounds to enable importing C(++) APIs using pointers as Span-based APIs, allowing them to be used safely from Swift. For example, a C++ API like the following

class MyBuffer {
public:
  std::span<const double> getContents() const __attribute__((lifetimebound));
};

could have its method imported into Swift as:

func getContents() -> Span<Double>

The lifetimebound attribute indicates that the pointer inside the std::span will live as long as the MyBuffer instance is still alive and hasn't been changed (e.g., via a non-const method), which describes the fundamental lifetime safety property. However, it does not imply that there are no aliases for that storage that might modify the memory it references: ensuring the lack of aliases would require reasoning about all C and C++ code that might ever have access to that pointer, which is impractical.

For these cases, where we have lifetime information but cannot provide exclusivity, the only suitable types in the Swift standard library are the Unsafe(Mutable)(Raw)BufferPointer types. However, these types throw away all aspects of memory safety, so their use is generally discouraged.

Synchronization Primitives and Concurrent Data Structures

A good example of safe shared memory are synchronization primitives like Atomic and Mutex. The concept of static mutability breaks down concurrently because multiple threads may need to mutate the same memory at the same time, while types like these help synchronize their accesses to ensure data race safety. Having methods like Mutex.withLock be mutating is impossible because there must only be a single exclusive access to the Mutex but two or more threads may be attempting to call this method at the same time. That's why methods like these are borrowing to allow for shared access while internally ensuring that multiple writes are not happening at the same time. In order to build such primitives, these types need to be able to "mutate" their memory while only having shared access to it. This is achievable through interior mutability.

Interior mutability is the notion of being to mutate memory while multiple shared references to the same memory are being aliased. Under the Law of Exclusivity, this is explicitly not supported due to the data racey nature that this introduces in multithread contexts.

struct Mutex<Value: ~Copyable>: ~Copyable {
  var value: Value

  var lockImplementation: ...

  borrowing func withLock(...) {
    lockImplementation.lock()

    defer {
      lockImplementation.unlock()
    }

    // error: cannot pass immutable value as inout argument: 'self' is immutable
    try body(&value)
  }
}

Unlike a lot of interop scenarios where you simply have references or spans of shared memory that you need to access, these primitives, and similarly concurrent data structures, need to create storage that allows for interior mutability and shared memory access.

Proposed solution

We propose introducing three new types to the standard library to make working on these types of issues easier, or in some cases, possible now.

Cell

Cell is a thin generic wrapper over a type that grants access to a mutable pointer to itself under a borrowing method. This makes it possible to implement the Mutex example above like the following:

struct Mutex<Value: ~Copyable>: ~Copyable {
  var value: Cell<Value>

  var lockImplementation: ...

  borrowing func withLock(...) {
    lockImplementation.lock()

    defer {
      lockImplementation.unlock()
    }

    try value.withUnsafeMutablePointer {
      try body(&$0.pointee)
    }
  }
}

The layout of Cell is exactly the same as type it's wrapping and it introduces no overhead in terms of layout or performance.

Having this new Cell type, we can create compositions that solve the C-interop use cases of needing a reference or buffer type that has the lifetime guarantees, but doesn't have the exclusivity guarantees. Ref<Cell<T>> and Span<Cell<T>> allow you to use the de facto lifetime bound reference and buffer type in Swift while also disabling exclusivity guarantees on a per element basis.

struct Person {
  var age: Int
}

func foo(people: Span<Cell<Person>>) {
  people[0].value.age += 1
}

Notice how we're now able to mutate the underlying Person struct within a read-only Span instance with Cell.

Cell implements a value property for Copyable types using a get/set to prevent exclusivity issues by performing a copy out/copy in.

ConstCell

In addition to Cell, there's a ConstCell type that provides the same guarantees as Cell, but disallow writes from Swift. The memory may still be altered from outside of Swift.

func foo(people: Span<ConstCell<Person>>) {
  people[0].value.age += 1 // error: 'value' is get-only
}

Volatile

Finally, we propose Volatile which accesses an underlying value through volatile pointer loads and stores, similar to volatile in C and C++.

While related to the aliasing issues mentioned above, Volatile serves to solve other problems with accessing shared memory in Swift. In some areas, like memory mapped IO (MMIO), it is critical that the compiler not eliminate certain pointer reads and writes as well as reordering them. This memory may be shared with processes outside of the one currently executing or may be directly altered by the hardware itself.

let controlRegister = UnsafePointer<Volatile<Bool>>(bitPattern: 0x40000100)!
print(controlRegister.pointee.value) // true
print(controlRegister.pointee.value) // maybe true, maybe false

Notably, writing to a Volatile value requires exclusive access (because .value requires a mutating set) contrary to the Cell types proposed above. If you need to be able to write to one of these values, you can compose the types like Cell<Volatile<T>> to be able to write into the Volatile:

func write(to x: borrowing Cell<Volatile<Bool>>) -> Volatile<Bool> {
  // `Cell.value` requires `T` to be `Copyable`,
  // `Volatile` is `~Copyable`.
  x.replace(with: Volatile(false))
}

Detailed design

Cell

public struct Cell<Value: ~Copyable>: ~Copyable {
  /// Initializes a value of this cell with the given initial value.
  ///
  /// - Parameter initialValue: The initial value to initialize the cell with.
  public init(_ initialValue: consuming Value)

  /// Calls the given closure with a mutable pointer to the value inside the cell.
  public func withUnsafeMutablePointer<Result: ~Copyable, E>(
    _ body: (UnsafeMutablePointer<Value>) throws(E) -> Result
  ) throws(E) -> Result

  /// Replaces the value in the cell to the given new value and returns the old
  /// value.
  ///
  /// - Parameter newValue: The new value to place inside the cell.
  @discardableResult
  public func replace(with newValue: consuming Value) -> Value

  /// Sets the value in the cell to the given new value.
  ///
  /// - Parameter newValue: The new value to place inside the cell.
  public func set(_ newValue: consuming Value)
}

extension Cell where Value: ~Copyable {
  /// Unsafely access the value within the cell through a borrow.
  ///
  /// This is unsafe because while borrows are outstanding, mutating the value
  /// within the cell is formally disallowed under the Law of Exclusivity.
  @unsafe
  public var unsafeBorrow: Value {
    borrow
    nonmutating mutate
  }
}

extension Cell where Value: Copyable {
  /// Accesses the value within the cell through a copy.
  public var value: Value {
    get
    nonmutating set
  }
}

ConstCell

public struct ConstCell<Value: ~Copyable>: ~Copyable {
  /// Initializes a value of this cell with the given initial value.
  ///
  /// - Parameter initialValue: The initial value to initialize the cell with.
  public init(_ initialValue: consuming Value)

  /// Calls the given closure with a read-only pointer to the value inside the cell.
  public func withUnsafePointer<Result: ~Copyable, E>(
    _ body: (UnsafePointer<Value>) throws(E) -> Result
  ) throws(E) -> Result
}

extension ConstCell: Sendable where Value: Sendable & FullyInhabited {}

extension ConstCell where Value: ~Copyable {
  /// Unsafely access the value within the cell through a borrow.
  ///
  /// This is unsafe because while borrows are outstanding, mutating the value
  /// within the cell is formally disallowed under the Law of Exclusivity.
  @unsafe
  public var unsafeBorrow: Value {
    borrow
  }
}

extension ConstCell where Value: Copyable {
  /// Accesses the value within the cell through a copy.
  public var value: Value {
    get
  }
}

Volatile

public struct Volatile<Value: BitwiseCopyable>: ~Copyable {
  /// Initializes a volatile value with the given initial value.
  ///
  /// - Parameter initialValue: The initial value to initialize the volatile
  ///                           cell with.
  public init(_ initialValue: consuming Value)

  /// Performs a volatile load from memory and reads the value out.
  public func read() -> Value

  /// Performs a volatile write to memory 
  public func write(_ newValue: Value)

  /// Performs a volatile read from memory and calls the given closure allowing 
  public func modify<Result: ~Copyable>(_ body: (inout Value) -> Result) -> Result

  /// Calls the given closure with a mutable pointer to the value inside the
  /// volatile cell.
  public func withUnsafeMutablePointer<Result: ~Copyable, E>(
    _ body: (UnsafeMutablePointer<Value>) throws(E) -> Result
  ) throws(E) -> Result
}

Conversions

extension MutableRef where Value: ~Copyable {
  public consuming func cell() -> Ref<Cell<Value>>
}

extension Span where Element: ~Copyable {
  public func constCells() -> Span<ConstCell<Element>>

  @unsafe
  public func unsafeCells() -> Span<Cell<Element>>
}

extension MutableSpan where Element: ~Copyable {
  public consuming func cells() -> Span<Cell<Element>>
}

Going from MutableRef<T> -> Ref<Cell<T>> is safe, because from the Swift side you've enforced that you had exclusive access to the T before downgrading it to a shared access pointing to shared memory. The same argument can be applied to the MutableSpan<T> -> Span<Cell<T>> conversion.

The Span<T> -> Span<ConstCell<T>> is safe, because from Swift's perspective the underlying memory being pointed to is still read-only even though the memory may be shared to other parts of the process not written in Swift or other parts of the operating system that will mutate the memory.

Span<T> -> Span<Cell<T>> is a fundamentally unsafe operation. This conversion is taking a buffer of non-shared memory and then treating it like shared memory that is directly mutatable from Swift.

Source compatibility

This proposal introduces new types into the Swift standard library, but otherwise has no effect on source compatibility.

ABI compatibility

This proposal introduces new types into the Swift standard library, which adds to the ABI, but otherwise has no effect on ABI compatibility.

Implications on adoption

The new Cell family of types is intended to be adopted in places where it is impossible or impractical to ensure non-aliasing, such as the shared-memory, concurrent data structures, and C-interoperability use cases. It is possible that these types will propagate further in the Swift ecosystem than intended, for example because something that is implemented on top of a C library provides Span<Cell<T>>-based APIs rather than Span<T>-based APIs for convenience (or safety). If this happens, it could lead to confusion about which types should be used, and potentially undermine Swift performance if Cell<T> is used in places where it shouldn't be.

Future directions

C(++) interoperability

As noted in the strict memory safety vision, a span of cells could be used to represent C pointers if we know about the bounds and lifetime of those pointers. For example, in this C++ API:

class MyBuffer {
public:
  std::span<double> getContents() __attribute__((lifetimebound));
};

the std::span provides a pointer and its bounds, and the lifetimebound attribute specifies that the lifetime of the resulting pointer is bound to the *this instance. This is sufficient information to import that C++ method with the following signature in Swift:

func getContents() -> Span<Cell<Double>>

The actual C and C++ annotations, as well as their mapping into Swift, are out of the scope of this proposal.

Alternatives considered

Don't solve the aliasing problem

Shared memory is somewhat of a niche concern, and it's generally our policy that C interoperability not dictate the direction of the Swift language. We could choose for Swift not to solve the aliasing problem at all, and require any code interacting with shared memory or C pointers to be unsafe. Doing so would limit the places where Swift is applicable in systems programming, as well as undermining the ability to safely interoperate with the C family of languages as expressed in the strict memory safety vision.

Name this type Aliased<T>

To more closely align these types and this proposal with solving the exclusivity aliasing issue, we could name Cell<T> to Aliased<T> (or Alias<T>) to make it more understandable about what specific problem it is solving. However, we don't feel that this name represents the use case of creating shared memory storage like in the Mutex example:

struct Mutex<Value: ~Copyable>: ~Copyable {
  var value: Aliased<Value>
}

While it could work, it seems a bit odd and out of place.

The Cell name comes from the Rust terminology for this type.

AliasedSpan and AliasedRef

A previous iteration of this proposal introduced a family of AliasedSpan and AliasedRef types mimiking the behavior that Span<Cell<T>> accomplishes. We decided against that direction due to large API surface that it adds and introduces both implmentation complexity (keeping Span and AliasedSpan API surface the same) and developer complexity (when should I use Span vs. AliasedSpan?). These aliased span and ref types also wouldn't be sufficient enough to be able to implement Atomic or Mutex because those types need to own their storage, but the aliased span/ref approach wouldn't allow you to own such memory.

Put these types in a different module

Arguably, these types are for niche low-level and interop purposes, so stuffing them in the default Swift namespace isn't really ideal for autocomplete purposes. Cell is also a very general name that could be applied to many domains in software. We could put this in a different module, but we feel there isn't a proper module already in the toolchain best suited for these types and introducing a new module for just these is a bit overkill. While it would be ideal if Swift had proper namespaces or submodules, we don't currently have such features to land these types under.

There is a somewhat compelling argument for putting Volatile in Synchronization as it is a kind of synchronization primitive, but that leads to interesting questions on how the Clang importer should deal with volatile from C. If the Synchronization module is already imported by the user, it's fine, but interop would potentially have different behavior if it wasn't already imported. This could lead to some API differences or some APIs not being imported entirely.

Have Value conform to AtomicRepresentable for Volatile

Instead of only providing read, write, and modify on the basic integer types, we could constrain the generic argument to Volatile to AtomicRepresentable. The same hardware constraints for atomics also somewhat apply to Volatile (allow load splitting could be allowed for types that aren't AtomicRepresentable). However, we would either need to sink AtomicRepresentable in the Swift module or we would need to move Volatile into the Synchronization module. A downside of not using this constraint is that things like the following wouldn't compose with Volatile:

enum Color: UInt8, AtomicRepresentable {
  case red
  case green
  case blue
}

Volatile<Color>.read()

A possible future could be submodules where Synchronization becomes a submodule of the main Swift module and the sinking and moving of types no longer becomes a problem.

UnsafeCell

A concern with Cell is that the unsafeBorrow property causes all uses of value and replace to be considered unsafe as well and needs handling to ensure its safety. We could sever the unsafeBorrow property out of Cell into a new UnsafeCell to give Cell more safety. We could also potentially have a freestanding pointer/address property on UnsafeCell instead of the closure based API on Cell.

We feel this is a little unnecessary as UnsafeCell would mimic Cell in almost all aspects with the added API of unsafeBorrow. In Rust, Cell is considered completely safe, but is limited to single-threaded interior mutability use cases. If you wanted to build synchronization primitives or concurrent data structures like we mentioned, you would build on top of UnsafeCell instead.

However, the presence of unsafeBorrow shouldn't be enough to warrant an entirely new type. We have similar situations with withUnsafeBufferPointer on Span and any safe type providing pointers to its internal storage.

Acknowledgments

Geoff Garen identified the core aliasing problem addressed by the cell types presented here, and noted the memory safety issues it introduces for Swift. Gábor Horváth comprehensively explored what imposing the Law of Exclusivity would mean for the C family of languages, which directly informed this design and the safe interoperability with C that it enables. Steve Canon helped design the cell types and provided valuable feedback on this proposal.