1

I am trying to get the bytes from an integer into an [UInt8] to send them over a wire protocol. While I found answers that work for Swift 2/3, none of the solutions work for Swift 4.

The following snippet works to encode a message for small message sizes (just the raw string data prepended with a network byte order Int32 size):

func send(message: String) {
    let messageSize = message.utf8.count
    let encodedMessageSize = Int32(messageSize).bigEndian
    let frameSize = messageSize + 4

    var buffer: [UInt8] = Array()
    buffer.append(0)
    buffer.append(0)
    buffer.append(0)
    buffer.append(UInt8(messageSize))
    buffer.append(contentsOf: message.utf8)
    outputStream.write(buffer, maxLength: frameSize)
}

I have also tried using raw pointers directly, but cannot get anything to work for Swift 4 along that avenue either.

The overall tasks is to encode and frame messages that consist of integers and strings. The encoding converts everything to strings and adds a null at the end of each string. The framing simply prepends the message with a network byte order Int32 size. I cannot change the protocol, but am willing to consider other approaches to achieving this end.

cheers,

[EDIT] Updated code using @MartinR's code (with @Hamish's suggestion). Also made some progress of the overall task in the mean time.

func encodeMessagePart(_ message: String) -> [UInt8] {
    var buffer: [UInt8] = Array(message.utf8)
    buffer.append(0)
    return buffer
}

func encodeMessagePart(_ message: Int) -> [UInt8] {
    return encodeMessagePart("\(message)")
}

func frameMessage(_ buffer: [UInt8]) -> [UInt8] {
    let bufferSize = buffer.count
    var encodedBufferSize = Int32(bufferSize).bigEndian
    let encodedBufferSizeData = withUnsafeBytes(of: &encodedBufferSize) { Data($0) }

    var frame: [UInt8] = Array()
    frame.append(contentsOf: encodedBufferSizeData)
    frame.append(contentsOf: buffer)
    return frame
}

func sendMessage(_ buffer: [UInt8]) {
    let frame = frameMessage(buffer)
    outputStream.write(frame, maxLength: frame.count)
}

func sendMessage(_ message: String) {
    let encodedPart = encodeMessagePart(message)
    sendMessage(encodedPart)
}

//    func sendMessage(_ messages: Encodable...) {
//        var buffer: [UInt8] = Array()
//        for message in messages {
//            let b = encodeMessagePart(message)
//            buffer.append(contentsOf: b)
//        }
//        sendMessage(buffer)
//    }
RandomBits
  • 4,194
  • 1
  • 17
  • 30

1 Answers1

2

You can create a Data value from the integer with

let encodedMessageSize = Int32(messageSize).bigEndian
let data = withUnsafeBytes(of: encodedMessageSize) { Data($0) }

(In Swift versions before 4.2 you'll have to write

var encodedMessageSize = Int32(messageSize).bigEndian
let data = withUnsafeBytes(of: &encodedMessageSize) { Data($0) }

instead.)

The data can then be appended to the array with

buffer.append(contentsOf: data)

Alternatively you can use a data buffer instead of an array:

func send(message: String) {
    let messageSize = message.utf8.count
    let encodedMessageSize = Int32(messageSize).bigEndian

    var data = withUnsafeBytes(of: encodedMessageSize) { Data($0) }
    data.append(Data(message.utf8))

    let amountWritten = data.withUnsafeBytes { [count = data.count] in
        outputStream.write($0, maxLength: count)
    }
}

Finally note that that the write() method might write less bytes than provided (e.g. on network connections), so you should always check the return value.

Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
  • Unfortunately, `Data(buffer: UnsafeBufferPointer(start: &encodedMessageSize, count: 1))` isn't well defined, as the inout-to-pointer conversion for `&encodedMessageSize` produces a pointer only valid for the duration of the call to `UnsafeBufferPointer.init`. You could use `withUnsafeBytes(of:_:)` instead – e.g `withUnsafeBytes(of: &encodedMessageSize) { Data($0) }`. And in Swift 4.2+, `encodedMessageSize` can be a constant and passed non-inout :) – Hamish Sep 29 '18 at 15:47
  • @Hamish: You are right (and I now I remember what I thought about it some time ago, but forgot about it again). It seems that I should use the weekend to rewrite https://stackoverflow.com/questions/38023838/round-trip-swift-number-types-to-from-data :) – Martin R Sep 29 '18 at 15:53
  • Hopefully a future version of the language prevents such temporary conversions for arguments of `Unsafe[...].init` calls (I've actually got an experimental branch going that does just that, but it needs a bunch more polishing before a PR), as IMO it's far too easy to accidentally do the wrong thing with them ([even the stdlib itself](https://github.com/apple/swift/pull/18799) can get it wrong!). – Hamish Sep 29 '18 at 16:00
  • @MartinR: What are the pro's / con's of using Data versus [UInt8]? – RandomBits Sep 29 '18 at 16:33
  • @RandomBits: There is a report that `Data` is slower than `Array` (https://forums.swift.org/t/performance-issues-with-foundation-data/16016) but that probably is only relevant for larger amounts. – Martin R Sep 29 '18 at 17:19
  • (for anyone interested – [I've now opened a PR](https://github.com/apple/swift/pull/20070) to reject unsound pointer conversions and am hoping to merge at least the warning logic for Swift 5) – Hamish Oct 27 '18 at 22:07