Hi!
This is the first of a series of proposed improvements to the UnsafePointer
and UnsafeBufferPointer
families. I look forward to your feedback!
EDIT: an updated pitch document can be found here.
Expand usability of withMemoryRebound
- Proposal: SE-NNNN
- Authors:
Guillaume Lessard,
Andrew Trick - Review Manager: TBD
- Status: Pending
- Implementation: draft pull request
- Decision Notes: TBD
- Bugs: SR-11082, SR-11087
- Previous Revision: none
Introduction
The function withMemoryRebound(to:capacity:_ body:)
executes a closure while temporarily binding a range of memory to a different type than the callee is bound to.
We propose to lift some notable limitations of withMemoryRebound
and enable rebinding to a larger set of types,
as well as rebinding from raw memory pointers and buffers.
Swift-evolution thread: Discussion thread TK
Motivation
When using Swift in a systems programming context or using Swift with libraries written in C,
we occasionally need to temporarily access a range of memory as instances of a different type than has been declared
(the pointer's Pointee
type parameter).
In those cases, withMemoryRebound
is the tool to reach for,
allowing scoped access to the range of memory as another type.
As a reminder, the function is declared as follows on the type UnsafePointer<Pointee>
:
func withMemoryRebound<T, Result>(
to type: T.Type,
capacity count: Int,
_ body: (UnsafePointer<T>) throws -> Result
) rethrows -> Result
This function is currently more limited than necessary.
It requires that the stride of Pointee
and T
be equal.
This requirement makes many legitimate use cases technically illegal,
even though they could be supported by the compiler.
We propose to allow temporarily binding to a type T
whose stride is
a whole fraction or whole multiple of Pointee
's stride,
when the starting address is properly aligned for type T
.
As before, T
's memory layout must be compatible with that ofPointee
.
For example, imagine that a buffer of Double
consisting of a series of (x,y) pairs is returned from data analysis code written in C.
The next step might be to display it in a preview graph, which needs to read CGPoint
values.
We need to copy the Double
values as pairs to values of type CGPoint
:
var count = 0
let pointer: UnsafePointer<Double> = calculation(&count)
var points = Array<CGPoint>(unsafeUninitializedCapacity: count/2) {
buffer, initializedCount in
var p = pointer
for i in buffer.indices where p+1 < pointer+count {
buffer[i] = CGPoint(x: p[0], y: p[1])
p += 2
}
initializedCount = buffer.count
}
We could do better with an improved version of withMemoryRebound
.
Since CGPoint
values consist of a pair of CGFloat
values,
and CGFloat
values are themselves layout-compatible with Double
:
var points = Array<CGPoint>(unsafeUninitializedCapacity: data.count/2) {
buffer, initializedCount in
pointer.withMemoryRebound(to: CGPoint.self, capacity: buffer.count) {
(_, initializedCount) = buffer.initialize(from: UnsafeBufferPointer(start: $0, count: buffer.count))
}
}
Alternately, the data could have been received as bytes from a network request, wrapped in a Data
instance.
Previously we would have needed to do:
let data: Data = ...
var points = Array<CGPoint>(unsafeUninitializedCapacity: data.count/MemoryLayout<CGPoint>.stride) {
buffer, initializedCount in
data.withUnsafeBytes { data in
var read = 0
for i in buffer.indices where (read+2*MemoryLayout<CGFloat>.stride)<=data.count {
let x = data.load(fromByteOffset: read, as: CGFloat.self)
read += MemoryLayout<CGFloat>.stride
let y = data.load(fromByteOffset: read, as: CGFloat.self)
read += MemoryLayout<CGFloat>.stride
buffer[i] = CGPoint(x: x, y: y)
}
initializedCount = read / MemoryLayout<CGPoint>.stride
}
}
In this case having the ability to use withMemoryRebound
with UnsafeRawBuffer
improves readability in a similar manner as in the example above:
var points = Array<CGPoint>(unsafeUninitializedCapacity: data.count/MemoryLayout<CGPoint>.stride) {
buffer, initializedCount in
data.withUnsafeBytes {
$0.withMemoryRebound(to: CGPoint.self) {
(_, initializedCount) = buffer.initialize(from: $0)
}
}
}
Proposed solution
We propose to lift the restriction that the strides of T
and Pointee
must be equal.
This means that it will now be considered correct to re-bind from a homogeneous aggregate type to the type of its constitutive elements,
as they are layout compatible, even though their stride is different.
Instance methods of UnsafePointer<Pointee>
and UnsafeMutablePointer<Pointee>
We propose to lift the restriction that the strides of T
and Pointee
must be equal, when calling withMemoryRebound
.
The function declarations remain the same on these two types,
though given the relaxed restriction,
we must clarify the meaning of the capacity
argument.
capacity
shall mean the number of strides of elements of the temporary type (T
) to be temporarily bound.
The documentation will be updated to reflect the changed behaviour.
We will also add parameter labels to the closure type declaration to benefit code completion (a source compatible change.)
extension UnsafePointer {
public func withMemoryRebound<T, Result>(
to type: T.Type,
capacity count: Int,
_ body: (_ pointer: UnsafePointer<T>) throws -> Result
) rethrows -> Result
}
extension UnsafeMutablePointer {
public func withMemoryRebound<T, Result>(
to type: T.Type,
capacity count: Int,
_ body: (_ pointer: UnsafeMutablePointer<T>) throws -> Result
) rethrows -> Result
}
Instance methods of UnsafeRawPointer
and UnsafeMutableRawPointer
We propose adding a withMemoryRebound
method, which currently does not exist on these types.
Since it operates on raw memory, this version of withMemoryRebound
places no restriction on the temporary type (T
).
It is therefore up to the program author to ensure type safety when using these methods.
As in the UnsafePointer
case, capacity
means the number of strides of elements of the temporary type (T
) to be temporarily bound.
extension UnsafeRawPointer {
public func withMemoryRebound<T, Result>(
to type: T.Type,
capacity count: Int,
_ body: (_ pointer: UnsafePointer<T>) throws -> Result
) rethrows -> Result
}
extension UnsafeMutableRawPointer {
public func withMemoryRebound<T, Result>(
to type: T.Type,
capacity count: Int,
_ body: (_ pointer: UnsafeMutablePointer<T>) throws -> Result
) rethrows -> Result
}
Instance methods of UnsafeBufferPointer
and UnsafeMutableBufferPointer
We propose to lift the restriction that the strides of T
and Pointee
must be equal, when calling withMemoryRebound
.
The function declarations remain the same on these two types.
The capacity of the buffer to the temporary type will be calculated using the length of the UnsafeBufferPointer<Element>
and the stride of the temporary type.
The documentation will be updated to reflect the changed behaviour.
We will add parameter labels to the closure type declaration to benefit code completion (a source compatible change.)
extension UnsafeBufferPointer {
public func withMemoryRebound<T, Result>(
to type: T.Type,
_ body: (_ buffer: UnsafeBufferPointer<T>) throws -> Result
) rethrows -> Result
}
extension UnsafeMutableBufferPointer {
public func withMemoryRebound<T, Result>(
to type: T.Type,
_ body: (_ buffer: UnsafeMutableBufferPointer<T>) throws -> Result
) rethrows -> Result
}
Instance methods of UnsafeRawBufferPointer
and UnsafeMutableRawBufferPointer
We propose adding a withMemoryRebound
method, which currently does not exist on these types.
Since it operates on raw memory, this version of withMemoryRebound
places no restriction on the temporary type (T
).
It is therefore up to the program author to ensure type safety when using these methods.
The capacity of the buffer to the temporary type will be calculated using the length of the UnsafeRawBufferPointer
and the stride of the temporary type.
Finally the set, we propose to add an assumingMemoryBound
function that calculates the capacity of the returned UnsafeBufferPointer
.
extension UnsafeRawBufferPointer {
public func withMemoryRebound<T, Result>(
to type: T.Type,
_ body: (_ buffer: UnsafeBufferPointer<T>) throws -> Result
) rethrows -> Result
public func assumingMemoryBound<T>(to type: T.Type) -> UnsafeBufferPointer<T>
}
extension UnsafeMutableRawBufferPointer {
public func withMemoryRebound<T, Result>(
to type: T.Type,
_ body: (_ buffer: UnsafeMutableBufferPointer<T>) throws -> Result
) rethrows -> Result
public func assumingMemoryBound<T>(to type: T.Type) -> UnsafeMutableBufferPointer<T>
}
Detailed design
Note: please see the draft PR or the full proposal for details.
Source compatibility
This proposal is source-compatible.
Some changes are compatible with existing correct uses of the API,
while others are additive.
Effect on ABI stability
This proposal consists of ABI-preserving changes and ABI-additive changes.
Effect on API resilience
The behaviour change for the withMemoryRebound
is compatible with previous uses,
since restrictions were lifted.
Code that depends on the new semantics may not be compatible with old versions of these functions.
Back-deployment of new binaries will be supported by making the updated versions @_alwaysEmitIntoClient
.
Compatibility of old binaries with a new standard library will be supported by ensuring that a compatible entry point remains.
Alternatives considered
One alternative is to implement none of this change, and leave withMemoryRebound
as is.
The usability problems of withMemoryRebound
would remain.
Another alternative is to leave the type layout restrictions as they are for the typed Pointer
and BufferPointer
types,
but add the withMemoryRebound
functions to the RawPointer
and RawBufferPointer
variants.
In that case, the stride restriction would be no more than a speedbump,
because it would be straightforward to bypass it by transiting through the appropriate Raw
variant.