This is a second pitch thread for this feature, after it was significantly reworked. The first pitch thread was [Pitch] Safe loading of integer values from `RawSpan` .
The most significant change is the introduction of a new layout constraint, FullyInhabited. The safe API are now constrained by this protocol, or by this protocol in combination with FixedWidthInteger.
Safe loading API for RawSpan
Introduction
We propose the introduction of a set of safe API to load values of certain safe types from the memory represented by RawSpan instances, as well as safe conversions from RawSpan to Span for the same types.
Motivation
In SE-0447, we introduced RawSpan along with some unsafe functions to load values of arbitrary types. While it is safe to load any of the native integer types with those functions, the unsafe annotation introduces an element of doubt for users of the standard library. This proposal aims to provide clarity for safe uses of byte-loading operations.
Proposed solution
FullyInhabited
We propose a new layout constraint, FullyInhabited, to refine BitwiseCopyable. A FullyInhabited type is a safe type with a valid value for every bit pattern that can fit in its representation.
By conforming to FullyInhabited, a type declares that it has the following characteristics:
- Its stored properties all themselves conform to
FullyInhabited. - It is frozen if its containing module is resilient.
- There are no semantic constraints on the values of its stored properties.
The standard library's FixedWidthInteger and BinaryFloatingPoint types will conform to FullyInhabited, as well as Never.
For example, a type representing two-dimensional Cartesian coordinates, such as struct Point { var x, y: Int } could conform to FullyInhabited. Its stored properties are Int, which is FullyInhabited. There are no semantic constraints between the x and y properties: any combination of Int values can represent a valid Point.
In contrast, Range<Int> could not conform to FullyInhabited, even though on the surface it has the same composition as Point. There is a semantic constraint between two two stored properties of Range: lowerBound must be less than or equal to upperBound. This makes it unable to conform to FullyInhabited.
Other examples of types that cannot conform to FullyInhabited are UnicodeScalar (some bit patterns are invalid), a hypothetical UTF8-encoded SmallString (the sequencing of the constituent bytes matters,) and UnsafeRawPointer (it is marked with @unsafe.)
In the initial release of FullyInhabited, the compiler will not validate conformances to it. Validation will be implemented in a later version of Swift.
RawSpan and MutableRawSpan
RawSpan and MutableRawSpan will have a new, generic load(as:) function that return FullyInhabited values read from the underlying memory, with no pointer-alignment restriction. Because the returned values are FullyInhabited and the request is bounds-checked, this load(as:) function is safe.
extension RawSpan {
func load<T: FullyInhabited>(
fromByteOffset: Int = 0,
as: T.Type = T.self
) -> T
}
Additionally, a special version of load() will have an additional argument to control the byte order of the value being loaded, for values of types conforming to both FullyInhabited and FixedWidthInteger:
extension RawSpan {
func load<T: FullyInhabited & FixedWidthInteger>(
fromByteOffset: Int = 0,
as: T.Type = T.self,
_ byteOrder: ByteOrder
) -> T
}
@frozen
public enum ByteOrder: Equatable, Hashable, Sendable {
case bigEndian, littleEndian
static var native: Self { get }
}
The list of standard library types to conform to FullyInhabited & FixedWidthInteger is UInt8, Int8, UInt16, Int16, UInt32, Int32, UInt64, Int64, UInt, Int, UInt128, and Int128.
The load() functions are not atomic operations.
The load(as:) functions will not have equivalents with unchecked byte offset. If that functionality is needed, the function unsafeLoad(fromUncheckedByteOffset:as:) is already available.
MutableRawSpan and OutputRawSpan
MutableRawSpan will gain a storeBytes() function that accept a byte order parameter:
extension MutableRawSpan {
mutating func storeBytes<T: FullyInhabited & FixedWidthInteger>(
of value: T,
toByteOffset offset: Int = 0,
as type: T.Type,
_ byteOrder: ByteOrder
)
}
OutputRawSpan will have a matching append() function:
extension OutputRawSpan {
mutating func append<T: FullyInhabited & FixedWidthInteger>(
_ value: T,
as type: T.Type,
_ byteOrder: ByteOrder
)
}
These functions do not need a default value for their byteOrder parameter, as the existing generic MutableSpan.storeBytes(of:toByteOffset:as:) and OutputRawSpan.append(_:as:) functions use the native byte order.
Span
Span will have a new initializer init(viewing: RawSpan) to allow viewing a range of untyped memory as a typed Span, when Span.Element FullyInhabited. These conversions will check for alignment and bounds.
extension Span where Element: FullyInhabited {
@_lifetime(borrow span)
init(viewing bytes: borrowing RawSpan) where Element == UInt32
}
The conversions from RawSpan to Span only support well-aligned views with the native byte order. The swift-binary-parsing package provides a more fully-featured ParserSpan type for use cases beyond reinterpreting memory in-place.
Please see the full proposal draft for the detailed design, alternatives and future directions.