如何在Swift中创建打包数据结构?

时间:2014-06-10 10:51:08

标签: swift

我将项目从Objective-C转换为Swift,并且我使用压缩结构来输入通过套接字发送的转换二进制消息:

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;

我不确定Swift中最好的方法是什么,我能得到的最接近的是:

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.
}

翻译中丢失了两个重要的细节:没有保证整数类型的比特,并且没有结构打包。我不认为这可以在Swift中表达,但如果是这样,怎么样?

我对替代方法的建议持开放态度,例如:类似于Python的struct模块。

3 个答案:

答案 0 :(得分:9)

我开始编写一个以Python的struct模块为模型的Swift类,它可以在github上找到MVPCStruct

第一个原型的代码如下:

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

}

答案 1 :(得分:6)

我决定尝试这个,因为它看起来很有趣所以我想出了struct.pack(...)的解决方案。

您还可以对结构中的整数强制执行位大小:

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

现在进入打包结构...

整数扩展

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

试一试

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

备注

此示例非常简单,没有真正的错误或类型检查。此外,它并没有真正强制执行任何系统字节顺序(字节顺序),因此可能会出现问题。希望这对有兴趣的人来说有点起点。

对于解压缩,我注意到Swift允许返回一个可变大小的元组。例如:func unpack(format: String) -> (AnyObject...)没有给出编译警告。但是,我不知道你是怎么回事的。

答案 2 :(得分:2)

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

类似于Python的struct.pack()

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

类似于Python的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)