AdventCalendar
iOS
Swift
Swift3.0
AdventCalendar2016
More than 1 year has passed since last update.

今回は、普段の開発では触れる機会が少ない MemoryLayout とそれに関係した Data について書きます。

API Reference - MemoryLayout

MemoryLayout 以前

Swift3.0 以前は、同じ役割を sizeofstrideof 等が担っていました。こちらの方が見覚えのある方は多いと思います。

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 と値の変換に関する振る舞いを見てみましょう。

IntData を相互に変換する

以下の stackoverflow の投稿が参考になりました。
stackoverflow - round trip Swift number types to/from Data

IntData

var num: Int = 6
let data = Data(buffer: UnsafeBufferPointer(start: &num, count: 1))
// data: <06000000 00000000>
// 上のコメントアウト部は `data` の内容を16進数で表示したものです。
// NSDataにキャストして `print` することで表示することができます

DataInt

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 の変換処理とかイケてない箇所が結構残ってますが)お時間のある時に覗いてみてください。

お粗末様でした。