Skip to content

Commit 27a37d6

Browse files
Data API proposed in SE-0485 (OutputSpan) (swiftlang#1758)
* Add the OutputSpan-taking initializers * improve the output-span taking initializer for inline * append-with-outputrawspan, for InlineData * append with typed OutputSpan * complete the OutputRawSpan-based initializer Co-authored-by: Jeremy Schonfeld <jschonfeld@apple.com> * the OutputRawSpan-based appender * remove stray availability annotations * Apply suggestions from code review * fix some indentation * Apply suggestions from code review * implement initializer in `_Representation` instead * add inout param to `withUninitializedBytes` (and simplify internal names) * slightly refactor `InlineData.append(_:_:)` * add doc-comments * update copyright headers * tests for `Data(rawCapacity:initializingWith:)` * tests for `Data(addingRawCapacity:initializingWith:)` * remove redundant uniqueness checks `reserveCapacity()` happens to do the uniqueness check. * fix a mistake * use a less confusing variable name * Apply suggestions from code review Co-authored-by: Jeremy Schonfeld <jeremyschonfeld@gmail.com> * fix large slice capacity reservation * make a design decision clearer. * tweak test to avoid appending 0 * improve new tests * fix appending to sliced data instances * add availability annotations * add one more availability annotation * remove a stray print statement --------- Co-authored-by: Jeremy Schonfeld <jschonfeld@apple.com> Co-authored-by: Jeremy Schonfeld <jeremyschonfeld@gmail.com>
1 parent 96d7858 commit 27a37d6

7 files changed

Lines changed: 545 additions & 7 deletions

File tree

Sources/FoundationEssentials/Data/Data.swift

Lines changed: 131 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// This source file is part of the Swift.org open source project
44
//
5-
// Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
5+
// Copyright (c) 2014 - 2026 Apple Inc. and the Swift project authors
66
// Licensed under Apache License v2.0 with Runtime Library Exception
77
//
88
// See https://swift.org/LICENSE.txt for license information
@@ -239,6 +239,68 @@ public struct Data : RandomAccessCollection, MutableCollection, RangeReplaceable
239239
_representation = .empty
240240
}
241241

242+
/// Creates a data instance with the specified capacity, and then calls the given
243+
/// closure with an output span covering the instance's uninitialized memory.
244+
///
245+
/// Inside the closure, initialize elements by appending to the `OutputRawSpan`.
246+
/// The `OutputRawSpan` keeps track of the initialized memory, ensuring
247+
/// safety. Its `count` at the end of the closure will become the `count` of
248+
/// the newly-initialized instance of `Data`.
249+
///
250+
/// - Note: While the resulting `Data` may have a capacity larger than the
251+
/// requested amount, the `OutputRawSpan` passed to the closure will cover
252+
/// exactly the number of bytes requested.
253+
///
254+
/// - Parameters:
255+
/// - capacity: The number of bytes to allocate space for in the new `Data`.
256+
/// - initializer: A closure to initialize the allocated memory.
257+
/// - Parameters:
258+
/// - span: An `OutputRawSpan` covering uninitialized memory with
259+
/// space for the specified number of bytes.
260+
@available(macOS 10.14.4, iOS 12.2, watchOS 5.2, tvOS 12.2, *)
261+
@_alwaysEmitIntoClient
262+
public init<E: Error>(
263+
rawCapacity capacity: Int,
264+
initializingWith initializer: (_ span: inout OutputRawSpan) throws(E) -> Void
265+
) throws(E) {
266+
precondition(capacity >= 0, "capacity must not be negative")
267+
_representation = try _Representation(capacity: capacity, initializer)
268+
}
269+
270+
/// Creates a data instance with the specified capacity, and then calls the given
271+
/// closure with an output span covering the instance's uninitialized memory.
272+
///
273+
/// Inside the closure, initialize elements by appending to the `OutputSpan`.
274+
/// The `OutputSpan` keeps track of the initialized memory, ensuring
275+
/// safety. Its `count` at the end of the closure will become the `count` of
276+
/// the newly-initialized instance of `Data`.
277+
///
278+
/// - Note: While the resulting `Data` may have a capacity larger than the
279+
/// requested amount, the `OutputSpan` passed to the closure will cover
280+
/// exactly the number of bytes requested.
281+
///
282+
/// - Parameters:
283+
/// - capacity: The number of bytes to allocate space for in the new `Data`.
284+
/// - initializer: A closure to initialize the allocated memory.
285+
/// - Parameters:
286+
/// - span: An `OutputSpan` covering uninitialized memory with
287+
/// space for the specified number of elements.
288+
@available(macOS 10.14.4, iOS 12.2, watchOS 5.2, tvOS 12.2, *)
289+
@_alwaysEmitIntoClient
290+
public init<E: Error>(
291+
capacity: Int,
292+
initializingWith initializer: (_ span: inout OutputSpan<UInt8>) throws(E) -> Void
293+
) throws(E) {
294+
self = try Data(rawCapacity: capacity) { output throws(E) in
295+
try output.withUnsafeMutableBytes { (bytes, count) throws(E) in
296+
try bytes.withMemoryRebound(to: UInt8.self) { buffer throws(E) in
297+
var span = OutputSpan<UInt8>(buffer: buffer, initializedCount: 0)
298+
try initializer(&span)
299+
count = span.finalize(for: buffer)
300+
}
301+
}
302+
}
303+
}
242304

