Add an offset(of:) method to MemoryLayout
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 を返します。「直接アドレス指定できる」とは、アクセス時に didSet や willSet が走ったり、ブリッジやクロージャの再抽象化のような表現変換が起きたり、ビットフィールドのようにマスク処理が必要になったりしない、という意味です。
使い方
次のように、ネストした 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:) を土台にした素直な拡張として今後検討される可能性があります(あくまで見通しであり、実現を約束するものではありません)。