Hello, Swift community!
I'd like to pitch a few small additions to the standard library that you'd be interested in. One notable part is that these proposed withTemporaryAllocation functions can be back-deployed, which means you'd get safe temporary spans (potentially optimized to stack allocations) on versions of Darwin that don't have InlineArray available.
A partial draft implementation is available at swiftlang/swift#85866.
Introduction
This proposal introduces new top-level functions that provide a temporary buffer wrapped in an OutputSpan or OutputRawSpan. This enables safe initialization of temporary memory, leveraging the safety guarantees of these span types while utilizing the stack-allocation optimization of withUnsafeTemporaryAllocation.
Motivation
SE-0322 and SE-0437 introduced and refined withUnsafeTemporaryAllocation, a facility for allocating temporary storage that may be stack-allocated. This function yields an UnsafeMutableBufferPointer or UnsafeMutableRawBufferPointer, requiring the user to manually manage initialization and deinitialization of the elements. This is error-prone, as the user must ensure that all initialized elements are correctly deinitialized before the closure returns, even in the presence of errors.
SE-0485 introduced OutputSpan and OutputRawSpan, types that manage the initialization state of a contiguous region of memory. These types track the number of initialized elements and ensure that memory operations maintain initialization invariants.
By combining these two facilities, we can provide a high-level, safe API for temporary allocations. Users can use the append methods on the span types to initialize the temporary memory without dealing with raw pointers or manually tracking the initialized count for deinitialization.
Proposed solution
We propose adding new global functions that wrap withUnsafeTemporaryAllocation. Instead of yielding a raw buffer pointer, they yield an inout OutputSpan for typed allocations, and an inout OutputRawSpan for raw byte allocations.
Typed Allocation
try withTemporaryAllocation(of: Int.self, capacity: 10) { span in
span.append(1)
span.append(2)
// `OutputSpan` passed to this closure deinitializes and deallocates
// elements upon exit
}
Raw Bytes Allocation
try withTemporaryAllocation(byteCount: 16, alignment: 4) { rawSpan in
rawSpan.append(UInt32(0xF00), as: UInt32.self)
// `OutputRawSpan` deallocates raw bytes upon exit
}
These functions handle the creation of the span types and ensure that any initialized elements are correctly deallocated (and deinitialized in the case of OutputSpan) when the scope exits.
Detailed design
The proposal adds two functions:
Typed Allocation with OutputSpan
This function is for working with temporary allocations of a specific, homogenous type.
@available(SwiftCompatibilitySpan 5.0, *)
@_alwaysEmitIntoClient @_transparent
public func withTemporaryAllocation<T: ~Copyable, R: ~Copyable, E: Error>(
of type: T.Type,
capacity: Int,
_ body: (inout OutputSpan<T>) throws(E) -> R
) throws(E) -> R where T : ~Copyable, R : ~Copyable {
try withUnsafeTemporaryAllocation(of: type, capacity: capacity) { (buffer) throws(E) in
var span = OutputSpan(buffer: buffer, initializedCount: 0)
defer {
let initializedCount = span.finalize(for: buffer)
span = OutputSpan()
buffer.extracting(..<initializedCount).deinitialize()
}
return try body(&span)
}
}
Here's the implementation walkthrough:
-
Allocation: It calls
withUnsafeTemporaryAllocation(of:capacity:)to obtain a typed buffer of uninitialized memory. -
Span Creation: It creates an
OutputSpancovering the buffer, with aninitializedCountof 0. -
Execution: It yields the
OutputSpanto the user's closure as aninoutparameter. -
Cleanup: A
deferblock ensures that upon exit,finalize(for:)is called on the span to get the count of initialized elements, and then those elements are deinitialized viadeinitialize().
Raw Byte Allocation with OutputRawSpan
This function is for working with temporary raw byte buffers.
@available(SwiftCompatibilitySpan 5.0, *)
@_alwaysEmitIntoClient @_transparent
public func withTemporaryAllocation<R: ~Copyable, E: Error>(
byteCount: Int,
alignment: Int,
_ body: (inout OutputRawSpan) throws(E) -> R
) throws(E) -> R where R: ~Copyable {
try withUnsafeTemporaryAllocation(byteCount: byteCount, alignment: alignment) { (buffer) throws(E) in
var span = OutputRawSpan(buffer: buffer, initializedCount: 0)
defer {
_ = span.finalize(for: buffer)
span = OutputRawSpan()
}
return try body(&span)
}
}
The flow slightly differs from the OutputSpan version in the cleanup step 4, here's the full walkthrough for completeness:
-
Allocation: It calls
withUnsafeTemporaryAllocation(byteCount:alignment:)to obtain a raw byte buffer. -
Span Creation: It creates an
OutputRawSpancovering the buffer, with aninitializedCountof 0. -
Execution: It yields the
OutputRawSpanto the user's closure as aninoutparameter. -
Cleanup: A
deferblock ensuresfinalize(for:)is called to consume the span. SinceOutputRawSpandeals with raw bytes (presumed to beBitwiseCopyable), no explicit deinitialization call is needed on the buffer itself. The temporary memory is automatically deallocated.
Source compatibility
This is an additive change and does not affect existing code.
ABI compatibility
The functions are marked @_alwaysEmitIntoClient and @_transparent. They will be emitted directly into the client's binary and do not constitute new ABI entry points in the standard library. They rely on existing ABI entry points.
Implications on adoption
These functions make temporary allocations significantly safer and easier to use. They lower the barrier to entry for using stack-allocated temporary memory, as users no longer need to be comfortable with "unsafe" pointer APIs.
Future directions
This proposal covers the primary safe wrappers for temporary allocation. Future work could consider specialized versions, like async overloads.
Alternatives considered
-
Do nothing: Users would continue to use the
withUnsafe...variants and manually wrap them inOutputSpanorOutputRawSpan, replicating the boilerplate code proposed here. -
Member of
OutputSpan/OutputRawSpan: We could make these static methods on their respective span types. However, top-level functions better match the existingwithUnsafeTemporaryAllocationandwithExtendedLifetimepatterns.