弊社では、一部のサービスでMessagePackを利用しています。
Swift製のMessagePackエンコーダー/デコーダーのライブラリは存在するのですが、Swift4から利用できるCodableに対応した、いい感じのライブラリがなかったので、Codableに対応したMessagePackのエンコーダー/デコーダーを作ってみました。
この記事では、MessagePackについて軽く説明した後、Codableに対応したカスタムエンコーダー/デコーダーを実装する上で得た知見を共有できればと思っています。
MessagePackとは
MessagePackは性能を重視したバイナリベースのシリアライズ形式です。
JSONのように汎用的なシリアライズ形式でありつつ、バイナリベースで高速です。
MessagePackの特徴
- シリアライズ/デシリアライズがとても高速
- シリアライズされたデータのサイズが小さい
- フォーマット定義(IDL)が不要
- ストリーム処理できる
MessagePackの仕様
- 基本的にFirst byteがデータ型を表し、後ろにデータが続く形式
- バイトオーダーはビックエンディアン
詳しい仕様はGithubに上がっていますので興味がある方はこちらを参照ください。
https://github.com/msgpack/msgpack/blob/master/spec.md
Codableとは
Swift4から採用されたプロトコルで、Codableに準拠するとエンコード/デコードが可能であるということを宣言でき、コンパイラが必要なコードを自動で補完してくれます。
Codableは、EncodableとDecodableのtypealiasとして定義されています。
typealias Codable = Encodable & Decodable
protocol Encodable {
func encode(to encoder: Encoder) throws
}
protocol Decodable {
init(from decoder: Decoder) throws
}
Codableのメリット
Codableに準拠している型は、Codableに対応したエンコーダー/デコーダーを利用するとエンコード/デコード処理が容易になります。
Swiftには標準で JSONEncoder
, JSONDecoder
が存在します。
let input = Landmark(name: "Mojave Desert")
// Landmark -> JSON
let data = try! JSONEncoder().encode(input)
// JSON -> Landmark
let landmark = try! JSONDecoder().decode(Landmark.self, from: data)
また、シリアライズ形式が変わった場合は、エンコーダー/デコーダーを変えるだけで対応可能です。
let input = Landmark(name: "Mojave Desert")
// Landmark -> MessagePack
let data = try! MessagePackEncoder().encode(input)
// MessagePack -> Landmark
let landmark = try! MessagePackDecoder().decode(Landmark.self, from: data)
カスタムエンコーダー/デコーダーの基本
ここからは、カスタムエンコーダー/デコーダーを実装するための基本的な部分を見ていきます。
エンコーダーとデコーダーは似たような形になるので、ここではエンコーダーを例にあげます。
エンコーダーの実装に必要なプロトコル
エンコーダーを実装するにあたり、次のプロトコルに準拠する型を定義する必要があります。
- Encoder
- CodingKey
- KeyedEncodingContainer(KeyedEncodingContainerProtocol)
- UnkeyedEncodingContainer
- SingleValueEncodingContainer
KeyedEncodingContainer
は、正確にいうとプロトコルではありません。
これは、KeyedEncodingContainer
が準拠している KeyedEncodingContainerProtocol
がassociatedtypeを持つため、Type Erasureが使われています。
KeyedEncodingContainer
の型を実装する際は、実質 KeyedEncodingContainerProtocol
に準拠する形になります。
public protocol Encoder {
public var codingPath: [CodingKey] { get }
public var userInfo: [CodingUserInfoKey : Any] { get }
public func container<Key>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> where Key : CodingKey
public func unkeyedContainer() -> UnkeyedEncodingContainer
public func singleValueContainer() -> SingleValueEncodingContainer
}
Encoder
のメソッドはエンコード対象の型により呼ばれるメソッドが変わります。
よって、使用される XXXEncodingContainer
プロトコルは各型と次のような対応関係にあります。
Protocol | Type |
---|---|
KeyedEncodingContainer | Dictionary |
UnkeyedEncodingContainer | Array |
SingleValueEncodingContainer | Optional, Bool, String, Double, Float, Int, Int8, Int16, Int32, Int64, UInt, UInt8, UInt16, UInt32, UInt64 And Custom type |
これらのプロトコルに準拠した型を定義し、エンコード処理を書いていくのが基本的な実装になります。
また、 必須ではないですが、標準のJSONEncoderやPropertyListEncoderを見るとエンコードしたデータを保持しておく XXXStrage
も必要になってくるのがわかります。
詳しい実装内容は、それらのEncoderやMessagePackEncoderの実装を見てみてください。
カスタムエンコーダー/デコーダー実装時に注意が必要な点
エンコーダーの対象外な型(Date/Data型など)
現在エンコーダーで対応できる型は次のようになっています。
Type |
---|
Bool |
String |
Double |
Float |
Int |
Int8 |
Int16 |
Int32 |
Int64 |
UInt |
UInt8 |
UInt16 |
UInt32 |
UInt64 |
(要素がCodableに準拠している場合の Dictionary
, Array
, Optional
)
ですが、Date/Data型はCodableに準拠しているのでエンコード/デコード可能です。
なぜでしょうか?
実装をみてみましょう。
extension Date : Codable {
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(self.timeIntervalSinceReferenceDate)
}
}
Date
型の encode
では、timeIntervalSinceReferenceDate
がエンコード対象となっています。
timeIntervalSinceReferenceDate
は、 TimeInterval
型で TimeInterval
は Double
型のエイリアスなので、実質 Double
として処理されます。
Data
型の実装もみてみましょう。
extension Data : Codable {
var container = encoder.unkeyedContainer()
try withUnsafeBytes { (buffer: UnsafeRawBufferPointer) in
try container.encode(contentsOf: buffer)
}
}
Data
型では、バイト配列に変換され、UnkeyedEncodingContainer
が使用されています。
よってData
型は、 [UInt8]
として処理されます。
これが意図した結果であればいいのですが、違う処理に変えたい場合は一工夫必要です。
JSONEncoder
では Date/Data
型にキャストし、それぞれ適切な処理に通しています。
fileprivate func box(_ date: Date) throws -> NSObject {
...
}
fileprivate func box(_ data: Data) throws -> NSObject {
...
}
fileprivate func box_<T : Encodable>(_ value: T) throws -> NSObject? {
if T.self == Date.self {
return try self.box((value as! Date))
} else if T.self == Data.self {
return try self.box((value as! Data))
}
}
DictionaryのキーがString or Int型じゃない場合
Swiftでは、Hashable
に準拠していれば Dictionary
のキーとして利用することができます。
この場合エンコード処理では、String
or Int
の場合と挙動が変わってきます。
public func encode(to encoder: Encoder) throws {
if Key.self == String.self {
var container = encoder.container(keyedBy: _DictionaryCodingKey.self)
for (key, value) in self {
let codingKey = _DictionaryCodingKey(stringValue: key as! String)!
try container.encode(value, forKey: codingKey)
}
} else if Key.self == Int.self {
var container = encoder.container(keyedBy: _DictionaryCodingKey.self)
for (key, value) in self {
let codingKey = _DictionaryCodingKey(intValue: key as! Int)!
try container.encode(value, forKey: codingKey)
}
} else {
var container = encoder.unkeyedContainer()
for (key, value) in self {
try container.encode(key)
try container.encode(value)
}
}
}
}
https://github.com/apple/swift/blob/master/stdlib/public/core/Codable.swift.gyb#L1820-L1857
String
or Int
の場合は、 KeyedEncodingContainer
が使われているのに対し、それ以外では UnkeyedEncodingContainer
が使用されています。
この辺を理解していないと意図した挙動をしない場合があるかもしれません。
superEncoder
KeyedEncodingContainer
と UnkeyedEncodingContainer
に準拠した型には、 superEncoder
メソッドを実装する必要があります。
これは継承関係にある型をエンコードする際に利用します。
class Animal: Codable {
var legCount: Int
private enum CodingKeys: String, CodingKey {
case legCount
}
init(legCount: Int) {
self.legCount = legCount
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(legCount, forKey: .legCount)
}
}
class Dog: Animal {
var name: String
private enum CodingKeys: String, CodingKey {
case name
}
required init(legCount: Int, name: String) {
self.name = name
super.init(legCount: legCount)
}
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)
}
}
自分が実装する際に superEncoder
がどのような場面で利用されるのかよくわからなかったので、???って感じだったのですが、このstack overflowのページで理解することができました
さいごに
この記事が、カスタムエンコーダー/デコーダーを実装する際の手助けになれば幸いです。
MessagePackerを実装するにあたり、JSONEncoderとMoreCodableの実装をとても参考にさせていただきました。ありがとうございます
もしiOSでMessagePackを利用する機会があれば、是非 MessagePackerを検討してみてください