243305
/// Initialize a `Data` without copying the bytes.
244306
///
@@ -536,6 +598,74 @@ public struct Data : RandomAccessCollection, MutableCollection, RangeReplaceable
536598
}
537599
}
538600

601+
/// Grows this data to have enough capacity for the specified number of
602+
/// bytes, then calls the closure with an output span covering the requested
603+
/// amount of uninitialized memory.
604+
///
605+
/// Inside the closure, initialize elements by appending to `span`. It
606+
/// ensures safety by keeping track of the initialized memory.
607+
/// At the end of the closure, `span`'s `count` elements will have
608+
/// been appended to this `Data` instance.
609+
///
610+
/// If the closure throws an error, the items appended until that point
611+
/// will remain in the `Data` instance.
612+
///
613+
/// - Parameters:
614+
/// - uninitializedCount: The number of new elements the `Data` should have
615+
/// space for.
616+
/// - initializer: A closure to initialize memory.
617+
/// - Parameters:
618+
/// - span: An `OutputRawSpan` covering uninitialized memory with
619+
/// space for the specified number of additional bytes.
620+
@available(macOS 10.14.4, iOS 12.2, watchOS 5.2, tvOS 12.2, *)
621+
@_alwaysEmitIntoClient
622+
public mutating func append<E: Error>(
623+
addingRawCapacity uninitializedCount: Int,
624+
initializingWith initializer: (_ span: inout OutputRawSpan) throws(E) -> Void
625+
) throws(E) {
626+
precondition(uninitializedCount >= 0, "uninitializedCount must not be negative")
627+
try _representation.append(addingCapacity: uninitializedCount, initializer)
628+
}
629+
630+
/// Grows this data to have enough capacity for the specified number of
631+
/// bytes, then calls the closure with an output span covering the requested
632+
/// amount of uninitialized memory.
633+
///
634+
/// Inside the closure, initialize elements by appending to `span`. It
635+
/// ensures safety by keeping track of the initialized memory.
636+
/// At the end of the closure, `span`'s `count` elements will have
637+
/// been appended to this `Data` instance.
638+
///
639+
/// If the closure throws an error, the items appended until that point
640+
/// will remain in the `Data` instance.
641+
///
642+
/// - Parameters:
643+
/// - uninitializedCount: The number of new elements the array should have
644+
/// space for.
645+
/// - initializer: A closure to initialize memory.
646+
/// - Parameters:
647+
/// - span: An `OutputSpan` covering uninitialized memory with
648+
/// space for the specified number of additional elements.
649+
@available(macOS 10.14.4, iOS 12.2, watchOS 5.2, tvOS 12.2, *)
650+
@_alwaysEmitIntoClient
651+
public mutating func append<E: Error>(
652+
addingCapacity uninitializedCount: Int,
653+
initializingWith initializer: (_ span: inout OutputSpan<UInt8>) throws(E) -> Void
654+
) throws(E) {
655+
try self.append(addingRawCapacity: uninitializedCount) { output throws(E) in
656+
try output.withUnsafeMutableBytes { (bytes, count) throws(E) in
657+
try bytes.withMemoryRebound(to: UInt8.self) { buffer throws(E) in
658+
var span = OutputSpan<UInt8>(buffer: buffer, initializedCount: 0)
659+
defer {
660+
count = span.finalize(for: buffer)
661+
span = OutputSpan()
662+
}
663+
try initializer(&span)
664+
}
665+
}
666+
}
667+
}
668+
539669
/// Append a buffer of bytes to the data.
540670
///
541671
/// - parameter buffer: The buffer of bytes to append. The size is calculated from `SourceType` and `buffer.count`.

Sources/FoundationEssentials/Data/Representations/Data+Inline.swift

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// This source file is part of the Swift.org open source project
44
//
5-
// Copyright (c) 2025 Apple Inc. and the Swift project authors
5+
// Copyright (c) 2025 - 2026 Apple Inc. and the Swift project authors
66
// Licensed under Apache License v2.0 with Runtime Library Exception
77
//
88
// See https://swift.org/LICENSE.txt for license information
@@ -76,6 +76,29 @@ extension Data {
7676
length = UInt8(count)
7777
}
7878

