Swift Digest
SE-0184 | Swift Evolution

Unsafe[Mutable][Raw][Buffer]Pointer: add missing methods, adjust existing labels for clarity, and remove deallocation size

Proposal
SE-0184
Authors
Diana Ma (“Taylor Swift”)
Review Manager
Doug Gregor
Status
Implemented (Swift 4.1)

01 何が問題だったのか

Swift の Unsafe[Mutable][Raw][Buffer]Pointer は低レベルなメモリ操作の窓口ですが、Swift 4.1 以前の API は、命名の不統一・欠けているメソッド・冗長なシグネチャ・実装と食い違うドキュメント、といった問題を抱えていました。結果として使いづらいだけでなく、安全に書いたつもりのコードでも実際にはメモリバグを埋め込んでしまう、という危うさがありました。

バッファ版に欠けているメソッドが多い

UnsafeMutableBufferPointer は「先頭アドレス + 要素数」を束ねて持てる便利な器であるにもかかわらず、UnsafeMutablePointer にある allocate / deallocate / initialize / assign といった API の多くが生えていませんでした。そのため、バッファを確保するだけでも baseAddresscount をいちいち取り出す必要があり、次のような定型コードが頻出していました。

let buffer = UnsafeMutableBufferPointer<UInt8>(
    start: UnsafeMutablePointer<UInt8>.allocate(capacity: byteCount),
    count: byteCount)

byteCount を 2 回書かされるうえ、一時的な UnsafeMutablePointer を噛ませないと UnsafeMutableBufferPointer を作れません。イミュータブル版とミュータブル版の相互変換も同様で、start:count: をわざわざ分解・再構築する必要がありました。

deallocate(capacity:) が危険

当時の UnsafeMutablePointer の解放 API は deallocate(capacity:) でした。名前からは「capacity 分だけ解放する(= realloc に近い縮小)」と読めますが、実装は capacity 引数を無視してブロック全体を free() するだけでした。ドキュメントすらこの動作を正確に反映しておらず、次のようなコードは一見合法に見えますが、実際には不正アクセスになります。

var ptr = UnsafeMutablePointer<UInt8>.allocate(capacity: 1000000)
ptr.initialize(to: 13, count: 1000000)
ptr.deallocate(capacity: 500000) // 後半だけ解放したつもり
ptr[0] // 実際には全体が解放されているのでセグフォ

挙動を知っているユーザは capacity:42 のような嘘の値を渡す書き方に流れてしまいがちで、将来「実装のほうを正しく直す」方向で修正されたら、コードは通るのに実行時だけ壊れるという、最悪の silent source break を引き起こしかねません。さらに UnsafeMutableRawPointer 側にも同様の deallocate(bytes:alignedTo:) があり、同じ問題を抱えていました。

引数ラベルの意味がぶれている

元々は「bytes は未初期化メモリ、capacity は未初期化の要素数、count は初期化済み要素数」という命名規則だったはずですが、実際の API ではこれが守られていませんでした。たとえば copyBytes(from:count:)count は(初期化の有無にかかわらず)バイト数を指しますし、UnsafeMutableRawBufferPointer.allocate(count:)count は未初期化のバイト数を指します。

また、to: ラベルが「型 T.Type」と「繰り返し値」の両方に使われているのも紛らわしく、initializeMemory<T>(as:at:count:to:) のように両者が 1 つのメソッド内に同居する場面では、どちらが型でどちらが値なのか一目ではわかりません。

at: オフセットがほぼ使われていない

UnsafeMutableRawPointer の一部メソッドには、stride 単位のオフセットを渡す at: 引数がありましたが、標準ライブラリ内で使われていないうえ、実用上もポインタ演算で代替できるため、残しておくほうが混乱のもとになっていました。

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

この提案では、Swift 4.1 でポインタ系 API 全体を整理し、バッファ版に欠けていたメソッドを追加し、サイズ付き deallocate を廃止し、紛らわしい引数ラベルを直す、という 3 方向の見直しを行います。ここでは partial initialization や Sequence 連携など、さらに大掛かりな設計が必要な領域は対象外で、allocate / deallocate / type rebind、および「繰り返し値による initialize / assign」に絞って手を入れます。

サイズ付き deallocate の廃止と全型への deallocate() 追加

まず、すべてのポインタ型(ミュータブル・イミュータブル・raw・buffer すべて)に、引数なしの deallocate() を追加します。挙動は「self が指すヒープブロック全体を解放する」で、free() と同じです。

let ptr = UnsafeMutablePointer<Int>.allocate(capacity: 100)
// ... 使う ...
ptr.deallocate() // ブロック全体を解放

