Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Getting data out of NSData with Swift

I am finding Swift and NSData to be an unholy matrimony of frustration. I find I feel like all the supposed new found Swift safety goes out the window every time I deal with this thing. The amount of crashes (with unhelpful traces) don't help.

So, I've learned, that I can avoid the scary UnsafeMutablePointer stuff by doing things like the following:

var bytes = [UInt8](count: 15, repeatedValue: 0)
anNSData.getBytes(&bytes, length=15)

I also discovered, that I can extract directly into singular values:

var u32:UInt32 = 0
anNSData.getBytes(&u32, length=4)

This leads to two intermediate questions:

1) Is there something I can use that is more reliable than hardcoded constants there. If this were C, I'd just use sizeof. But I think I read that maybe I should be using strideof instead sizeof? And that wouldn't work on [UInt8]'s, would it?

2) The docs (for Swift) says that this parameter is supposed to be _ buffer: UnsafeMutablePointer<Void>. So how does this work? Am I just getting lucky? Why would I want to do that instead of the more native/managed [Uint8] construct?? I wondered if UnsafeMutablePointer was a protocol, but it's a struct.

Emboldened with reading the values directly (rather than as an Array), I thought maybe I could try another kind of struct. I have a 6 byte struct that looks like:

struct TreeDescription : Hashable {
    var id:UInt32 = 0x00000000
    var channel:UInt8 = 0x00
    var rssi:UInt8 = 0x00

    var hashValue:Int {
        return Int(self.id)
    }
}

Which actually works (after thinking it didn't, but eventually doing a clean which made some crashes go away)!

var tree = TreeDescription()
anNSData.getBytes(&newTree, length: 6)

But this leads me to worries about structure packing details? Why does this work? What should I be worrying about doing this?

This all feels very C-ish to me. I thought Swift took the C out of ObjectiveC.

like image 721
Travis Griggs Avatar asked Aug 31 '15 18:08

Travis Griggs


1 Answers

You may want to check out RawData which is really new and this guy just experimented a bit with this idea, so don't think that it's tested well or anything, some function aren't even implemented yet. It's basically a Swift-y wrapper around (you guessed it) raw data, a series of bytes.

Using this extension, you can initialise it with an NSData instance:

extension RawData {
    convenience init(data: NSData) {
        self.init(UnsafeMutableBufferPointer(start: UnsafeMutablePointer(data.bytes), count: data.length))
    }
}

You'de be calling it like this:

let data = "Hello, data!".dataUsingEncoding(NSASCIIStringEncoding)!

let rawData = RawData(data: data)

EDIT: To answer your questions:

The thing is that data can be large, very large. You generally don't want to copy large stuff, as space is valuable. The difference between an array of [UInt8] values and an NSData instance is that the array gets copied every time, you give it to a function -> new copy, you do an assignment -> new copy. That's not very desirable with large data.

1) If you want the most native, safe way, without any 3rd party libraries as the one mentioned, you can do this:

let data = UnsafeMutableBufferPointer(start: UnsafeMutablePointer(data.bytes), count: data.length)

(I know it doesn't sound very safe, but believe me it is). You can use this almost like an ordinary array:

let data = "Hello, data!".dataUsingEncoding(NSASCIIStringEncoding)!

let bytes = UnsafeMutableBufferPointer(start: UnsafeMutablePointer<UInt8>(data.bytes), count: data.length)

for byte in bytes {}
bytes.indexOf(0)
bytes.maxElement()

and it doesn't copy the data as you pass it along.

2) UnsafeMutablePointer<Void> is indeed very C-like, in this context it represents the starting value (also called base) in a sequence of pointers. The Void type comes from C as well, it means that the pointer doesn't know what kind of value it's storing. You can cast all kinds of pointers to the type you're expecting like this: UnsafeMutablePointer<Int>(yourVoidPointer) (This shouldn't crash). As mentioned before, you can use UnsafeMutableBufferPointer to use it as a collection of your type. UnsafeMutableBufferPointer is just a wrapper around your base pointer and the length (this explains the initialiser I used).

Your method of decoding the data directly into your struct does indeed work, the properties of a struct are in the right order, even after compile time, and the size of a struct is exactly the sum of it's stored properties. For simple data like yours that's totally fine. There is an alternative: To use the NSCoding protocol. Advantage: safer. Disadvantage: You have to subclass NSObject. I think you should be sticking to the way you do it now. One thing I would change though is to put the decoding of your struct inside the struct itself and use sizeof. Have it like this:

struct TreeDescription {
    var id:UInt32 = 0x00000000
    var channel:UInt8 = 0x00
    var rssi:UInt8 = 0x00

    init(data: NSData) {
        data.getBytes(&self, length: sizeof(TreeDescription))
    }
}

Another EDIT: You can always get the underlying data from an Unsafe(Mutable)Pointer<T> with the method memory whose return type is T. If you need to you can always shift pointers (to get the next value e.g.) by just adding/subtracting Ints to it.

EDIT answering your comment: You use the & to pass an inout variable, which can then be modified within the function. Because an inout variable is basically the same as passing the pointer, the Swift devs decided to make it possible to pass &value for an argument that is expecting an UnsafeMutablePointer. Demonstration:

func inoutArray(inout array: [Int]) {}

func pointerArray(array: UnsafeMutablePointer<Int>) {}

var array = [1, 2, 3]

inoutArray(&array)
pointerArray(&array)

This also works for structs (and maybe some other things)

like image 184
Kametrixom Avatar answered Sep 27 '22 18:09

Kametrixom