0

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:)

Gama
  • 352
  • 2
  • 16
  • Consider to use native `Data` and suggestions like https://stackoverflow.com/questions/38023838/round-trip-swift-number-types-to-from-data – vadian Feb 19 '22 at 15:43
  • @vadian tried using the native Data, but I could't find a way to get it working with the different offsets of the objects in the data received. – Gama Feb 19 '22 at 17:35

1 Answers1

0

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)
}
vadian
  • 274,689
  • 30
  • 353
  • 361
  • @I had something similar to that before but the issue I rant into was that I do not have access to the length of the json. – Gama Feb 21 '22 at 17:43
  • If the JSON represents an array or dictionary you can calculate the length: Get the first character (`{` or `[`) and increment a counter. Then walk through the bytes and increment the counter on an *open* character and decrement it on a *close* character. If the counter reaches zero the index is the position of the last character (`}` or `]`) of the JSON. The rest is a bit math. – vadian Feb 21 '22 at 17:50
  • Yes, of course, but the byte array represents a JSON **string**. If the first character is `[` the byte is 0x5b (or 0x7b for `{`). In my example the byte array is `<5b226865 6c6c6f22 2c202277 6f726c64 225d>` – vadian Feb 21 '22 at 18:14
  • gotcha! So after going through everything (again) I realized that I'm actually supposed to pass the length of the json as you mentioned. Just a honest silly mistake but thank you for sharing your thoughts, it helped my code a lot. – Gama Feb 21 '22 at 21:06