Swift Digest
SE-0349 | Swift Evolution

Unaligned Loads and Stores from Raw Memory

Proposal
SE-0349
Authors
Guillaume Lessard, Andrew Trick
Review Manager
John McCall
Status
Implemented (Swift 5.7)

01 何が問題だったのか

バイナリファイルやネットワークから受け取ったバイト列には、メモリアライメントの制約がそのまま反映されているとは限りません。一方で、Swift の UnsafeRawPointer.load(fromByteOffset:as:) は、読み込み先のアドレスが型 T に対して正しくアライメントされていることを要求し、アライメントがずれていると実行時にクラッシュします。

たとえば、ストリーム中のバイトオフセット 3 から 7 に 4 バイト値が格納されているデータを UInt32 として読み出したい場合、次のように書きたくなります。

let data = Data([0x0, 0x0, 0x0, 0xff, 0xff, 0xff, 0xff, 0x0])
let result = data.dropFirst(3).withUnsafeBytes { $0.load(as: UInt32.self) }

しかし dropFirst(3) によって先頭がオフセット 3 にずれており、UInt32 の 4 バイトアライメントを満たさないため、これは実行時にクラッシュします。そのため、これまでは一度正しくアライメントされた一時領域にバイトをコピーしてから読み出すという迂回策が必要でした。

let result = data.dropFirst(3).withUnsafeBytes { buffer -> UInt32 in
  var storage = UInt32.zero
  withUnsafeMutableBytes(of: &storage) {
    $0.copyBytes(from: buffer.prefix(MemoryLayout<UInt32>.size))
  }
  return storage
}

この回避策は発見しにくい上に、本来 1 回で済むはずのコピーを 2 回行うことになり非効率でもあります。

また、書き込み側の UnsafeMutableRawPointer.storeBytes(of:toByteOffset:) も、ドキュメント上は trivial 型(参照カウントを伴わずビット単位でコピーできる型)にしか意味がないと説明されている一方で、実行時には書き込み先のオフセットが型 T に対してアライメントされているかどうかまでチェックしていました。バイト列として扱う用途では、この実行時のアライメントチェックが余計な制約になっていました。

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

アライメントに依らずに読み書きするための API を、UnsafeRawPointer / UnsafeMutableRawPointer とそれらの buffer 版に追加・整理します。

loadUnaligned(fromByteOffset:as:) の追加

UnsafeRawPointerUnsafeMutableRawPointer、および UnsafeRawBufferPointer / UnsafeMutableRawBufferPointer に、アライメントを要求しない loadUnaligned(fromByteOffset:as:) を追加します。対象は trivial 型(参照カウントなどの間接参照を持たず、ビット単位でコピーできる型。ネイティブの数値型や、インポートされた C の struct / enum など)に限定され、デバッグビルドではこの前提のチェックが走ります。

先ほどの例は、そのまま 1 回のコピーで書けるようになります。

let data = Data([0x0, 0x0, 0x0, 0xff, 0xff, 0xff, 0xff, 0x0])
let result = data.dropFirst(3).withUnsafeBytes {
    $0.loadUnaligned(as: UInt32.self)
}

既存の load(fromByteOffset:as:) はそのまま残ります。こちらは依然としてアライメントが必要で、別の生きているオブジェクトの中身を読み出すようにアライメントが構造的に保証されているケースで使う、という役割分担です。Rust の read / read_unaligned のように、デフォルト(短い名前)側をより厳格で高速なほうに割り当てる方針になっています。

storeBytes(of:toByteOffset:as:) の挙動変更

UnsafeMutableRawPointer.storeBytes(of:toByteOffset:as:) および UnsafeMutableRawBufferPointer.storeBytes(of:toByteOffset:as:) は、シグネチャはそのままに、実行時のアライメントチェックを廃止します。代わりに、ドキュメント通り「trivial 型であること」だけが要件として残ります(デバッグビルドでチェックされます)。これにより、任意のバイトオフセットへ trivial な値のバイト列を書き込めるようになります。

バイナリ互換性のため、古い(アライメントを要求する)シンボルは従来どおり残されます。新しい挙動の API には @_alwaysEmitIntoClient が付与されるため、古い OS にデプロイしても新しい挙動でバックデプロイされます。

命名について

読み出し側を loadUnaligned と呼ぶ一方で、書き込み側を storeUnaligned のように揃えなかったのは意図的です。load は読み出した結果を Swift ランタイムが管理する新しい値として生成するのに対し、storeBytes は書き込み先がランタイムに管理されない単なるバイト列です。この非対称性を反映して、書き込み側の名前には引き続き “Bytes” が含まれています。