Help us understand the problem. What is going on with this article?

MemoryLayout and Data in Swift3

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

お粗末様でした。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away