Search code examples
swiftnsdatafoundation

Data Value Changing in Swift Playground


In a playground, the following code initializes Data using an UnsafeBufferPointer, as described in the Apple Foundation Documentation

let data = Data()
let test = Array(0..<10)
let pointer = UnsafeRawPointer(test).assumingMemoryBound(to: UInt8.self)
data = Data.init(buffer: UnsafeBufferPointer(start: pointer, count: MemoryLayout.size(ofValue: test)))
data[8]

Running this program multiple times produces different values for data[8]. Why is the value changing?


Solution

  • MemoryLayout.size(ofValue: test) is equivalent to MemoryLayout<[Int]>.size (the parameter is only used as a way to infer the generic placeholder type). It's not giving you the size of the array's buffer, it's giving you the size of the Array struct itself, which is currently 1 word (8 bytes on a 64-bit machine) in size, as the elements are held indirectly.

    Therefore the Data instance you construct only holds 8 bytes, so accessing data[8] will read out of bounds garbage; which is why you're getting unexpected results. This out of bounds access will actually cause a runtime error in Swift 4 (as of the version that ships with Xcode 9 beta 4).

    But ignoring all of that, using UnsafeRawPointer(test) to begin with is undefined behaviour, as it's using a pointer to a buffer that's only valid for the duration of the initialiser call. Swift only guarantees that auto-generated pointer arguments (e.g when passing an array to a constant pointer parameter) will be valid for the duration of the given function call (see the Swift team's blog post on Interacting with C Pointers).

    If you just want to dump the bytes of the array's buffer into a Data instance, you simply want:

    let test = Array(0 ..< 10)
    let data = test.withUnsafeBufferPointer(Data.init)
    // or let data = test.withUnsafeBufferPointer { Data(buffer: $0) }
    
    print(data as NSData) // bridge to NSData to get a full print-out of bytes  
    
    // <00000000 00000000
    //  01000000 00000000
    //  02000000 00000000
    //  03000000 00000000
    //  04000000 00000000
    //  05000000 00000000
    //  06000000 00000000
    //  07000000 00000000
    //  08000000 00000000
    //  09000000 00000000>
    
    print(data[8]) // 1
    

    (64-bit little endian machine)

    which uses withUnsafeBufferPointer(_:) to get an immutable buffer pointer view onto the array's buffer (and if it's not native, e.g wrapping an NSArray; it will have to be created), and Data's init(buffer:) to construct a new instance with the bytes from the given buffer pointer.

    If you want the bytes to correspond 1 : 1 to the elements in the array, you need to make each of the elements 1 byte in length.

    For example, by starting with a [UInt8]:

    let test = [UInt8](0 ..< 10)
    let data = test.withUnsafeBufferPointer(Data.init)
    print(data as NSData) // <00010203 04050607 0809>
    print(data[8]) // 8
    

    And because you're now working with a sequence of UInt8, you can actually simplify the initialisation slightly by using Data's sequence of UInt8 initialiser:

    let data = Data(test)