Search code examples
swiftnsmutabledata

Appending and extracting objects from NSMutableData


I need to create create a data stream that contains multiple parameters, send it over the network and then extract those parameter when I receive the data. This is how and create my data (I'm certain all of my variables contain a value)

 let dataToSend = NSMutableData()
 var mType = Int32(messageType.rawValue)
 var reqId = Int32(requestId)
 dataToSend.appendDataWithUnsafeBytes(from: &mType, of: Int32.self)
 dataToSend.appendDataWithUnsafeBytes(from: &reqId, of: Int32.self)
 /* extra protocol data length. In version 0, this is0 as thereis no extra data.
 In the future, if you need to add extra protocol data use this*/
 var unUsedProtocol = Int32(0)
 dataToSend.appendDataWithUnsafeBytes(from: &unUsedProtocol, of: Int32.self)
 var encodedDataJson = !jsonString.isEmptyOrNil ? jsonString?.asciiValues : [UInt8]()
 dataToSend.appendDataWithUnsafeBytes(from: &encodedDataJson, of: [UInt8]?.self)
 var bData = bindaryData
 dataToSend.appendDataWithUnsafeBytes(from: &bData, of: Data.self)

here is my appendDataWithUnsafeBytes NSMutableData extension.

    extension NSMutableData {
        func appendDataWithUnsafeBytes<T>(from element: inout T, of type: T.Type) {
            let size = MemoryLayout.size(ofValue: element)
            withUnsafeBytes(of: &element) { ptr in
                let buffer = ptr.bindMemory(to: type)
                if let address = buffer.baseAddress {
                    self.append(address, length: size)
                } else {
                    VsLogger.logDebug("appendDataWithUnsafeBytes", "unable to get base address of pointer of type: \(type)")
                }
            }
        }
    }

and this is how try to extract it (I get the index value along with the data)

        var messageTypeValue: Int32? = nil
        var requestId: Int32? = nil
        var encodedJsonData: Data? = nil
        var binaryData: Data? = nil
        let intSize = MemoryLayout<Int32>.size
        let dataSize = MemoryLayout<Data>.size
        var offset = index
        
        bufferData.getBytes(&messageTypeValue, range: NSRange(location: offset, length: intSize))
        offset += intSize //8
        bufferData.getBytes(&requestId, range: NSRange(location: offset, length: intSize))
        offset += intSize //12
        /*skipping extra bytes (unsuedProtocol in sendMessageFunction). They come from a future version
         that this code doesn't understand*/
        offset += intSize //16
        bufferData.getBytes(&encodedJsonData, range: NSRange(location: offset, length: dataSize))
        offset += dataSize //32
        bufferData.getBytes(&binaryData, range: NSRange(location: offset, length: dataSize))

I'm only able to get the first value (messageTypeValue) but for the rest I either get nil or not the right data.

Thank you!

***** UPDATE *****

I got it working by modifying my sending and receiving functions as follows. Where I send it.

            let dataToSend = NSMutableData()
            var mType = Int32(messageType.rawValue)
            var reqId = Int32(requestId)
            dataToSend.appendDataWithUnsafeBytes(from: &mType, of: Int32.self)
            dataToSend.appendDataWithUnsafeBytes(from: &reqId, of: Int32.self)
            /* estra protocol data length. In version 0, this is0 as thereis no extra data.
             In the future, if you need to add extra protocol data use this*/
            var unUsedProtocol = Int32(0)
            dataToSend.appendDataWithUnsafeBytes(from: &unUsedProtocol, of: Int32.self)
            var jsonData = Data(!jsonString.isEmptyOrNil ? jsonString!.asciiValues : [UInt8]())
            dataToSend.appendDataWithUnsafeBytes(from: &jsonData, of: Data.self)
            var bData = bindaryData
            dataToSend.appendDataWithUnsafeBytes(from: &bData, of: Data.self)

where I receive it

        var offset = Int(index)
        let int32Size = MemoryLayout<Int32>.size
        let dataSize = MemoryLayout<Data>.size
        
        let messageTypeValue = (bufferData.bytes + offset).load(as: Int32.self)
        offset += int32Size
        let requestId = (bufferData.bytes + offset).load(as: Int32.self)
        offset += int32Size
        //skip this one since it not used
        //let unusedProtocol = (bufferData.bytes + offset).load(as: Int32.self)
        offset += int32Size
        let encodedJsonData = (bufferData.bytes + offset).load(as: Data.self)
        offset += dataSize
        let binaryData = bufferData.bytes.load(fromByteOffset: offset, as: Data.self)

the data is supposed to always be align properly, but is there a way to do some error checking on bufferData.bytes.load(fromByteOffset:as:)


Solution

  • As already mentioned in the comments I highly recommend to use native Data.

    First of all you need MartinR's Data extension to convert numeric types to Data and vice versa. I added an custom append method

    extension Data {
    
        init<T>(from value: T) {
            self = Swift.withUnsafeBytes(of: value) { Data($0) }
        }
    
        func to<T>(type: T.Type) -> T? where T: ExpressibleByIntegerLiteral {
            var value: T = 0
            guard count >= MemoryLayout.size(ofValue: value) else { return nil }
            _ = Swift.withUnsafeMutableBytes(of: &value, { copyBytes(to: $0)} )
            return value
        }
    
        mutating func append<T>(_ other: T) {
            append(.init(from: other))
        }
    } 
    

    This is a very simple version of your data set, three Int32 values and a JSON array

    let mType : Int32 = 1
    let reqId : Int32 = 2
    let unUsedProtocol : Int32 = 0
    let json = try! JSONEncoder().encode(["hello", "world"])
    

    For convenience I omitted the error handling. In production code try! is discouraged.

    The huge benefit of Data is that it can be treated as a collection type, an array of (UInt8) bytes. To build the Data package convert the numeric values and append the bytes respectively

    var dataToSend = Data()
    dataToSend.append(mType)
    dataToSend.append(reqId)
    dataToSend.append(unUsedProtocol)
    dataToSend.append(json)
    

    On the receiver side to extract the numeric values back this is a helper function which increments also the current index (as inout type), the byte length arises from the type. The function throws an error if the index is out of range and if the type cannot be converted.

    enum ConvertDataError : Error { case outOfRange, invalidType}
    
    func extractNumber<T : ExpressibleByIntegerLiteral>(from data : Data, type: T.Type, startIndex: inout Int) throws -> T {
        let endIndex = startIndex + MemoryLayout<T>.size
        guard endIndex <= data.endIndex else { throw ConvertDataError.outOfRange }
        let subdata = data[startIndex..<endIndex]
        guard let resultType = subdata.to(type: type) else { throw ConvertDataError.invalidType }
        startIndex = endIndex
        return resultType
    }
    

    Get the start index of the data package and extract the Int32 values

    var index = dataToSend.startIndex
    
    do {
        let mType1 = try extractNumber(from: dataToSend, type: Int32.self, startIndex: &index)
        let reqId1 = try extractNumber(from: dataToSend, type: Int32.self, startIndex: &index)
        let unUsedProtocol1 = try extractNumber(from: dataToSend, type: Int32.self, startIndex: &index)
        print(mType1, reqId1, unUsedProtocol1)
    

    For the json part you need the length, in this example 17 bytes

        let jsonLength = json.count
    
        let jsonOffset = index
        index += jsonLength
        let json1 = try JSONDecoder().decode([String].self, from: dataToSend[jsonOffset..<index])
        print(json1)
    } catch {
        print(error)
    }