How to create a packed data structure in Swift?

I am converting a project from Objective-C to Swift, and I use a packaged structure to input binary messages sent over a socket:

typedef struct { uint16_t version; // Message format version, currently 0x0100. uint32_t length; // Length of data in bytes. uint16_t reserved; // Reserved for future use. uint8_t data[]; // Binary encoded plist. } __attribute__((packed)) mma_msg_t; 

I'm not sure if the best approach in Swift, and the closest approximation I can get is:

 struct mma_msg { var version: CUnsignedShort // Message format version, currently 0x0100. var length: CUnsignedInt // Length of data in bytes. var reserved: CUnsignedShort // Reserved for future use. var data: CUnsignedChar[] // Binary encoded plist. } 

Two important details are lost in the translation: there is no guaranteed bit rate for integer types, and there is no packaging structure. I do not think this can be expressed in Swift, but if so, how?

I am open to suggestions for an alternative approach, for example. something similar to a Python struct module.

+6
source share
3 answers

I started writing the Swift class, modeled after the Python structural module, which can be found on github as MVPCStruct .

The code for the first prototype was as follows:

 enum Endianness { case littleEndian case bigEndian } // Split a large integer into bytes. extension Int { func splitBytes(endianness: Endianness, size: Int) -> UInt8[] { var bytes = UInt8[]() var shift: Int var step: Int if endianness == .littleEndian { shift = 0 step = 8 } else { shift = (size - 1) * 8 step = -8 } for count in 0..size { bytes.append(UInt8((self >> shift) & 0xff)) shift += step } return bytes } } extension UInt { func splitBytes(endianness: Endianness, size: Int) -> UInt8[] { var bytes = UInt8[]() var shift: Int var step: Int if endianness == .littleEndian { shift = 0 step = 8 } else { shift = Int((size - 1) * 8) step = -8 } for count in 0..size { bytes.append(UInt8((self >> UInt(shift)) & 0xff)) shift = shift + step } return bytes } } class Struct: NSObject { //class let PAD_BYTE = UInt8(0x00) // error: class variables not yet supported //class let ERROR_PACKING = -1 class func platformEndianness() -> Endianness { return .littleEndian } // Pack an array of data according to the format string. Return NSData // or nil if there an error. class func pack(format: String, data: AnyObject[], error: NSErrorPointer) -> NSData? { let PAD_BYTE = UInt8(0x00) let ERROR_PACKING = -1 var bytes = UInt8[]() var index = 0 var repeat = 0 var alignment = true var endianness = Struct.platformEndianness() // Set error message and return nil. func failure(message: String) -> NSData? { if error { error.memory = NSError(domain: "se.gu.it.GUStructPacker", code: ERROR_PACKING, userInfo: [NSLocalizedDescriptionKey: message]) } return nil } // If alignment is requested, emit pad bytes until alignment is // satisfied. func padAlignment(size: Int) { if alignment { let mask = size - 1 while (bytes.count & mask) != 0 { bytes.append(PAD_BYTE) } } } for c in format { // Integers are repeat counters. Consume and continue. if let value = String(c).toInt() { repeat = repeat * 10 + value continue } // Process repeat count values, minimum of 1. for i in 0..(repeat > 0 ? repeat : 1) { switch c { case "@": endianness = Struct.platformEndianness() alignment = true case "=": endianness = Struct.platformEndianness() alignment = false case "<": endianness = Endianness.littleEndian alignment = false case ">": endianness = Endianness.bigEndian alignment = false case "!": endianness = Endianness.bigEndian alignment = false case "x": bytes.append(PAD_BYTE) default: if index >= data.count { return failure("expected at least \(index) items for packing, got \(data.count)") } let rawValue: AnyObject = data[index++] switch c { case "c": if let str = rawValue as? String { let codePoint = str.utf16[0] if codePoint < 128 { bytes.append(UInt8(codePoint)) } else { return failure("char format requires String of length 1") } } else { return failure("char format requires String of length 1") } case "b": if let value = rawValue as? Int { if value >= -0x80 && value <= 0x7f { bytes.append(UInt8(value & 0xff)) } else { return failure("value outside valid range of Int8") } } else { return failure("cannot convert argument to Int") } case "B": if let value = rawValue as? UInt { if value > 0xff { return failure("value outside valid range of UInt8") } else { bytes.append(UInt8(value)) } } else { return failure("cannot convert argument to UInt") } case "?": if let value = rawValue as? Bool { if value { bytes.append(UInt8(1)) } else { bytes.append(UInt8(0)) } } else { return failure("cannot convert argument to Bool") } case "h": if let value = rawValue as? Int { if value >= -0x8000 && value <= 0x7fff { padAlignment(2) bytes.extend(value.splitBytes(endianness, size: 2)) } else { return failure("value outside valid range of Int16") } } else { return failure("cannot convert argument to Int") } case "H": if let value = rawValue as? UInt { if value > 0xffff { return failure("value outside valid range of UInt16") } else { padAlignment(2) bytes.extend(value.splitBytes(endianness, size: 2)) } } else { return failure("cannot convert argument to UInt") } case "i", "l": if let value = rawValue as? Int { if value >= -0x80000000 && value <= 0x7fffffff { padAlignment(4) bytes.extend(value.splitBytes(endianness, size: 4)) } else { return failure("value outside valid range of Int32") } } else { return failure("cannot convert argument to Int") } case "I", "L": if let value = rawValue as? UInt { if value > 0xffffffff { return failure("value outside valid range of UInt32") } else { padAlignment(4) bytes.extend(value.splitBytes(endianness, size: 4)) } } else { return failure("cannot convert argument to UInt") } case "q": if let value = rawValue as? Int { padAlignment(8) bytes.extend(value.splitBytes(endianness, size: 8)) } else { return failure("cannot convert argument to Int") } case "Q": if let value = rawValue as? UInt { padAlignment(8) bytes.extend(value.splitBytes(endianness, size: 8)) } else { return failure("cannot convert argument to UInt") } case "f", "d": assert(false, "float/double unimplemented") case "s", "p": assert(false, "cstring/pstring unimplemented") case "P": assert(false, "pointer unimplemented") default: return failure("bad character in format") } } } // Reset the repeat counter. repeat = 0 } if index != data.count { return failure("expected \(index) items for packing, got \(data.count)") } return NSData(bytes: bytes, length: bytes.count) } } 
+9
source

I decided to try this, since it seemed funny to me, so I came up with a solution for struct.pack(...) .

You can also apply the bit size for integers in your structure:

 struct MyMessage { var version : UInt16 var length : UInt32 var reserved : UInt16 var data : UInt8[] } 

Now on packed structures ...

Integer Extensions

  extension Int { func loByte() -> UInt8 { return UInt8(self & 0xFF) } func hiByte() -> UInt8 { return UInt8((self >> 8) & 0xFF) } func loWord() -> Int16 { return Int16(self & 0xFFFF) } func hiWord() -> Int16 { return Int16((self >> 16) & 0xFFFF) } } extension Int16 { func loByte() -> UInt8 { return UInt8(self & 0xFF) } func hiByte() -> UInt8 { return UInt8((self >> 8) & 0xFF) } } extension UInt { func loByte() -> UInt8 { return UInt8(self & 0xFF) } func hiByte() -> UInt8 { return UInt8((self >> 8) & 0xFF) } func loWord() -> UInt16 { return UInt16(self & 0xFFFF) } func hiWord() -> UInt16 { return UInt16((self >> 16) & 0xFFFF) } } extension UInt16 { func loByte() -> UInt8 { return UInt8(self & 0xFF) } func hiByte() -> UInt8 { return UInt8((self >> 8) & 0xFF) } } 

Data packer

 class DataPacker { class func pack(format: String, values: AnyObject...) -> String? { var bytes = UInt8[]() var index = 0 for char in format { let value : AnyObject! = values[index++] switch(char) { case "h": bytes.append((value as Int).loByte()) bytes.append((value as Int).hiByte()) case "H": bytes.append((value as UInt).loByte()) bytes.append((value as UInt).hiByte()) case "i": bytes.append((value as Int).loWord().loByte()) bytes.append((value as Int).loWord().hiByte()) bytes.append((value as Int).hiWord().loByte()) bytes.append((value as Int).hiWord().hiByte()) case "I": bytes.append((value as UInt).loWord().loByte()) bytes.append((value as UInt).loWord().hiByte()) bytes.append((value as UInt).hiWord().loByte()) bytes.append((value as UInt).hiWord().hiByte()) default: println("Unrecognized character: \(char)") } } return String.stringWithBytes(bytes, length: bytes.count, encoding: NSASCIIStringEncoding) } } 

To try

 let packedString = DataPacker.pack("HHI", values: 0x100, 0x0, 512) println(packedString) 

Notes

This example is extremely simple and has no real error or type checking. Furthermore, it does not actually use system byte order (endianness), so this can be problematic. Hope this will be the starting point for those interested.

For unpacking, I noticed that Swift allowed to return a tuple with a variable size. For example: func unpack(format: String) -> (AnyObject...) did not give a compilation warning. However, I have no idea how you will return something like that.

+6
source

https://github.com/nst/BinUtils/

Python complexity struct.pack ()

 let d = pack("<h2I3sf", [1, 2, 3, "asd", 0.5]) assert(d == unhexlify("0100 02000000 03000000 617364 0000003f")) 

Python complexity struct.unpack ()

 let a = unpack(">hBsf", unhexlify("0500 01 41 3fc00000")!) assert(a[0] as? Int == 1280) assert(a[1] as? Int == 1) assert(a[2] as? String == "A") assert(a[3] as? Double == 1.5) 
+2
source

Source: https://habr.com/ru/post/970614/


All Articles