9
\$\begingroup\$

I need the byte representation of a given type, especially double, float, int8, etc.

typealias Byte = UInt8

enum ByteOrder {
    case BigEndian
    case LittleEndian
}

func pack<T>(var value: T, byteOrder: ByteOrder) -> [Byte] {
    let valueByteArray = withUnsafePointer(&value) {
        Array(UnsafeBufferPointer(start: UnsafePointer<Byte>($0), count: sizeof(T)))
    }
    return (byteOrder == .LittleEndian) ? valueByteArray : valueByteArray.reverse()
}

func unpack<T>(valueByteArray: [Byte], toType type: T.Type, byteOrder: ByteOrder) -> T {
    let bytes = (byteOrder == .LittleEndian) ? valueByteArray : valueByteArray.reverse()
    return UnsafePointer<T>(bytes).memory
}

What do you think?

Especially I am interested in your opinion on

  • CFByteOrder does offer already a byte order, but I decided to go with a different definition.
  • We can use the type inference and get rid of forType type: T.Type, which works fine for

    let result: double = unpack(someArray, .LittleEndian)
    

    but fails for

    let result = unpack(someArray, .LittleEndian)
    
  • Is the following nicer? I don't think so, but what about you. :-)

    func unpack<T>(valueByteArray: [Byte], byteOrder: ByteOrder) -> T {
        let bytes = (byteOrder == .LittleEndian) ? valueByteArray : valueByteArray.reverse()    
        return bytes.withUnsafeBufferPointer {
            return UnsafePointer<T>($0.baseAddress).memory
        }
    }
    
  • What do you think about encapsulating the two function in a class, e.g. ByteBackPacker and convert them to class functions?

  • What about giving byteOrder: ByteOrder a default value, e.g. .LittleEndian, because this is the native byte order in iOS and Mac OS X, the primary platforms for Swift, I assume?

Update: I created a ByteBackpacker project on github. Let me know what you think about it.

\$\endgroup\$
7
\$\begingroup\$

Using a dedicated enumeration type ByteOrder is a good idea. CFByteOrder is (ultimately) a type alias for Int, so you can assign arbitrary integers to it, not just CFByteOrderBig/LittleEndian.


If you don't pass the type in the unpack function then the compiler has to infer it from the context and either of these would work:

let d1 = unpack(bytes, byteOrder: .BigEndian) as Double
let d2 : Double = unpack(bytes, byteOrder: .BigEndian)

If you pass the type as an argument then the compiler infers the return type:

let d3 = unpack(bytes, toType: Double.self, byteOrder: .BigEndian)

I think it is a matter of taste which version you choose, I would prefer passing the type explicitly.


Encapsulating the functions into a type is also a good idea. But I would use a struct which is initialized with the byte order, and make that instance methods and not class methods. The reason is that you probably will do several calls with the same byte order and you don't have to pass it to every call:

struct BytePacker {
    let byteOrder : ByteOrder

    init (byteOrder: ByteOrder) {
        self.byteOrder = byteOrder
    }

    func pack<T>(var value: T) -> [Byte] { ... }
    func unpack<T>(valueByteArray: [Byte], toType type: T.Type) -> T { ... }
}

which can be used as

let packer = BytePacker(byteOrder: .BigEndian)
let bytes = packer.pack(12.34)
let d = packer.unpack(bytes, toType: Double.self)

There is a problem with

return UnsafePointer<T>(bytes).memory

in your first version of unpack() because is not guaranteed that the elements of a Swift Array are stored in contiguous memory.

return bytes.withUnsafeBufferPointer {
    return UnsafePointer<T>($0.baseAddress).memory
}

as in your second version of unpack() is the correct solution because withUnsafeBufferPointer() calls the block with a pointer to contiguous storage.


Your code assumes that the host byte order is little-endian, which is the case for all current OS X and iOS platforms. On the other hand, Swift is open source now and people already start to port it to different platforms. I have no idea if Swift will be ported to a big-endian platform at some time.

I would at least make that assumption explicit by defining

enum ByteOrder {
    case BigEndian
    case LittleEndian

    static var hostByteOrder = ByteOrder.LittleEndian
}

and then replace

return (byteOrder == .LittleEndian) ? valueByteArray : valueByteArray.reverse()

by

return (byteOrder == ByteOrder.hostByteOrder) ? valueByteArray : valueByteArray.reverse()

In addition, I would add an assertion

assert(UInt(littleEndian: 1) == 1)

e.g. to the init method of BytePacker.

Alternatively, you can determine the actual host byte order at runtime:

enum ByteOrder {
    case BigEndian
    case LittleEndian

    static var hostByteOrder = ByteOrder()

    init() {
        self = (UInt(littleEndian: 1) == 1) ? .LittleEndian : .BigEndian
    }
}

but I don't know if that is worth the hassle. (Note that the static property hostByteOrder is computed only once in the lifetime of the app, not each time that it is used.)


