What happened to `Data(rawCapacity:)` initializer?

Greetings!

Trying to understand how to work with output spans. I noticed this WWDC session showcases the use of:

 let pixelData = Data(rawCapacity: totalBytes) { outputSpan in

Which does not seem to exist in Foundation as of 6.2.

The same example in Swift Binary Parsing has:

// TODO: Use OutputRawSpan instead of Data
var pixelData = Data(capacity: header.pixelCount * channels)

Which leads me to believe the feature didn’t make the cut.

Does anyone know of a work-around way of obtaining a OutputRawSpan that does not involve unsafe code?

Is there another safe and efficient way to output serialized binary representations of Swift structs?

Thanks!

1 Like

Hi Craig, that initializer didn’t make the release of Swift 6.2. It is still intended to appear in a future release of Swift.

1 Like

Just following up on this topic, as of Swift 6.4 there's still a few small holes to plug if you want to use OutputRawSpan for serializing arbitrary types into a binary representation.

For the missing Data.init(rawCapacity:building) we ended up "polyfilling" it with:

extension Data {

    public init<E: Error>(
        rawCapacity size: Int,
        building initializer: (inout OutputRawSpan) throws(E) -> Void
    ) throws(E) {
        precondition(size >= 0, "size must be non-negative")
        guard size > 0 else { self = Data(); return }

        let raw = UnsafeMutableRawBufferPointer.allocate(byteCount: size, alignment: 1)
        var out = OutputRawSpan(buffer: raw, initializedCount: 0)

        do {
            try initializer(&out)
        } catch {
            // The closure throwing must not leak the allocation.
            _ = out.finalize(for: raw)
            raw.deallocate()
            throw error
        }

        let written = out.finalize(for: raw)
        precondition(written == size, "declared \(size) bytes but wrote \(written)")

        self = Data(
            bytesNoCopy: raw.baseAddress!,
            count: written,
            deallocator: .custom { pointer, _ in pointer.deallocate() }
        )
    }
}

This way you can serialize while minimizing copying memory. This is how we're using it to serialize variable (compact) integers:

let varInt = VarInt(Int.max)
let data = try Data(rawCapacity: varInt.dataSize) { out in
    try varInt.encode(into: &out)
}

An alternative way of achieving something similar is just using withTemporaryAllocation() but that also needs a little extension, especially if you want to keep unsafe code contained.

extension Data {
    public init(_ span: borrowing OutputRawSpan) {
        self = span.withUnsafeBytes { pointer in
            guard let base = pointer.baseAddress else {
                return Data()
            }
            return Data(bytes: base, count: pointer.count)
        }
    }
}

Now you can do something like the following – again to serialize a variable integer:

let data = try withTemporaryAllocation(byteCount: varInt.dataSize, alignment: 1) { span in
    try varInt.encode(into: &span)
    return Data(span)
}

Finally, this last extension we found useful for appending Data to an OutputRawSpan when serializing:

extension OutputRawSpan {

    /// Appends the raw bytes of a Data instance directly into the buffer layout.
    public mutating func append<D: ContiguousBytes>(contentsOf data: D) {
        data.withUnsafeBytes { sourceBuffer in
            guard !sourceBuffer.isEmpty else { return }

            // Gain access to the underlying storage pointer and the layout's active count
            withUnsafeMutableBytes { targetBuffer, initializedCount in

                // Precondition to protect against runtime buffer overflows
                precondition(targetBuffer.count - initializedCount >= sourceBuffer.count, "Out of bounds: OutputRawSpan has insufficient capacity.")

                // Point directly to the first uninitialized byte in the memory block
                let writePointer = targetBuffer.baseAddress! + initializedCount

                // Access the source data's contiguous memory buffer safely
                data.withUnsafeBytes { sourceBuffer in
                    // Perform an optimized memcpy directly into the buffer layout
                    writePointer.copyMemory(from: sourceBuffer.baseAddress!, byteCount: sourceBuffer.count)
                }

                // Explicitly step the internal tracking counter forward
                initializedCount += sourceBuffer.count
            }
        }
    }
}

For instance we use this to serialize a Script consisting of a sequence of operations and a potential unparsable part which is just bytes:

public func encode(into out: inout OutputRawSpan) throws {
    for op in operations {
        try op.encode(into: &out)
    }
    out.append(contentsOf: unparsable)
}

Hope someone finds some of this useful. Please provide feedback if you have any, we've been struggling with binary parsing and serialization ever since we started Swift Bitcoin even implementing our own library and helpers before falling back to Swift Binary Parsing and OutputRawSpan.

2 Likes

Hey @craigwrong - thanks for your insight here! You might be interested in checking out this new API proposal which is approved and currently in the nightly-main toolchains: swift-foundation/Proposals/0042-data-span-apis.md at main · swiftlang/swift-foundation · GitHub. I think this should cover a few of the APIs that you are reaching for here:

  1. It introduces Data(capacity:) which provides you with an OutputRawSpan (there is no more Data(rawCapacity:), if you want an OutputSpan then you will use the (to be implemented) APIs to get an OutputSpan from an OutputRawSpan
  2. I introduces Data(copying:) which initializes a data copying from a span (which would be useful in your second example, although I'm not certain because the extension above it seems like it might be a mismatch from copy/paste

Your final example (appending a ContiguousBytes or a Data to an OutputRawSpan) we expect would be expressed as the following:

myOutputRawSpan.append(copying: myData.bytes)

// - or -
myContiguousBytes.withBytes {
    myOutputRawSpan.append(copying: $0)
}

ContiguousBytes.withBytes (providing a RawSpan) is new in Swift 6.4, but the Data.bytes RawSpan property has been around for a little while now. The key piece that's missing is the ability to easily append a RawSpan to an OutputRawSpan, but that is an API that we'd like to introduce as well.

Would love to hear if some of those APIs in that proposal fit your needs or if there's any additional APIs we don't have yet aside from OutputRawSpan appending a RawSpan that you're reaching for here that we have unaccounted for!

3 Likes

OMG thank you so much @jmschonfeld! The proposal is pure gold and just what I feel is missing for us to remove the mentioned extensions. BTW you were right about the second extension, I've corrected my post with the actual code we are using.

As for OutputRawSpan.append(copying:) even though it is not in the shared proposal, it would obviously be extremely useful for our use case.

Our project is 50% serialization/deserialization logic and we are competing against a highly optimized C++ implementation so it's important we get this right. With Swift Binary Parsing and OutputRawSpan I feel like we are almost there.

Our custom BinaryEncodable protocol has been reduced to a mere sink for counting bytes.

Once again thanks for the valuable pointers.

3 Likes

That's great to hear, and thank you for the kind words! I'm glad to hear it fits your use case. Definitely feel free to take the APIs for a spin with a recent nightly toolchain if you want to try them out.

Definitely agreed - this would be an API that would go into the standard library rather than Foundation and @glessard has been looking into it along with other similar collection-adjacent APIs on output span types.

2 Likes