We are proposing a new abstraction for safe, direct access ot memory: Span
. This is a substantial update to our earlier pitch, "Safe Access to Contiguous Storage", and is a companion to the Non-Escapable Types proposal.
Some of the changes in this version are:
-
The type's name is changed to
Span
. As a name,Span
is short, pithy, precedented, and composable. -
We add an untyped counterpart to
Span
:RawSpan
, along with a motivating parser example and parsing utilities forRawSpan
. -
The Indexing model is simplified to use integer offsets.
-
Most of the
Collection
-like API is removed, and we use the slicing alternative adopted forUnsafeBufferPointer
in SE-0437 Noncopyable Standard Library Primitives. This leaves more room to get forthcomingSequence
-like andCollection
-like improvements right.
We have added a prototype of Span
to a branch of the swift-collections package. This branch requires a recent Swift development toolchain.
Full proposal: Safe Access to Contiguous Storage (#2307)
Safe Access to Contiguous Storage
- Proposal: SE-NNNN
- Authors: Guillaume Lessard, Andrew Trick, Michael Ilseman
- Review Manager: TBD
- Roadmap: BufferView Roadmap
- Bug: rdar://48132971, rdar://96837923
- Implementation: Prototyped in GitHub - apple/swift-collections: Commonly used data structures for Swift (on branch "future")
- Review: (pitch 1)
Introduction
We introduce Span<T>
, an abstraction for container-agnostic access to contiguous memory. It will expand the expressivity of performant Swift code without comprimising on the memory safety properties we rely on: temporal safety, spatial safety, definite initialization and type safety.
In the C family of programming languages, memory can be shared with any function by using a pointer and (ideally) a length. This allows contiguous memory to be shared with a function that doesn't know the layout of a container being used by the caller. A heap-allocated array, contiguously-stored named fields or even a single stack-allocated instance can all be accessed through a C pointer. We aim to create a similar idiom in Swift, without compromising Swift's memory safety.
This proposal is related to two other features being proposed along with it: Nonescapable types (~Escapable
) and Compile-time Lifetime Dependency Annotations, as well as related to the BufferView roadmap forum thread. This proposal is also related to the following proposals:
- SE-0426 BitwiseCopyable
- SE-0427 Noncopyable generics
- SE-0377
borrowing
andconsuming
parameter ownership modifiers - SE-0256
{Mutable}ContiguousCollection
protocol (rejected, superseded by this proposal)
Motivation
Swift needs safe and performant types for local processing over values in contiguous memory. Consider for example a program using multiple libraries, including one for base64 decoding. The program would obtain encoded data from one or more of its dependencies, which could supply the data in the form of [UInt8]
, Foundation.Data
or even String
, among others. None of these types is necessarily more correct than another, but the base64 decoding library must pick an input format. It could declare its input parameter type to be some Sequence<UInt8>
, but such a generic function significantly limits performance. This may force the library author to either declare its entry point as inlinable, or to implement an internal fast path using withContiguousStorageIfAvailable()
, forcing them to use an unsafe type. The ideal interface would have a combination of the properties of both some Sequence<UInt8>
and UnsafeBufferPointer<UInt8>
.
The UnsafeBufferPointer
passed to a withUnsafeXXX
closure-style API, while performant, is unsafe in multiple ways:
- The pointer itself is unsafe and unmanaged
subscript
is only bounds-checked in debug builds of client code- It might escape the duration of the closure
Even if the body of the withUnsafeXXX
call does not escape the pointer, other functions called inside the closure have to be written in terms of unsafe pointers. This requires programmer vigilance across a project and potentially spreads the use of unsafe types, even when it could have been written in terms of safe constructs.
We want to take advantage of the features of non-escapable types to replace some closure-taking API with simple properties, resulting in more composable code:
let array = Array("Hello\0".utf8)
// Old
array.withUnsafeBufferPointer {
// use `$0` here for direct memory access
}
// New
let span: Span<UInt8> = array.storage
// use `span` in the same scope as `array` for direct memory access
Proposed solution
Span
will allow sharing the contiguous internal representation of a type, by providing access to a borrowed view of an interval of contiguous memory. A view does not copy the underlying data: it instead relies on a guarantee that the original container cannot be modified or destroyed during the lifetime of the view. Span
's lifetime is statically enforced as a lifetime dependency to a binding of the type vending it, preventing its escape from the scope where it is valid for use. This guarantee preserves temporal safety. Span
also performs bounds-checking on every access to preserve spatial safety. Additionally Span
always represents initialized memory, preserving the definite initialization guarantee.
By relying on borrowing, Span
can provide simultaneous access to a non-copyable container, and can help avoid unwanted copies of copyable containers. Note that Span
is not a replacement for a copyable container with owned storage; see the future directions for more details (Resizable, contiguously-stored, untyped collection in the standard library)
Span
is the currency type for local processing over values in contiguous memory. It is the replacement for any API currently using Array
, UnsafeBufferPointer
, Foundation.Data
, etc., that does not need to escape the value.
RawSpan
RawSpan
allows sharing the contiguous internal representation for values which may be heterogenously-typed, such as in decoders. Since it is a fully concrete type, it can achieve better performance in debug builds of client code as well as a more straight-forwards understanding of performance for library code.
Span<some BitwiseCopyable>
can always be converted to RawSpan
, using a conditionally-available property or a constructor.
Please see the full proposal for the Detailed Design, Alternatives Considered and Future Directions sections.