Lilliput

Lilliput 是一个原生的 Swift 框架,用于处理二进制数据。

设计决策

基本用法

import Lilliput

// Allocate enough storage for a 32-bit unsigned integer
let buffer = ByteBuffer(count: 4)
try buffer.slice(...) { bytes in
    // Write a UInt32 to that storage in little endian encoding
    var writer = ByteSpanWriter(bytes)
    try writer.write(UInt32(100), as: UInt32.LittleEndian.self)

    // Read a UInt32 from that stroage in little endian encoding 
    var reader = ByteSpanReader(bytes)
    let value = try reader.read(UInt32.LittleEndian.self)
}

自定义的字节可解码/可编码类型

struct MyType {
    var value0: Int
    var value1: Int8
    var value3: Double
}

extension MyType: ByteDecoder {
    static func decode<R: Reader>(from reader: inout R) throws -> MyType {
        return MyType(
            value0: Int(try reader.read(UInt32.LittleEndian.self)),
            value1: try reader.read(Int8.self),
            value2: try reader.read(Float64.LittleEndian.self)
        )
    }
}

extension MyType: ByteEncoder {
    static func encode<W: Writer>(_ value: MyType, to writer: inout W) throws {
        try writer.write(UInt32(value.value0), as: UInt32.LittleEndian.self)
        try writer.write(value.value1)
        try writer.write(value.value2, as: Float64.LittleEndian.self)
    }
}

同一类型有多种解码/编码方式

extension MyType {
    @frozen enum Custom {}
}

extension MyType.Custom: ByteDecoder {
    static func decode<R: Reader>(from reader: inout R) throws -> MyType {
        // Values are loaded/stored in reverse order from the type definition and using big endian
        let value2 = try reader.read(Float64.BigEndian.self)
        let value1 = try reader.read(Int8.self)
        try reader.alignTo(4) // Make sure next read occurs after three padding bytes
        let value0 = Int(try reader.read(UInt32.BigEndian.self))
         
        return MyType(
            value0: value0,
            value1: value1,
            value2: value2
        )
    }
}

extension MyType.Custom: ByteEncoder {
    static func encode<W: Writer>(_ value: MyType, to writer: inout W) throws {
        // Values are loaded/stored in reverse order from the type definition and using big endian
        try writer.write(value.value2, as: Float64.BigEndian.self)
        try writer.write(value.value1)
        try writer.write((0, 0, 0), as: UInt8.Tuple3.self) // Add three padding bytes
        try writer.write(UInt32(value.value0), as: UInt32.BigEndian.self)
    }
}

从磁盘读取类型

import Lilliput
import SystemPackage

let file = try FileDescriptor.open(path, .readOnly)
let buffer = ByteBuffer(count: 13)
let myType = try buffer.slice(...) { bytes in
    try file.readAll(into: bytes)
    var reader = ByteSpanReader(bytes)
    return try reader.read(MyType.self)
}

如果数据是使用 Custom 编码存储在磁盘上

return try reader.read(MyType.Custom.self)

将类型写入磁盘

import Lilliput
import SystemPackage

let myType = MyType(value0: 100, value1: 8, value2: 300.56)
let buffer = ByteBuffer(count: 13)
try buffer.slice(...) { bytes in
    var writer = SpanByteWriter(buffer)
    try writer.write(myType)
}
let file = try FileDescriptor.open(path, .writeOnly)
file.writeAll(buffer)

如果我们想使用 Custom 编码存储数据

try writer.write(myType, as: MyType.Custom.self)

读取可解码类型的数组

let count = Int(try reader.read(UInt32.LittleEndian.self))
let arrayOfMyType = try reader.read(Element<MyType>.self, count: count)

写入可编码类型的数组

try writer.write(UInt32(arrayOfMyType.count), as: UInt32.LittleEndian.self)
try writer.write(arrayOfMyType, as: Element<MyType>.self)

自定义数组解码/编码

注意:由于关于如何编码计数器值的决定在不同用例中可能差异很大,因此该功能未内置到库中。

@frozen struct MyArray<E> {}

extension MyArray: ByteDecoder where E: ByteDecoder {
    static func decode<R: Reader>(from reader: inout R) throws -> [E.Decodable] {
        let count = Int(try reader.read(UInt32.LittleEndian.self))
        return try reader.read(Element<E>.self, count: count)
    }
}

extension MyArray: ByteEncoder where E: ByteEncoder {
    static func encode<W: ByteWriter>(_ value: [E.Encodable], to writer: inout W) throws {
        try writer.write(UInt32(value.count), as: UInt32.LittleEndian.self)
        
        for element in value {
            try writer.write(element, as: E.self)
        }
    }
}

用法(请注意,计数器值现在无需额外工作即可处理)

let arrayOfMyType = try reader.read(MyArray<MyType>.self)

try writer.write(arrayOfMyType, as: MyArray<MyType>.self)

Foundation 集成

let data = Data(...) // Get data somehow
var reader = DataReader(data: data, maxReadCount: 8)
let myType = try reader.read(MyType.self)

maxReadCount 选择一个值,该值表示一次调用 read 中读取的最大字节数。 在上面的示例中,最长的读取是 Double,它是 8 个字节。 在最坏的情况下,您可以省略 maxReadCount 参数,然后 Data 的整个长度将用作默认值。

版本 12.0.0+ API 中的小陷阱

ByteReader 上的所有 read 方法和 ByteWriter 上的所有 write 方法,如果它们不涉及 ByteDecoder/ByteEncoder,则不再检查是否有足够的字节来完成操作。 现在,这通过调用 ensure 单独处理。 例如,如果没有足够的字节可以读取,try reader.ensure(5) 将抛出异常。

此更改的副作用是,当您写入单个 UInt8 时,它不再触发 ByteEncoderUInt8 实现。

以前你可以这样做,如果空间不足以写入,它会抛出异常

try writer.write(UInt8(7))

现在 ByteWriter 上新的接受单个 UInt8write 方法取代了它。 要获得新行为,请执行以下操作

try ensure(1)
writer.write(UInt8(7))

或执行此操作,手动触发 ByteEncoder 实现

try writer.write(UInt8(7), as: UInt8.self)  

添加 Lilliput 作为依赖项

要在 SwiftPM 项目中使用 Lilliput 库,请将以下行添加到 Package.swift 文件中的依赖项中

.package(url: "https://github.com/jkolb/Lilliput.git", from: "12.0.0"),

最后,将 "Lilliput" 作为目标的依赖项包含进来

let package = Package(
    // name, platforms, products, etc.
    dependencies: [
        .package(url: "https://github.com/jkolb/Lilliput.git", from: "12.0.0"),
        // other dependencies
    ],
    targets: [
        .target(name: "MyTarget", dependencies: [
            .product(name: "Lilliput", package: "Lilliput"),
        ]),
        // other targets
    ]
)

许可

Lilliput 在 MIT 许可下可用。 有关更多信息,请参见 LICENSE 文件。