Swift Digest
SE-0447 | Swift Evolution

Span: Safe Access to Contiguous Storage

Proposal
SE-0447
Authors
Guillaume Lessard, Michael Ilseman, Andrew Trick
Review Manager
Doug Gregor
Status
Implemented (Swift 6.2)

01 何が問題だったのか

C 系言語では、ポインタと長さを渡すだけで任意の関数と連続メモリを共有できます。ヒープ上の配列、struct の連続フィールド、スタック上の値まで、容器の種類を問わず同じ関数で処理できるのが大きな利点です。Swift でも同じことをしたいのですが、単純に真似るとメモリ安全性を損ねてしまいます。

現状、連続メモリ上のデータを性能を落とさずに関数間で受け渡す手段は大きく 2 つあります。

  • some Sequence<UInt8> のようなジェネリックな抽象を受け取る
  • UnsafeBufferPointerwithUnsafeBufferPointer(_:) のクロージャ経由で受け取る

前者は抽象化コストが大きく、実用的な性能を出すには @inlinable にするか、内部で withContiguousStorageIfAvailable() を使って unsafe な高速パスを書くことになります。後者の UnsafeBufferPointer は高速ですが、次のような危険性があります。

  • ポインタ自身が unsafe で、寿命の管理もされません。
  • subscript の境界チェックはデバッグビルドでしか行われません。
  • クロージャのスコープを越えてエスケープさせてしまうことが可能です。

さらに、withUnsafeXXX のクロージャ内でポインタを直接エスケープさせなくても、その中から呼ぶヘルパー関数は unsafe なポインタ型を引数に取らざるを得ず、プロジェクト全体に unsafe な型が伝播していきます。本来は安全に書けるはずのコードまで unsafe 前提になってしまう、というのが現状の問題です。

たとえば base64 デコードライブラリが入力データをどの型で受け取るかを考えてみます。呼び出し側は [UInt8]Foundation.DataString など様々な形で連続メモリを持っています。ライブラリがそれらすべてから高速に読めるようにするには、結局 UnsafeBufferPointer を経由することになり、そのぶん安全性を手放すことになります。欲しいのは、some Sequence<UInt8> のような抽象性と UnsafeBufferPointer のような性能を両立し、しかも安全な「連続メモリのビュー」です。

この要件を満たすには、連続メモリを借用(borrow)としてだけ渡し、元の容器が生きている間だけ有効で、外へ持ち出せない型が必要になります。SE-0446 で導入された ~Escapable が、まさにこの「スコープを越えて持ち出せない値型」を表現するための土台です。Span はその上に構築される、容器に依存しない安全な連続メモリビューとして提案されます。

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

標準ライブラリに、連続メモリへの安全な借用ビューとして Span<Element> と、その型消去版にあたる RawSpan の 2 種類の型を導入します。いずれも Copyable かつ ~Escapable な値型で、生成元の容器が生きている間だけローカルに使えます。

Span は、同じ関数から ArrayFoundation.DataString、スタック上の値など、連続メモリを持ちうる様々な容器の中身を統一的・高速・安全に処理するための「通貨型」として位置付けられます。容器からの借用なので、non-copyable な容器に対しても複数の読み取り手段を同時に提供できますし、copyable な容器でも不要なコピーを避けられます。

Span<Element> の基本形

Span は次のような定義になります。

@frozen
public struct Span<Element: ~Copyable>: Copyable, ~Escapable {
  internal var _start: UnsafeRawPointer?
  internal var _count: Int
}

extension Span: Sendable where Element: Sendable & ~Copyable {}

型引数 Element~Copyable も許容するため、non-copyable な要素の連続メモリも扱えます。内部的には生ポインタと長さの 2 ワードで、C の std::span に相当する軽量な表現です。

