Search code examples
arraysswiftbytensdata

Is there a way to "cast" Data to [UInt8] (a byte array) without copying, or is Unsafe the way to go?


I know I can do

//Setup
let originalBytes: [UInt8] = [0x0, 0x1, 0x2]
let originalData = Data(originalBytes)

//This
let getByteArFromData: [UInt8] = [UInt8](originalData)

Now, I'm pretty sure that getByteArFromData COPIES the bytes into new allocated memory (am I wrong here?).

That gets expensive as the data scales (e.g. images, codec manipulation, etc.).

I am motivated to ask this question because I would like to manipulate data through an array-like interface, but as far as I know, the Unsafe API is the way to go. So...

Is using the Unsafe API the only way to do this? Is it the recommended way to do this?

I've played with some pointer arithmetic too (just getting into the Unsafe API, not sure if I should invest the time if I can use an array-like interface).

Here's some playground code (please critique it and answer the questions in the comments if you'd be so kind):

import Foundation

//MARK:- Setup

final class PlaygroundSwingSet
{
    //Squish unimportant setup code
    private let byteAr: [UInt8]
    private lazy var data: Data = {Data(byteAr)}()
    init(_ bytes: [UInt8]) {self.byteAr = bytes}

    func swing() -> Int
    {
        //Unsafe API stuff
        return data.withUnsafeBytes {(dataBufferPtr) -> (Int) in   //No need for [weak self], right?
            //I don't like how the pointers are only valid in this closure.
            //It adds an indent to my code to which my "code OCD" forces me to abstract out
            self.middleOut(dataBufferPtr)
        }
    }

    private func middleOut(_ dataBufferPtr: UnsafeRawBufferPointer) -> Int
    {
        //Yuck, I have to make sure count isn't 0 AND safely unwrap.
        //Why is .baseAddress even optional then?
        guard let dataPtr = dataBufferPtr.baseAddress?.assumingMemoryBound(to: UInt8.self), dataBufferPtr.count > 0 else {
            print("data is empty")
            return 0
        }
        let middishIndex = dataBufferPtr.count / 2 - 1

        //More yuck.
        print("truncated middle element's value is \(dataPtr.advanced(by: middishIndex).pointee)")
        print("this should yield the same thing: \((dataPtr + middishIndex).pointee)")
        return middishIndex
    }
}

//MARK:- Code Execution

let a = PlaygroundSwingSet([0x1, 0x2, 0x3, 0x4]).swing() //a = 1
//Console prints
/*
 truncated middle element's value is 3
 this should yield the same thing: 2
 */
let b = PlaygroundSwingSet([]).swing()  //b = 0
//Console prints
/*
 data is empty
 */

Solution

  • There is no need to cast Data because it supports the subscript operator to access the UInt8 at a given index.

    Incidentally it also supports isEmpty, so you do not need to check if count is 0.

    var data = Data(repeating: 0, count: 5)
    data.indices.forEach { data[$0] = UInt8($0) }
    print(data[2])