Swift Digest
SE-0210 | Swift Evolution

Add an offset(of:) method to MemoryLayout

Proposal
SE-0210
Authors
Joe Groff
Review Manager
Doug Gregor
Status
Implemented (Swift 4.2)

01 何が問題だったのか

グラフィックスや数値計算のライブラリでは、頂点バッファのように「この型のこのフィールドは、先頭から何バイト目に配置されているか」という情報を渡す必要が生じることがあります。C言語には offsetof マクロがあり、構造体のフィールドのオフセット(先頭からのバイト数)をコンパイラに計算させることができます。たとえば OpenGL の glVertexAttribPointer に頂点属性のレイアウトを伝えるために、次のように offsetof が使われます。

struct MyVertex {
  float position[4];
  float normal[4];
  uint16_t texcoord[2];
};

glVertexAttribPointer(Position, 4, GL_FLOAT, GL_FALSE,
                      sizeof(MyVertex), (void*)offsetof(MyVertex, position));

一方、Swift にはこれに相当する仕組みがありませんでした。そのため、この種の API を使う場合には該当部分を C で書くか、Swift 側で自前でメモリレイアウトを暗算する必要がありました。しかし stored property の配置は Swift コンパイラのレイアウトアルゴリズムに委ねられており、コンパイラ側でアルゴリズムが変わる可能性もあるため、手計算は誤りを生みやすく壊れやすい方法です。

Swift 4.0 で導入された key path(SE-0161)は、\MyVertex.position のように型のフィールドを値として参照する手段を提供します。これを使えば「どのフィールドか」を型安全に指し示せるため、フィールドのオフセットを問い合わせる API の自然な入力として利用できます。

02 どのように解決されるのか

MemoryLayout に、key path からフィールドのオフセットを取得する静的メソッド offset(of:) が追加されます。

extension MemoryLayout {
  public static func offset(of key: PartialKeyPath<T>) -> Int?
}

戻り値は Int? で、key path が指す先が「直接アドレス指定できる inline な stored property」であればバイト単位のオフセットを返し、そうでなければ nil を返します。「直接アドレス指定できる」とは、アクセス時に didSetwillSet が走ったり、ブリッジやクロージャの再抽象化のような表現変換が起きたり、ビットフィールドのようにマスク処理が必要になったりしない、という意味です。

使い方

次のように、ネストした stored property まで含めてオフセットを取得できます。

struct Point {
  var x, y: Double
}

struct Size {
  var w, h: Double

  var area: Double { return w*h }
}

struct Rect {
  var origin: Point
  var size: Size
}

MemoryLayout<Rect>.offset(of: \.origin.x) // => 0
MemoryLayout<Rect>.offset(of: \.origin.y) // => 8
MemoryLayout<Rect>.offset(of: \.size.w)   // => 16
MemoryLayout<Rect>.offset(of: \.size.h)   // => 24
MemoryLayout<Rect>.offset(of: \.size.area) // => nil(computed property なので nil)

返ってきたオフセットがノンnilであれば、次の二つの書き方は等価になります。つまりオフセットを経由した生ポインタ操作で、key path 経由の代入と同じ結果が得られます。

var root: T, value: U
var key: WritableKeyPath<T, U>

// key path による代入
root[keyPath: \.key] = value

// 上と等価な、offset を用いたポインタ経由の代入
withUnsafePointer(to: &root) {
  (UnsafeMutableRawPointer($0) + MemoryLayout<T>.offset(of: \.key))
    .assumingMemoryBound(to: U.self).pointee = value
}

対象となる key path

Swift 4.2 時点で offset(of:) が非nilを返すのは、struct の stored property を指す key path のみです。クラスのプロパティは常にアウトオブラインに格納され、アクセス時に排他性チェックが入るため、この API では取得できず nil が返ります。computed property も同様に nil を返します。

API resilience 上の注意

ライブラリ利用側がこの機能を悪用すると、公開プロパティが stored で実装されているか computed で実装されているかを外部から動的に判別できてしまいます。もし利用側が offset(of:) の戻り値を強制アンラップしてプロパティが常に stored であることを前提としてしまうと、ライブラリ側が将来そのプロパティを computed に変えたときに壊れます。自分の管理下にない型については、stored であることに依存したコードを書かないように注意してください。

Future Directions

Proposal では補助的な API として、UnsafePointer / UnsafeMutablePointer に key path サブスクリプトを追加してフィールドへのポインタを直接得る案にも触れています。こちらは今回のスコープ外ですが、offset(of:) を土台にした素直な拡張として今後検討される可能性があります(あくまで見通しであり、実現を約束するものではありません)。