Span には 3 種類の安全性が組み込まれています。

  • 時間的安全性 (temporal safety): 元の容器が生きている間しか Span を使えません。本提案の段階ではクロージャの外へ構造的にエスケープできないよう制限され、後続の lifetime dependency アノテーション提案で、より一般的な束縛にもこの保証が拡張される予定です。
  • 空間的安全性 (spatial safety): すべての要素アクセスで境界チェックが行われます。
  • 初期化済みであることの保証: Span が指すメモリは常に初期化済みです。

Span の基本 API

Span はバッファライクなインターフェイスを提供します。インデックスは Int のオフセットで、先頭が常に 0、末尾が count - 1 です。

extension Span where Element: ~Copyable {
  public var count: Int { get }
  public var isEmpty: Bool { get }

  public typealias Index = Int
  public var indices: Range<Index> { get }

  public subscript(_ position: Index) -> Element { _read }
}

subscript_read アクセサで要素を借用して返すため、non-copyable な要素でもコピーを発生させずに参照できます。_read はまだ公式な言語機能ではなく、将来的により良い「projection アクセサ」が提供された際に差し替えられる予定ですが、ソース互換で置き換えられる見込みです。

注意点として、SpanCollection にも Sequence にも適合しません。Collection は自身も要素もコピー・エスケープ可能であることを前提としており、スライスは元のコレクションとは別の値として扱われます。一方、Span の部分範囲は元の Span と同じ寿命を共有する必要があるため、既存の Collection プロトコル階層には収まりません。non-copyable / non-escapable に対応した新しいコレクション系プロトコルは別提案として検討されます。

この結果、Spanfor 文で直接は回せませんが、インデックスを使えば同じように書けます。

for i in mySpan.indices {
  calculation(mySpan[i])
}

境界チェックを省くアクセス

タイトなループで境界チェックのコストが問題になる場合のために、チェックを省く別のラベル付き subscript が用意されます。

extension Span where Element: ~Copyable {
  // 境界チェックを行わない。position の妥当性は呼び出し側の責任
  public subscript(unchecked position: Index) -> Element { _read }
}

呼び出し側で indicesRange.contains(_:) を使って事前に妥当性を確認した上で使うことを想定しています。

部分範囲関係の判定

複数の Span 間で、一方がもう一方の部分範囲かどうかを知りたい場面のために、次の関数が提供されます。

extension Span where Element: ~Copyable {
  // 同じメモリ領域を指しているか
  public func isIdentical(to span: borrowing Self) -> Bool

  // `span` が self の部分範囲なら、self 内でのオフセット範囲を返す。そうでなければ nil
  public func indices(of span: borrowing Self) -> Range<Index>?
}

unsafe コードとの相互運用

既存の C API や UnsafeBufferPointer を取るヘルパーに橋渡しするためのクロージャ型 API も提供されます。

extension Span where Element: ~Copyable {
  func withUnsafeBufferPointer<E: Error, Result: ~Copyable>(
    _ body: (_ buffer: UnsafeBufferPointer<Element>) throws(E) -> Result
  ) throws(E) -> Result
}

extension Span where Element: BitwiseCopyable {
  func withUnsafeBytes<E: Error, Result: ~Copyable>(
    _ body: (_ buffer: UnsafeRawBufferPointer) throws(E) -> Result
  ) throws(E) -> Result
}

クロージャの実行中だけ UnsafeBufferPointer / UnsafeRawBufferPointer が有効で、元の Span とそれが依存する束縛もクロージャ終了まで生存することが保証されます。

RawSpan

RawSpan は、異なる型の値が混在しうる連続メモリ、つまりパーサやデコーダが扱う「生バイト列」を表現するための型です。Span と同じ安全性保証を持ちつつ、ジェネリックではない具体型なので、デバッグビルドでも安定した性能を出しやすいという特徴があります。

@frozen
public struct RawSpan: Copyable, ~Escapable {
  internal var _start: UnsafeRawPointer
  internal var _count: Int
}

extension RawSpan: Sendable {}

