Search code examples
swiftunsafe-pointers

What is the modern canonical way to support converting ContiguousBytes into both Array and "not-Array"?


For example, without this second overload, loading an Array will yield "UnsafeRawBufferPointer.load out of bounds". Is there a way to handle both cases without overloads?

let bytes: [UInt8] = [1, 0, 1, 0]
bytes.load() as Int32 // 0x1_00_01
bytes.load() as [Int16] // [1, 1]
public extension ContiguousBytes {
  func load<T>(_: T.Type = T.self) -> T {
    withUnsafeBytes { $0.load(as: T.self) }
  }

  func load<Element>(_: [Element].Type = [Element].self) -> [Element] {
    withUnsafeBytes { .init($0.bindMemory(to: Element.self)) }
  }
}

Solution

  • In terms of having two overloads: you unfortunately can't avoid them. ContiguousBytes.withUnsafeBytes gives you access to the underlying storage of a type which implements ContiguousBytes, so the buffer given by, say, [UInt8].withUnsafeBytes will be the actual buffer the array instance is using to store data, not a pointer to itself in memory (e.g. its length, capacity, storage pointer, etc.).

    The reason your first overload can't work is that you'd be calling the equivalent of:

    withUnsafeBytes { rawBuffer in
        rawBuffer.load(as: [Int16].self)
    }
    

    where load(as:) is attempting to read [1, 0, 1, 0] as if those contents were the length, capacity, and buffer pointer of a single [Int16] instance, but that's not the case — it's only the contents of bytes.

    You need the second overload in order to create a new array with the contents of that UnsafeRawBufferPointer.


    As for the actual implementation of your array variant, as of SE-0333 Expand usability of withMemoryRebound coming in Swift 5.7, the correct way to do this would be to use withMemoryRebound(to:_:), as the unconditional bindMemory(to:) is unsafe if the buffer was already bound to a different type. withMemoryRebound(to:_:) is now always safe, even if the buffer is bound, so long as you meet alignment requirements:

    /// A raw buffer may represent memory that has been bound to a type.
    /// If that is the case, then T must be layout compatible with the
    /// type to which the memory has been bound. This requirement does not
    /// apply if the raw buffer represents memory that has not been bound
    /// to any type.

    This would look like

    func load<Element>(_: [Element].Type = [Element].self) -> [Element] {
        withUnsafeBytes { rawBuffer in
            rawBuffer.withMemoryRebound(to: Element.self) { typedBuffer in
                Array(typedBuffer)
            }
        }
    }
    

    UnsafeRawBufferPointer.withMemoryRebound(to:_:) will do the math on binding the raw bytes to the right stride of Element, though keep in mind that this is only valid to do if the alignment of the raw buffer matches the alignment requirements of Element — otherwise you'll either crash or trigger undefined behavior.