Swift Digest
SE-0456 | Swift Evolution

Add Span-providing Properties to Standard Library Types

Proposal
SE-0456
Authors
Guillaume Lessard
Review Manager
Doug Gregor
Status
Implemented (Swift 6.2)

01 何が問題だったのか

SE-0447 で導入された Span / RawSpan は、連続メモリへの安全な借用ビューを提供する型ですが、その時点では既存の標準ライブラリ型から SpanRawSpan を取り出す手段が用意されていませんでした。

標準ライブラリの多くのコンテナ(ArrayString.UTF8ViewUnsafeBufferPointer など)は、内部表現である連続メモリへの直接アクセスを可能にしていますが、従来はそれを withUnsafeBufferPointer() / withContiguousStorageIfAvailable() / withUnsafeBytes() といったクロージャを受け取る API でしか行えませんでした。これらには次のような問題があります。

  • 渡ってくる UnsafeBufferPointer / UnsafeRawBufferPointer 自体が unsafe な型であり、セキュリティを重視する場面では採用しづらい。
  • クロージャベースの API は他の API や新機能と組み合わせにくい。例えば、クロージャの外へ戻り値として non-copyable な値を返したり、クロージャ内で asyncTask/typed throws を組み合わせたりするのが困難です。

具体例として、myArray.withUnsafeBufferPointer { buffer in ... } のようなコードは、クロージャという構造があるためにコードの発展(戻り値を non-copyable にする、並行タスクを組み込む、typed throws を追加するなど)を妨げます。Span は non-escapable なビューとして安全かつ合成しやすい代替になり得ますが、そのためには標準ライブラリ側からインスタンスを取り出せる必要があります。

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

標準ライブラリと Foundation の主要な型に、連続メモリへの借用ビューを提供する span / bytes computed property を追加します。これらは withUnsafeBufferPointer()withUnsafeBytes() の安全かつ合成しやすい代替として利用できます。

computed property による borrowing lifetime

本 Proposal では、~Escapable & Copyable な値を返す computed property getter を、特別なアノテーションなしで書けるようにします。返される値は「呼び出し側のバインディングを借用する」という関係を持ち、返り値(そのローカルコピーを含む)が生存している間、呼び出し側のバインディングは借用されたままとなります。借用は排他律の観点では read-only アクセスなので、複数の借用は重なり得ますが、その間はミュータブルなアクセス(書き換え)が行えません。

この仕組みのおかげで、Span を返すプロパティを宣言でき、コンパイラがライフタイムを追跡して安全性を保証します。

標準ライブラリ型への span プロパティ

次のような型に span プロパティが追加されます。戻り値は呼び出し側を借用する Span です。

extension Array {
  var span: Span<Element> { get }
}

extension ArraySlice {
  var span: Span<Element> { get }
}

extension ContiguousArray {
  var span: Span<Element> { get }
}

extension String.UTF8View {
  var span: Span<Unicode.UTF8.CodeUnit> { get }
}

extension Substring.UTF8View {
  var span: Span<Unicode.UTF8.CodeUnit> { get }
}

extension CollectionOfOne {
  var span: Span<Element> { get }
}

extension KeyValuePairs {
  var span: Span<(Key, Value)> { get }
}

また、SE-0453 で受理された InlineArray についても次のプロパティが追加されます。

extension InlineArray where Element: ~Copyable {
  var span: Span<Element> { get }
}

これにより、従来のクロージャベースのコードを次のように書き換えられます。

// 従来
let result = try myArray.withUnsafeBufferPointer { buffer in
    let indices = findElements(buffer)
    var myResult = MyResult()
    for i in indices {
        try myResult.modify(buffer[i])
    }
    return myResult
}

// Span を使った書き換え
let span = myArray.span
let indices = findElements(span)
var myResult = MyResult()
for i in indices {
    try myResult.modify(span[i])
}

クロージャという構造がなくなるため、戻り値を non-copyable な型にしたり、typed throws や並行処理を後から追加したりといった発展がしやすくなります。また、span を不正にエスケープさせようとするとコンパイラが診断します。

Span から生バイト列を見る bytes

要素が BitwiseCopyable である Span については、同じメモリを untyped な生バイト列として見るための bytes プロパティが追加されます。

extension Span where Element: BitwiseCopyable {
  var bytes: RawSpan { get }
}

返される RawSpan は、元の Span と同じバインディングを借用します。bytes を各コンテナ型ごとに個別に用意するのではなく、Span の条件付きプロパティとしてまとめて提供することで、型ごとの重複を避けています。

UnsafeBufferPointer 系からの取り出し

UnsafeBufferPointerUnsafeRawBufferPointer から Span / RawSpan に橋渡しするための unsafe な変換も用意されます。

extension UnsafeBufferPointer {
  var span: Span<Element> { get }
}

extension UnsafeMutableBufferPointer {
  var span: Span<Element> { get }
}

extension UnsafeRawBufferPointer {
  var bytes: RawSpan { get }
}

extension UnsafeMutableRawBufferPointer {
  var bytes: RawSpan { get }
}

これらが返す値のライフタイムは、UnsafeBufferPointerバインディングに依存するだけで、背後のメモリの生存を保証するものではありません。プログラマは、返された Span / RawSpan が存在している間、

  • そのメモリが確保され続けていること、
  • そのメモリが初期化された状態を保っていること、
  • そのメモリが書き換えられないこと

を自分で保証する必要があります。これらの不変条件を破ると undefined behaviour になります。

Foundation.Data

Foundation.Data は Swift Evolution の対象外ですが、使い方が標準ライブラリ型に近いため、同じ方針で次のプロパティが追加される予定です。

extension Foundation.Data {
  var span: Span<UInt8> { get }
  var bytes: RawSpan { get }
}

Data は概念的に untyped なバイト列のコンテナなので、標準ライブラリ型と違って bytes を直接持たせる形になっています。

パフォーマンス

spanbytes は原則として O(1) で返ります。ただし、Darwin プラットフォームでは、Objective-C 側から bridging された ArrayString が必ずしも連続メモリ表現を持たない場合があります。そのようなケースでは、span / bytes の取得時に一度だけ native な Swift 表現へ eager にコピーが行われ、そのコピーを指す Span / RawSpan が返されます。

このため、bridging された String.UTF8ViewArrayspan プロパティの計算量は「amortized constant time」とドキュメントされます。既存のクロージャベース API の挙動は変わらないため、span / bytes を新しく採用したコードだけがこの追加コピーの可能性を引き受けることになります。

今後の見通し

Span 側の方向性(SE-0447 の Future Directions)はすべてここにも当てはまります。そのほかに、今回含まれなかったが将来検討され得る項目として次のようなものが挙げられています。いずれも speculative であり、実現を約束するものではありません。

  • MutableSpan<T> による安全な mutation: withMutableBufferPointer() の安全な代替として、初期化済みメモリへの書き換えを委譲できる MutableSpan 型と、標準ライブラリ型からそれを取り出す API が検討されています。span と返り値の型を被せられないため、名前は別(例えば mutableStorage)になる見込みです。
  • ContiguousStorage プロトコル: 「Span を提供できる型」を表すプロトコルを導入し、Span を介したジェネリックな抽象と具象実装の橋渡しをする案です。現状は associatedtype 要件の抑制などの言語機能が揃っていないため見送られています。
  • SIMD 型への span: レビュー版には含まれていましたが、SIMD 関連プロトコルが連続メモリ表現を明示的に要求していないため今回は見送られました。将来プロトコル側を整備するか、InlineArray 経由で提供するかといった選択肢があります。