Swift structs to NSData and back

I have a struct containing a struct and an NSObject that I want to serialize into an NSData object:

struct Packet {
  var name: String
  var index: Int
  var numberOfPackets: Int
  var data: NSData
}

var thePacket = Packet(name: name, index: i, numberOfPackets: numberOfPackets, data: packetData)

How do I best serialize thePacket into an NSData, and how do I best unserialize it?

  • Creating a large Dummy File
  • Getting data out of NSData with Swift
  • NSData and UIImage
  • NSData from hex String?
  • iOS - Download file only if modified (NSURL & NSData)
  • Objective-C Convert NSData to NSString
  • Using

    var bufferData = NSData(bytes: & thePacket, length: sizeof(Packet))
    

    of only gives me the pointers of name and data. I was exploring NSKeyedArchiver, but then I’d have to make Packet an object, and I’d prefer to keep it a struct.

    Cheers

    Nik

    4 Solutions Collect From Internet About “Swift structs to NSData and back”

    Not really getting any feedback, this is the solution I ended up with:

    1. Make encode() and decode() functions for my struct
    2. Change Int to Int64 so the Int has the same size on 32-bit and 64-bit platforms
    3. Have an intermediate struct (ArchivedPacket) that has no String or Data, but only Int64

    Here is my code, I would be very grateful for your feedback, especially if there are less cumbersome ways to do this:

    public struct Packet {
        var name: String
        var index: Int64
        var numberOfPackets: Int64
        var data: NSData
    
        struct ArchivedPacket {
            var index : Int64
            var numberOfPackets : Int64
            var nameLength : Int64
            var dataLength : Int64
        }
    
        func archive() -> NSData {
    
            var archivedPacket = ArchivedPacket(index: Int64(self.index), numberOfPackets: Int64(self.numberOfPackets), nameLength: Int64(self.name.lengthOfBytesUsingEncoding(NSUTF8StringEncoding)), dataLength: Int64(self.data.length))
    
            var metadata = NSData(
                bytes: &archivedPacket,
                length: sizeof(ArchivedPacket)
            )
    
            let archivedData = NSMutableData(data: metadata)
            archivedData.appendData(name.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)!)
            archivedData.appendData(data)
    
            return archivedData
        }
    
        func unarchive(data: NSData!) -> Packet {
            var archivedPacket = ArchivedPacket(index: 0, numberOfPackets: 0, nameLength: 0, dataLength: 0)
            let archivedStructLength = sizeof(ArchivedPacket)
    
            let archivedData = data.subdataWithRange(NSMakeRange(0, archivedStructLength))
            archivedData.getBytes(&archivedPacket)
    
            let nameRange = NSMakeRange(archivedStructLength, Int(archivedPacket.nameLength))
            let dataRange = NSMakeRange(archivedStructLength + Int(archivedPacket.nameLength), Int(archivedPacket.dataLength))
    
            let nameData = data.subdataWithRange(nameRange)
            let name = NSString(data: nameData, encoding: NSUTF8StringEncoding) as! String
            let theData = data.subdataWithRange(dataRange)
    
            let packet = Packet(name: name, index: archivedPacket.index, numberOfPackets: archivedPacket.numberOfPackets, data: theData)
    
            return packet
        }
    }
    

    Swift 3

    This is an unaltered copy-paste from a Playground in Xcode 8.2.1 that works. It is a bit simpler than other answers.

    import Foundation
    
    enum WhizzoKind {
        case floom
        case bzzz
    }
    
    struct Whizzo {
        let name: String
        let num: Int
        let kind:WhizzoKind
    
        static func archive(w:Whizzo) -> Data {
            var fw = w
            return Data(bytes: &fw, count: MemoryLayout<Whizzo>.stride)
        }
    
        static func unarchive(d:Data) -> Whizzo {
            guard d.count == MemoryLayout<Whizzo>.stride else {
                fatalError("BOOM!")
            }
    
            var w:Whizzo?
            d.withUnsafeBytes({(bytes: UnsafePointer<Whizzo>)->Void in
                w = UnsafePointer<Whizzo>(bytes).pointee
            })
            return w!
        }
    }
    
    let thing = Whizzo(name:"Bob", num:77, kind:.bzzz)
    print("thing = \(thing)")
    let dataThing = Whizzo.archive(w: thing)
    let convertedThing = Whizzo.unarchive(d: dataThing)
    print("convertedThing = \(convertedThing)")
    

    Notes

    I couldn’t make archive and unarchive instance methods because Data.init(bytes:​count:​) is mutating on the bytes parameter? And self isn’t mutable, so… This made no sense to me.

    The WhizzoKind enum is in there because that is something I care about. It’s not important for the example. Someone might be paranoid about enums like I am.

    I had to cobble this answer together from 4 other SO question/answers:

    • Getting data out of NSData with Swift
    • Extract struct from NSData in Swift
    • 'bytes' is unavailable: use withUnsafeBytes instead
    • Unsafe bytes in Swift 3

    And these docs:
    http://swiftdoc.org/v3.1/type/UnsafePointer/

    And meditating on the Swift closure syntax until I wanted to scream.

    So thanks to those other SO askers/authors.

    Update

    So this will not work across devices. For example, sending from iPhone 7 to Apple Watch. Because the stride is different. The above example is 80 bytes on iPhone 7 Simulator but 40 bytes on Apple Watch Series 2 Simulator.

    It looks like the approach (but not syntax) by @niklassaers is still the only one that will work. I’m going to leave this answer here because it might help others with all the new Swift 3 syntax and API changes surrounding this topic.

    Our only real hope is this Swift proposal: https://github.com/apple/swift-evolution/blob/master/proposals/0166-swift-archival-serialization.md

    I used Jeff’s example to create the following struct:

    struct Series {
    
    var name: String?
    var season: String?
    var episode: String?
    
    init(name: String?, season: String?, episode: String?) {
        self.name = name
        self.season = season
        self.episode = episode
    }
    
    static func archive(w: Series) -> Data {
        var fw = w
        return Data(bytes: &fw, count: MemoryLayout<Series>.stride)
    }
    
    static func unarchive(d: Data) -> Series {
        guard d.count == MemoryLayout<Series>.stride else {
            fatalError("Error!")
        }
    
        var w: Series?
        d.withUnsafeBytes({(bytes: UnsafePointer<Series>) -> Void in
            w = UnsafePointer<Series>(bytes).pointee
        })
        return w!
    }
    

    }

    Like Dag mentioned the whole thing is a bit fragile. Sometimes the App crashes when the name contains a whitespace or an underline/underscore, and sometimes it crashes just without reason. In all cases the name which is unarchived looks similar to this ‘4\200a\256’. Surprisingly this is not a problem in case of season or episode (like in “Season 2”). Here the whitespace doesn’t force the app to crash.

    Maybe it’s an alternative to encode the strings to utf8 but I’m not familiar enough with the archive/unarchive methods to adopt them for this case.

    It seems this came out recently, and to me it is looking solid. Didn’t try it yet…

    https://github.com/a2/MessagePack.swift


    Well, Swift doesn’t have any magical serialization method, if that’s what you are after. Since the good days of C, when you have a struct with a pointer, that is a flag that you can’t serialize the bytes of that struct’s instance without following the pointers and fetching their data. Same applies to Swift.

    Depending on your Serialization needs and constraints, I’d say using NSCoding or even JSON strings will tidy your code and make it more predictable than the current state. Sure, you’ll need to write a mapper, and there is an overhead. Everyone will tell you this: “Measure first”.

    Now, here is the interesting part:

    If you really want to inline your data in that struct, and stream the contents without building the packet around NSData as you’re doing, you can reserve bytes using Swift Tuples, which work much like how you would reserve bytes in C using char[CONST]:

    struct what { 
        var x = 3 
    }    
    
    sizeof(what)
    
    $R0: Int = 8
    
    struct the { 
        var y = (3, 4, 5, 7, 8, 9, 33) 
    }    
    
    sizeof(the)
    
    $R1: Int = 56
    

    To expand a bit on this, I think it’s pretty horrible, but possible. You can write to the tuple’s memory location and read from it using something like this.