I'm converting a project from Objective-C to Swift, and I'm using a packed struct to type cast 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 what the best approach is 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's no guaranteed bitsize of the integer types, and there's no structure packing. I don't think this can be expressed in Swift, but if so, how?
I'm open to suggestions of an alternative approach, e.g. something along the lines of Python's struct
module.
I started writing a Swift class modeled after Python's struct module, it can be found on github as MVPCStruct.
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's 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)
}
}
I decided to try this out since it seemed fun so I came up with a solution for struct.pack(...)
.
You can also enforce bit-size on integers in your struct:
struct MyMessage
{
var version : UInt16
var length : UInt32
var reserved : UInt16
var data : UInt8[]
}
Now on to the packed structures...
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) }
}
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)
}
}
let packedString = DataPacker.pack("HHI", values: 0x100, 0x0, 512)
println(packedString)
This example is extremely simple and has no real error or type checking. Also, it does not really enforce any system byte-order (endianness) so that may be problematic. Hopefully this is somewhat a starting point for those interested.
For unpacking I did notice that Swift allowed returning a variable-sized tuple. For example: func unpack(format: String) -> (AnyObject...)
did not give a compile warning. However, I have no idea how you'd return something like that.
https://github.com/nst/BinUtils/
Akin to Python's struct.pack()
let d = pack("<h2I3sf", [1, 2, 3, "asd", 0.5])
assert(d == unhexlify("0100 02000000 03000000 617364 0000003f"))
Akin to Python's 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)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With