今回は、普段の開発では触れる機会が少ない MemoryLayout とそれに関係した Data について書きます。
MemoryLayout 以前
Swift3.0 以前は、同じ役割を sizeof や strideof 等が担っていました。こちらの方が見覚えのある方は多いと思います。
MemoryLayout は以下の Proposal で提案されました。
Reconfiguring sizeof and related functions into a unified MemoryLayout struct
Motivation には
関数を一つのネームスペースにまとめることで検索性を向上させる。
といったことが書かれています。(LLVM の呼び出しにも触れられていますが、そこは調べられていません。)
MemoryLayout の概要
MemoryLayout は、それぞれの型に対するデータのメモリ上の配置に関する情報を取得することができます。情報は、size, stride, alignment の3種類があります。それぞれ
| 情報 | 説明 |
|---|---|
size |
一つの値が必要とするメモリ上のバイト数 |
stride |
配列中で一つの値が必要とするメモリ上のバイト数 |
alignment |
配列中の値のバイト数はこの値の整数倍である必要がある |
という内容です。alignment は特に分かりにくいですね。stackoverflow の以下の投稿にある Ray Fix さんの返信が理解の助けになってくれます。
stackoverflow - Swift: How to use sizeof?
サンプルコードの一部を引用します。
MemoryLayout<Foo>.size // returns 9
MemoryLayout<Foo>.stride // returns 16 because of alignment requirements
MemoryLayout<Foo>.alignment // returns 8, addresses must be multiples of 8
上から、
-
size-
Fooクラス単体ではメモリ上の9バイトを占める。
-
-
alignment- 要素が並んだ時、一つの要素が占めるメモリは
8バイトの倍数でなければならない。
- 要素が並んだ時、一つの要素が占めるメモリは
-
stride- 配列中で一つの要素が必要とするバイト数。これは
9バイトではなくalignmentの整数倍でなければならないので、要素は16バイトを必要とする。
- 配列中で一つの要素が必要とするバイト数。これは
という情報と読めます。
Data と値の相互変換
MemoryLayout を一部利用して、Data と値の変換に関する振る舞いを見てみましょう。
Int と Data を相互に変換する
以下の stackoverflow の投稿が参考になりました。
stackoverflow - round trip Swift number types to/from Data
・ Int → Data
var num: Int = 6
let data = Data(buffer: UnsafeBufferPointer(start: &num, count: 1))
// data: <06000000 00000000>
// 上のコメントアウト部は `data` の内容を16進数で表示したものです。
// NSDataにキャストして `print` することで表示することができます
・ Data → Int
let num: Int = data.withUnsafeBytes { $0.pointee }
// 6
withUnsafeBytes 関数は型推論やジェネリクス、pointee など要素が多く、理解が大変そうです。
・ [Int32] -> Data
var nums: [Int32] = [2, 11, 17]
let data = Data(buffer: UnsafeBufferPointer(start: nums, count: nums.count))
// data: <02000000 0b000000 11000000>
// 32ビットごとに 2, 11, 17 が入っていることが分かります。
データが32ビットでまとまっていると見やすいので Int32 にしています。nums の先頭から count(3) 個分の Int32 を変換です。
・ Data -> [Int32]
let values: [Int32] = data.withUnsafeBytes {
[Int32](UnsafeBufferPointer(start: $0, count: data.count/MemoryLayout<Int32>.stride))
}
// [2, 11, 17]
count のところは データ長/各要素のメモリ長 で要素数を計算しています。
簡単なデータは相互に変換できることが分かりました。String は少し方法が違いますが簡単に出来ます。
Structの場合を見てみましょう。
Struct と Data の相互変換を試みる
シンプルな要素を持つStruct
Int8 を要素としてもつ Struct Number を定義します。
struct Number {
let value: Int8
}
Data に変換して、続けて Data から Numbers にも変換します。
var value: Number = Number(value: 10)
let data = Data(buffer: UnsafeBufferPointer(start: &value, count: 1))
// value: 10
// <0a>
let restored_value: Number = data.withUnsafeBytes { $0.pointee }
// restored_value: 10
シンプル。データには 10 を表す <0a> しか見えません。
複雑な構造の要素を持つStruct
Int の配列を要素としてもつ Struct Numbers を定義します。
struct Numbers {
let values: [Int]
}
Data に変換して、続けて Data から Numbers にも変換します。
var value: Numbers = Numbers(values: [1, 2, 3])
let data = Data(buffer: UnsafeBufferPointer(start: &value, count: 1))
// value: [1, 2, 3]
// data: <40480700 00600000>
let restored_value: Numbers = data.withUnsafeBytes { $0.pointee }
// restored_value: [1, 2, 3]
無事できたようです。ですがデータの内容がさっきとだいぶ違いますね。
要素を変えながらデータを観察してみます。
var value: Numbers = Numbers(values: [1])
let data = Data(buffer: UnsafeBufferPointer(start: &value, count: 1))
// data: <a09d0500 00600000>
var value: Numbers = Numbers(values: [1, 2, 3, 4, 5])
let data = Data(buffer: UnsafeBufferPointer(start: &value, count: 1))
// data: <b0850900 80600000>
あれ、要素が増えても data のバイナリデータは大きくならない。。
試しに、Int の配列から Numbers を生成、データへ変換して返却する関数を定義、それを使って同じ処理の結果を見てみます。
func numbersData(from values: [Int]) -> Data {
var value: Numbers = Numbers(values: values)
return Data(buffer: UnsafeBufferPointer(start: &value, count: 1))
}
let data: Data = numbersData(from: [1, 2, 3])
let restored_value: Numbers = data.withUnsafeBytes { $0.pointee } // ここで error: SIGABRT !!!
落ちました。
おそらく大きさが可変な要素を持つ Struct は、バイナリとして値自体を保持しているのではなく、ポインタを保持しているようです。関数の中でデータの生成に使った配列データは、関数の外ではスコープ外になり、ポインタが指すアドレスの先にはアクセスできるデータはなくなってしまっているように見えます。
つまり、ここで得られたデータは有効なスコープ内でしか生きられない、__永続化の為には使えない__ということです。
そして、永続化の為に
ライブラリを作りました。
https://github.com/naru-jpn/pencil
標準的な Int, String, [Int], ... 等はもちろん、作成した Struct も簡単に永続化できます。(ちょっと Data の変換処理とかイケてない箇所が結構残ってますが)お時間のある時に覗いてみてください。
お粗末様でした。