RawSpanBitwiseCopyable な要素を持つ容器からも、Span<T: BitwiseCopyable> からも得られるようになる想定です。Sendable である点は意図的で、isolation boundary を越えて送ることができます(提案中では、ポインタを Int に詰めて送る既存の抜け道と同程度の unsafe さしか増やさないと整理されています)。

メモリからの値の読み出し

RawSpan は連続バイト列を任意の型としてロードする unsafe な API を提供します。

extension RawSpan {
  // properly aligned であることが前提
  public func unsafeLoad<T>(
    fromByteOffset offset: Int = 0, as: T.Type
  ) -> T

  // アライン不問。T は BitwiseCopyable に限る
  public func unsafeLoadUnaligned<T: BitwiseCopyable>(
    fromByteOffset offset: Int = 0, as: T.Type
  ) -> T
}

これらは型安全ではなく、前提条件を破ると不正な値が得られる可能性があるため unsafe と銘打たれています。境界チェック不要なホットパス向けに、fromUncheckedByteOffset: 版も同じ形で用意されます。

サイズ情報と部分範囲判定

extension RawSpan {
  public var byteCount: Int { get }
  public var isEmpty: Bool { get }
  public var byteOffsets: Range<Int> { get }

  public func isIdentical(to span: borrowing Self) -> Bool
  public func byteOffsets(of span: borrowing Self) -> Range<Int>?
}

Span と同じように、自身のバイト数や有効なバイトオフセットの範囲、他の RawSpan が部分範囲かどうかを取得できます。C 連携用の withUnsafeBytes(_:) も同様に提供されます。

初期化についての制限

本提案では、SpanRawSpan を外部から直接作るためのイニシャライザは 提案されません~Escapable な型のイニシャライザには、「生成される値の寿命をどの引数に紐付けるか」を表明する lifetime dependency アノテーションが必要ですが、その仕組み自体がまだ別提案として検討中だからです。

そのため当面は、標準ライブラリ内部でだけ Span / RawSpan が構築され、withUnsafeBufferPointer のようなクロージャ経由で利用者に渡される想定です。lifetime dependency 提案の採択後に、Array.span プロパティや Span.extracting(_:) のような派生操作、Span<T: BitwiseCopyable> から RawSpan への変換などが順次追加される予定です。

Future Directions

本提案はあくまで Span / RawSpan の土台で、これを軸に多くの拡張が予定されています。いずれも speculative で、実現を約束するものではありません。

  • lifetime dependency アノテーション: イニシャライザや extracting(_:) のような派生メソッド、Array.span のようなプロパティを安全に提供するための前提となる提案。
  • MutableSpan<T>: 書き込みの排他性と初期化状態を保ちながら連続メモリを安全に書き換えるための型。withUnsafeMutableBufferPointer の安全な代替。
  • OutputSpan<T>: 未初期化領域への追記を安全に行うための型。Array.init(unsafeUninitializedCapacity:initializingWith:) のような初期化 API をより安全にする。
  • 標準ライブラリ型への組み込み: Array などに withSpan(_:) / withBytes(_:) クロージャ API や span / bytes 計算プロパティを追加する。
  • ContiguousStorage プロトコル: 「Span を提供できる型」を表現し、base64Decoder.decode(bytes: some ContiguousStorage<UInt8>) のような汎用的な受け口を書けるようにする。
  • バイトパーサ用ヘルパ: RawSpan.Cursor のような、位置を管理しながら順次 parse(_:) していける API。PNG のチャンクをそのまま切り出すようなコードが素直に書けるようになる。
  • for 文対応: IteratorProtocol が escapable を前提としているため、borrowing 要素を順に返す新しい iterator プロトコルを別途導入する必要がある。
  • C++ std::span や LLVM の bounds-safety モードとの相互運用: 同種の意味論を持つ他言語の連続メモリ表現との橋渡し。

こうした拡張の積み重ねによって、現在 UnsafeBufferPointer が担っている役割の多くを Span / RawSpan が安全に置き換えていくことを目指しています。