79+
@available(macOS 10.14.4, iOS 12.2, watchOS 5.2, tvOS 12.2, *)
80+
@_alwaysEmitIntoClient @inline(__always)
81+
init<E: Error>(
82+
rawCapacity: Int,
83+
initializingWith initializer: (inout OutputRawSpan) throws(E) -> Void
84+
) throws(E) {
85+
self.init()
86+
do throws(E) {
87+
let count = try Swift.withUnsafeMutableBytes(of: &bytes) {
88+
buffer throws(E) in
89+
let prefix = buffer.prefix(rawCapacity)
90+
var output = OutputRawSpan(buffer: prefix, initializedCount: 0)
91+
try initializer(&output)
92+
return output.finalize(for: prefix)
93+
}
94+
assert(count <= rawCapacity)
95+
length = UInt8(count)
96+
} catch {
97+
self = .init()
98+
throw error
99+
}
100+
}
101+
79102
@inlinable @inline(__always) // This is @inlinable as a convenience initializer.
80103
init(_ slice: InlineSlice, count: Int) {
81104
self.init(count: count)
@@ -162,6 +185,28 @@ extension Data {
162185
length += UInt8(buffer.count)
163186
}
164187

188+
@available(macOS 10.14.4, iOS 12.2, watchOS 5.2, tvOS 12.2, *)
189+
@_alwaysEmitIntoClient
190+
mutating func append<E: Error>(
191+
_ extraCapacity: Int, _ initializer: (inout OutputRawSpan) throws(E) -> Void
192+
) throws(E) {
193+
let oldCount = self.count
194+
let newCapacity = oldCount + extraCapacity
195+
assert(newCapacity <= capacity)
196+
try Swift.withUnsafeMutableBytes(of: &bytes) {
197+
buffer throws(E) in
198+
let slice = buffer[oldCount..<newCapacity]
199+
var span = OutputRawSpan(buffer: slice, initializedCount: 0)
200+
defer {
201+
let addedCount = unsafe span.finalize(for: slice)
202+
length = UInt8(truncatingIfNeeded: oldCount + addedCount)
203+
assert(addedCount <= extraCapacity)
204+
span = OutputRawSpan()
205+
}
206+
try initializer(&span)
207+
}
208+
}
209+
165210
@inlinable // This is @inlinable as trivially computable.
166211
subscript(index: Index) -> UInt8 {
167212
get {

Sources/FoundationEssentials/Data/Representations/Data+InlineSlice.swift

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// This source file is part of the Swift.org open source project
44
//
5-
// Copyright (c) 2025 Apple Inc. and the Swift project authors
5+
// Copyright (c) 2025 - 2026 Apple Inc. and the Swift project authors
66
// Licensed under Apache License v2.0 with Runtime Library Exception
77
//
88
// See https://swift.org/LICENSE.txt for license information
@@ -181,6 +181,20 @@ extension Data {
181181
slice = slice.lowerBound..<HalfInt(Int(slice.upperBound) + buffer.count)
182182
}
183183

184+
@available(macOS 10.14.4, iOS 12.2, watchOS 5.2, tvOS 12.2, *)
185+
@_alwaysEmitIntoClient
186+
mutating func append<E: Error>(
187+
_ extraCapacity: Int, _ initializer: (inout OutputRawSpan) throws(E) -> Void
188+
) throws(E) {
189+
assert(count + extraCapacity < HalfInt.max)
190+
reserveCapacity(count + extraCapacity)
191+
var appendedCount = 0
192+
defer {
193+
slice = slice.lowerBound..<(slice.upperBound + HalfInt(appendedCount))
194+
}
195+
try storage.withUninitializedBytes(extraCapacity: extraCapacity, location: endIndex, &appendedCount, initializer)
196+
}
197+
184198
@inlinable // This is @inlinable as reasonably small.
185199
subscript(index: Index) -> UInt8 {
186200
get {

Sources/FoundationEssentials/Data/Representations/Data+LargeSlice.swift

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// This source file is part of the Swift.org open source project
44
//
5-
// Copyright (c) 2025 Apple Inc. and the Swift project authors
5+
// Copyright (c) 2025 - 2026 Apple Inc. and the Swift project authors
66
// Licensed under Apache License v2.0 with Runtime Library Exception
77
//
88
// See https://swift.org/LICENSE.txt for license information
@@ -169,6 +169,19 @@ extension Data {
169169
slice.range = slice.range.lowerBound..<slice.range.upperBound + buffer.count
170170
}
171171

172+
@available(macOS 10.14.4, iOS 12.2, watchOS 5.2, tvOS 12.2, *)
173+
@_alwaysEmitIntoClient
174+
mutating func append<E: Error>(
175+
_ extraCapacity: Int, _ initializer: (inout OutputRawSpan) throws(E) -> Void
176+
) throws(E) {
177+
reserveCapacity(count + extraCapacity)
178+
var appendedCount = 0
179+
defer {
180+
slice.range = slice.range.lowerBound..<(slice.range.upperBound + appendedCount)
181+
}
182+
try storage.withUninitializedBytes(extraCapacity: extraCapacity, location: endIndex, &appendedCount, initializer)
183+
}
184+
172185
@inlinable // This is @inlinable as trivially computable.
173186
subscript(index: Index) -> UInt8 {
174187
get {

Sources/FoundationEssentials/Data/Representations/Data+Representation.swift

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// This source file is part of the Swift.org open source project
44
//
5-
// Copyright (c) 2025 Apple Inc. and the Swift project authors
5+
// Copyright (c) 2025 - 2026 Apple Inc. and the Swift project authors
66
// Licensed under Apache License v2.0 with Runtime Library Exception
77
//
88
// See https://swift.org/LICENSE.txt for license information
@@ -93,6 +93,26 @@ extension Data {
9393
}
9494
}
9595

96+
@available(macOS 10.14.4, iOS 12.2, watchOS 5.2, tvOS 12.2, *)
97+
@_alwaysEmitIntoClient
98+
init<E: Error>(
99+
capacity: Int, _ initializer: (inout OutputRawSpan) throws(E) -> Void
100+
) throws(E) {
101+
if InlineData.canStore(count: capacity) {
102+
let inline = try InlineData(rawCapacity: capacity, initializingWith: initializer)
103+
self = (inline.count == 0) ? .empty : .inline(inline)
104+
} else {
105+
let storage = __DataStorage(capacity: capacity)
106+
var appendedCount = 0
107+
try storage.withUninitializedBytes(extraCapacity: capacity, location: 0, &appendedCount, initializer)
108+
if InlineSlice.canStore(count: appendedCount) {
109+
self = .slice(InlineSlice(storage, count: appendedCount))
110+
} else {
111+
self = .large(LargeSlice(storage, count: appendedCount))
112+
}
113+
}
114+
}
115+
96116
@usableFromInline // This is not @inlinable as it is a non-trivial, non-generic function.
97117
mutating func reserveCapacity(_ minimumCapacity: Int) {
98118
guard minimumCapacity > 0 else { return }
@@ -302,6 +322,60 @@ extension Data {
302322
}
303323
}
304324

325+
@available(macOS 10.14.4, iOS 12.2, watchOS 5.2, tvOS 12.2, *)
326+
@_alwaysEmitIntoClient
327+
mutating func append<E: Error>(
328+
addingCapacity uninitializedCount: Int,
329+
_ initializer: (inout OutputRawSpan) throws(E) -> Void
330+
) throws(E) {
331+
let newCapacity = count + uninitializedCount
332+
333+
func appendInline(_ inline: consuming InlineData) throws(E) {
334+
if InlineData.canStore(count: newCapacity) {
335+
defer {
336+
self = (inline.count == 0) ? .empty : .inline(inline)
337+
}
338+
try inline.append(uninitializedCount, initializer)
339+
} else {
340+
let storage = __DataStorage(capacity: newCapacity)
341+
inline.withUnsafeBytes { storage.append($0.baseAddress!, length: $0.count) }
342+
var appendedCount = 0
343+
defer {
344+
assert(inline.count+appendedCount == storage.length)
345+
let newCount = storage.length
346+
if InlineSlice.canStore(count: newCount) {
347+
self = .slice(InlineSlice(storage, count: newCount))
348+
} else {
349+
self = .large(LargeSlice(storage, count: newCount))
350+
}
351+
}
352+
try storage.withUninitializedBytes(extraCapacity: uninitializedCount, location: inline.count, &appendedCount, initializer)
353+
}
354+
}
355+
356+
switch self {
357+
case .empty:
358+
try appendInline(InlineData())
359+
case .inline(let inline):
360+
try appendInline(inline)
361+
case .slice(var slice):
362+
if InlineSlice.canStore(count: newCapacity) {
363+
self = .empty
364+
defer { self = .slice(slice) }
365+
try slice.append(uninitializedCount, initializer)
366+
} else {
367+
self = .empty
368+
var newSlice = LargeSlice(slice)
369+
defer { self = .large(newSlice) }
370+
try newSlice.append(uninitializedCount, initializer)
371+
}
372+
case .large(var slice):
373+
self = .empty
374+
defer { self = .large(slice) }
375+
try slice.append(uninitializedCount, initializer)
376+
}
377+
}
378+
305379
@inlinable // This is @inlinable as reasonably small.
306380
mutating func resetBytes(in range: Range<Index>) {
307381
switch self {

0 commit comments

Comments
 (0)