💀

Swiftで書いたMessagePackライブラリの紹介

2022/08/06に公開

MessagePackライブラリのswift-msgpackをgithubに公開しました。
https://github.com/nnabeyang/swift-msgpack

ここでは、swift-msgpackについて紹介します。このライブラリはCodableを利用したエンコーダ&デコーダのため、標準ライブラリのJSONEncoder/JSONDecooderと同様に出力をカスタムすることが出来ます。

arrayにするかmapにするか

swiftのstructかclassをMessagePackにエンコードすると、arrayにするかmapにするか任意性が存在します。swiftコンパイラが自動生成するCodableのコードはmapにしますが、自分でコードを書くことでarrayにすることもできます。まずmapの場合のコードを次に示します。

main.swift
struct Pair: Codable, Equatable {
    var X: Int
    var Y: Int
}

let input = Pair(X: 1, Y: 2)

let data = try! MsgPackEncoder().encode(input)
let out = try! MsgPackDecoder().decode(Pair.self, from: data)

print(data.hexDescription)// => "82a15801a15902"
assert(out == input)

arrayにエンコードし、arrayからデコードできるように書き換えてみます。

struct Pair: Equatable {
    var X: Int
    var Y: Int
}

extension Pair: Codable {
    init(from decoder: Decoder) throws {
        var container = try decoder.unkeyedContainer()
        X = try container.decode(Int.self)
        Y = try container.decode(Int.self)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.unkeyedContainer()
        try container.encode(X)
        try container.encode(Y)
    }
}

mapのキーの名前を変更する

これはJSONEncoder/JSONDecoderの場合と同じですが、次のようにします。

struct Pair: Codable, Equatable {
    var X: Int
    var Y: Int

    private enum CodingKeys: String, CodingKey {
        case X = "x"
        case Y = "y"
    }
}

Timestamp

MessagePackには拡張型としてTimestampが定義されています。拡張型は拡張タイプという番号で識別されます。MessagePackとして定義されるものは-1から-128が割り当て可能で、アプリケーション独自に使える拡張タイプは0から127です。Timestampの拡張タイプは-1です。
MsgPackTimestampとして定義しています。Dateへの変換はライブラリ側としては用意していませんが、必要に応じてDateのextensionとして定義すれば良いと考えています。簡単な例は次の通りです。

let format = ISO8601DateFormatter()
format.timeZone = .init(identifier: "UTC")
format.formatOptions.insert(.withFractionalSeconds)
let date = format.date(from: "2022-06-22T08:56:32.999Z")!
let timeInterval = date.timeIntervalSince1970
let seconds = Int64(timeInterval)
let nanoseconds = Int32(timeInterval.truncatingRemainder(dividingBy: 1) * 1_000_000_000)
let input = MsgPackTimestamp(seconds: seconds, nanoseconds: nanoseconds)
print(input)// => MsgPackTimestamp(seconds: 1655888192, nanoseconds: 999000072)
let data = try! MsgPackEncoder().encode(input)
let out = try! MsgPackDecoder().decode(MsgPackTimestamp.self, from: data)
print(data.hexDescription) // => "d7ffee2e202062b2d940"
assert(out == input)

拡張型の定義

MsgPackEncodableプロトコルを実装するとエンコードできるようになり、MsgPackDecodableを実装するとデコードできるようになります。MsgPackCodableMsgPackEncodable & MsgPackDecodableのエイリアスになっています。また、上で説明したように拡張タイプ番号を決める必要があります。

public protocol MsgPackDecodable: Decodable {
    var type: Int8 { get }
    init(msgPack data: Data) throws
}

public protocol MsgPackEncodable: Encodable {
    func encodeMsgPack() throws -> Data
    var type: Int8 { get }
}

簡単な例を挙げておきます。

struct Position: MsgPackCodable, Equatable {
    init(msgPack data: Data) throws {
        let a = [UInt8](data)
        x = a[0]
        y = a[1]
    }

    private var x: UInt8
    private var y: UInt8
    init(x: UInt8, y: UInt8) {
        self.x = x
        self.y = y
    }

    func encodeMsgPack() throws -> Data {
        .init([x, y])
    }

    var type: Int8 { 1 }
}

let input: Position = .init(x: 34, y: 78)
let data = try! MsgPackEncoder().encode(input)
let out = try! MsgPackDecoder().decode(Position.self, from: data)
print(data.hexDescription) // => "d501224e"
assert(out == input)

サブクラスの場合

クラスをCodableにして、さらにスーパークラスがある場合はsuperDecoder/superEncoderを呼び出すと、うまく変換できます。この辺りはCodableを使ったEncoder/Decoder一般に言えることですが、このようなケースにも対応しています。

class Animal: Codable {
    var legCount: Int

    init(legCount: Int) {
        self.legCount = legCount
    }

    func equals(rhs: Animal) -> Bool {
        return legCount == rhs.legCount
    }
}

extension Animal: Equatable {
    static func == (lhs: Animal, rhs: Animal) -> Bool {
        return lhs.equals(rhs: rhs)
    }
}

class Pet: Animal {
    var name: String

    private enum CodingKeys: String, CodingKey {
        case name
    }

    required init(legCount: Int, name: String) {
        self.name = name
        super.init(legCount: legCount)
    }

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        name = try container.decode(String.self, forKey: .name)
        let superDecoder = try container.superDecoder()
        try super.init(from: superDecoder)
    }

    override func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(name, forKey: .name)
        let superEncoder = container.superEncoder()
        try super.encode(to: superEncoder)
    }

    override func equals(rhs: Animal) -> Bool {
        guard let rhs = rhs as? Pet else { return false }
        return super.equals(rhs: rhs) && name == rhs.name
    }
}
let input = Pet(legCount: 4, name: "siro")

let data = try! MsgPackEncoder().encode(input)
let out = try! MsgPackDecoder().decode(Pet.self, from: data)

print(data.hexDescription) // => "82a46e616d65a47369726fa5737570657281a86c6567436f756e7404"
assert(out == input)

Directory

DirectoryのキーがCodableであれば、mapにエンコーディングされます。

    let input: [Double: String] = [3.14159265359: "pi", 1.41421356237: "sqrt(2)"]

    let data = try! MsgPackEncoder().encode(input)
    let out = try! MsgPackDecoder().decode([Double: String].self, from: data)

    print(data.hexDescription) // "82cb400921fb54442eeaa27069cb3ff6a09e667f055aa773717274283229"
    assert(input == out)

structでnilを含む場合

swift的にはnilの場合はデコードの時に使わないので、エンコードでも何も出力しないという選択肢があります。Codableを使ったエンコーダ&デコーダの標準的な動作としてもそのようになっていますが、出力が必要な言語と通信している場合は詰んでしまうのであえて出力するようにしています。この辺りはオプションにできると良いかもしれません。

struct NilArray: Codable, Equatable {
    var a: [String]?
}

let input = NilArray(a: nil)

let data = try! MsgPackEncoder().encode(input)
let out = try! MsgPackDecoder().decode(NilArray.self, from: data)

print(data.hexDescription) // == "81a161c0"
assert(input == out)

Discussion