Your generic pack/unpack functions can be called with arguments of any type, and that would give wrong results or crashes for

  • reference types, where the instance is just a pointer to the object,
  • "complex types" like Array or String, which are a struct but use opaque pointers to the actual memory holding the elements.

There is – as far as I know – no protocol which comprises all integer and floating point types. What you could do is to define a protocol

protocol PackableType { }

and make all types which can safely be packed and unpacked explicitly conform to it:

extension Int : PackableType { }
extension UInt16 : PackableType { }
// More integer types ...

extension Double : PackableType { }
// More floating point types ...

(Unfortunately, there are many integer types and I don't know if the repetition can be avoided.)

Now you can restrict the type <T> to <T : PackableType>. Alternatively, take a different approach and make the pack/unpack methods operate on the type itself by defining a protocol extension:

extension PackableType {

    func pack(byteOrder byteOrder: ByteOrder) -> [Byte] {
        var value = self
        let valueByteArray = withUnsafePointer(&value) {
            Array(UnsafeBufferPointer(start: UnsafePointer<Byte>($0), count: sizeofValue(value)))
        }
        return (byteOrder == ByteOrder.hostByteOrder) ? valueByteArray : valueByteArray.reverse()
    }

    static func unpack(valueByteArray: [Byte], byteOrder: ByteOrder) -> Self {
        let bytes = (byteOrder == ByteOrder.hostByteOrder) ? valueByteArray : valueByteArray.reverse()
        return bytes.withUnsafeBufferPointer {
            return UnsafePointer($0.baseAddress).memory
        }
    }
}

(Note how the compiler infers the type in return UnsafePointer($0.baseAddress).memory automatically.)

This would now be used as

let bytes = 12.34.pack(byteOrder: .LittleEndian)
let double = Double.unpack(bytes, byteOrder: .LittleEndian)

and solve your question "how to define the return type in unpack" in a different way.

\$\endgroup\$
  • \$\begingroup\$ There is one dangerous thing you should change. sizeofValue(value) (which is now MemoryLayout.size(ofValue:)) should be MemoryLayout<Self>.size. The thing is, if you cast to PackableType and execute pack(byteOrder:) you'll get a wrong result. For instance MemoryLayout.size(ofValue: UInt8(0) as Any) is 32 bytes instead of what you would expect. \$\endgroup\$ – DevAndArtist Oct 29 '16 at 20:28
  • \$\begingroup\$ @DevAndArtist: Thank you for the feedback. Note that conformance to PackableType is (intentionally) declared explicitly for all "simple" types (like integers and floating point values) which can safely be byte-copied to an array and back. You cannot call the pack method on UInt8(0) as Any. \$\endgroup\$ – Martin R Oct 29 '16 at 22:27
  • \$\begingroup\$ That's correct but if you'd try ([1, 0.5] as [PackageType]).map { $0.pack(byteOrder: .littleEndian) } you'll get an array of two arrays of bytes, but these two array will be 40 bytes long each. The result is clearly not what you'd expect ;) \$\endgroup\$ – DevAndArtist Oct 30 '16 at 6:08
  • \$\begingroup\$ @DevAndArtist: Sorry, but I cannot reproduce. ([1, 0.5, UInt16(0x1234), Float(0.5)] as [PackableType]).map { $0.pack(byteOrder: .LittleEndian) } gives [[1, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 224, 63], [52, 18], [0, 0, 0, 63]] with above code. Am I missing something? \$\endgroup\$ – Martin R Oct 30 '16 at 7:59
  • \$\begingroup\$ Hmm, forget about my last example. I run into that problem I mentioned yesterday, because in my codebase I'm reusing the global generic function that converts T to [Byte]. It seems like from within an extension we're getting the correct size of Self. swiftlang.ng.bluemix.net/#/repl/5815aec90a78bb5651913186 At the bottom when I just the bare function you'll see the wrong result I meant. Sorry for confusion. \$\endgroup\$ – DevAndArtist Oct 30 '16 at 8:28
3
\$\begingroup\$

I'm working my way through Martin's very useful answer. I have discovered one small simplification. The ByteOrder enum is not required. NSHostByteOrder can be used:

var bigEndian = true
if (NSHostByteOrder() == NS_LittleEndian) {
    bigEndian = false
}
\$\endgroup\$
  • \$\begingroup\$ I am not completely sure, if this is a good idea, because the enum NSByteOrder has three entries: enum NSByteOrder {NS_UnknownByteOrder, NS_LittleEndian, NS_BigEndian};. Are we sure that NS_UnknownByteOrder will never happen? \$\endgroup\$ – Michael Dorner Mar 24 '16 at 12:43
  • \$\begingroup\$ The enumeration is also used as an argument to the pack method to specify the destination byte order. \$\endgroup\$ – Martin R Oct 29 '16 at 22:31

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.