同時に、実際には機能していなかった UnsafeMutablePointer.deallocate(capacity:) と、UnsafeMutableRawPointer.deallocate(bytes:alignedTo:) は deprecated にして将来的に削除します。capacity を部分解放と勘違いするバグ源がなくなり、API 名も空きます。将来、真の部分解放やリアロケートを導入するときに deallocate(capacity:)reallocate(toCapacity:) といった名前を安全に再利用できるよう、あえて今のうちに解放してリセットしておく、という位置づけです。

イミュータブル版(UnsafePointerUnsafeBufferPointer)にも deallocate() が追加されます。Swift のメモリモデル上、解放にミュータビリティは必要ないためです。

なお、バッファ版の deallocate() は、そのバッファがヒープブロック全体を丸ごと指しているときのみ定義された動作になります。スライスしたバッファを解放するような使い方は未定義です。

UnsafeMutableBufferPointer に allocate / deallocate / initialize / assign を追加

バッファ版にも、ポインタ版と同等の確保・解放・初期化・代入メソッドが生えます。

// 確保
let buffer = UnsafeMutableBufferPointer<UInt8>.allocate(capacity: byteCount)

// 繰り返し値で初期化
buffer.initialize(repeating: 0)

// 繰り返し値で代入(すでに初期化済みの領域に上書き)
buffer.assign(repeating: 0xFF)

// 解放
buffer.deallocate()

count を 2 回書かされていた定型コードが 1 行になり、一時的な UnsafeMutablePointer を噛ませる必要もなくなります。raw 版にも同様に、UnsafeMutableRawBufferPointer.allocate(byteCount:alignment:)initializeMemory(as:repeating:) / bindMemory(to:) が追加されます。

let raw = UnsafeMutableRawBufferPointer.allocate(
    byteCount: 1024, alignment: MemoryLayout<UInt>.alignment)
let typed: UnsafeMutableBufferPointer<Int32> = raw.initializeMemory(as: Int32.self, repeating: 0)

バッファ版の bindMemory(to:)initializeMemory(as:repeating:) は、新しい型の stride に合わせて要素数を整数除算で再計算します(元のバイト数から自動で割り出します)。

イミュータブルバッファとミュータブルバッファの相互変換

UnsafeMutableBufferPointerinit(mutating:) を、UnsafeBufferPointerinit(_:) を追加して、バッファ同士の mutable / immutable 変換が 1 ステップでできるようになります。UnsafeMutableRawBufferPointer 側にはすでに同種のイニシャライザがあったため、整合性の観点でも自然です。

引数ラベルの整理

紛らわしかったラベルが、次のように書き換わります。

  • UnsafeMutablePointer.initialize(to:count:)initialize(repeating:count:)(繰り返し値であることを明示)。単一要素の初期化には別途 initialize(to:)count なし)が用意されます。
  • UnsafeMutableRawPointer.initializeMemory(as:at:count:to:)initializeMemory(as:repeating:count:)at: を削除、to:repeating: に変更、引数順も整理)。
  • UnsafeMutableRawPointer.allocate(bytes:alignedTo:)allocate(byteCount:alignment:)
  • UnsafeMutableRawBufferPointer.allocate(count:)allocate(byteCount:alignment:)alignment: が新たに必須)。
  • copyBytes(from:count:) / copyBytes(from:)copyMemory(from:byteCount:) / copyMemory(from:)bytes / byteCount に揃え、ラベルの意味もはっきりさせる)。

なぜ value: ではなく repeating: なのか、という点については、Array.init(repeating:count:) と揃っていて「繰り返し値である」というニュアンスが出ること、そして value: だと ptr.initialize(value: value) のような見づらい呼び出しになりがちであることが理由です。

型リバインドのバッファ版

UnsafeBufferPointerUnsafeMutableBufferPointer にも withMemoryRebound(to:_:) が追加されます(クロージャ版、デコレータ形式)。単独ポインタ版と違って capacity: は不要で、新しい要素型の stride から必要な要素数が内部で計算されます。

let bytes: UnsafeBufferPointer<UInt8> = ...
let sum: Int = bytes.withMemoryRebound(to: Int32.self) { rebound in
    rebound.reduce(0) { $0 + Int($1) }
}

移行

ほとんどの変更はソース互換ですが、一部(copyBytescopyMemoryinitializeMemory のラベル整理、allocate のラベル変更、サイズ付き deallocate の廃止)はソース破壊的です。ただしマイグレータで機械的に置換可能な範囲に収まっており、UnsafeMutableRawBufferPointer.allocate の新しい alignment: には MemoryLayout<UInt>.stride を自動で埋めるといった対応が想定されています。