MutableSpan and MutableRawSpan: delegate mutations of contiguous memory
01 何が問題だったのか
SE-0447 で導入された Span / RawSpan は、コンテナの連続メモリを安全に「読み取り専用で借用」するための型でした。一方で、同じようにコンテナの内部ストレージを書き換える安全な方法は用意されておらず、標準ライブラリでも Array.withUnsafeMutableBufferPointer(_:) や withContiguousMutableStorageIfAvailable(_:) といったクロージャ+UnsafeMutableBufferPointer ベースの API に頼るしかありませんでした。
これらの API にはいくつかの課題があります。
UnsafeMutableBufferPointerを経由するため、型としてはバッファの範囲外アクセスや寿命切れを防げません。安全性に敏感なコード(たとえばセキュリティ上厳しい要件を持つプロジェクト)では使いにくい道具です。- クロージャ形式の API は合成しづらく、non-copyable 値や typed throws などの新機能に対応させる度に、同じ形の API を何種類もメンテナンスすることになります。
Spanをそのままvarやinoutに束ねて書き換えに使う、というアプローチも取れません。Spanは copyable なので、書き換えを許すとSwift の排他制御則(law of exclusivity)を破ってしまいます。読み取りは複数同時に成り立つ必要があり、Spanが copyable であることは正しい設計ですが、そのぶん「変更」を表す型としては使えません。
つまり、排他借用されたメモリに対する変更を、Span と同じくらい安全に委譲できる型がまだ存在しない、という状態でした。Array や Foundation.Data の内部ストレージを書き換える API を、安全で合成可能な形で公開していくためには、Span の「書き換え版」に相当する専用の型が必要になります。
02 どのように解決されるのか
Span の書き換え版として、排他借用された連続メモリへのビューを表す MutableSpan と、その型なし版の MutableRawSpan を標準ライブラリに追加します。どちらも ~Copyable かつ ~Escapable で、Array などのコンテナが自身の内部ストレージの変更を安全に委譲するための窓口になります。
MutableSpan の基本設計
MutableSpan<Element> は、初期化済みの連続メモリへの排他借用ビューです。
@frozen
public struct MutableSpan<Element: ~Copyable>: ~Copyable, ~Escapable {
// 内部的には開始ポインタと要素数を保持
}
extension MutableSpan: @unchecked Sendable
where Element: Sendable & ~Copyable {}
~Copyable で排他アクセス(law of exclusivity)を型として強制し、~Escapable で「メモリの有効期間を越えて持ち出されない」ことを保証します。要素アクセスは添字で行い、すべて境界チェック付きです。
for i in myMutableSpan.indices {
mutatingFunction(&myMutableSpan[i])
}
MutableSpan は Collection / MutableCollection には適合しません(これらは要素もコンテナも copyable であることを前提としているため)。そのため for 文でも上のように indices を介す形になります。添字は 0 始まりのオフセットです。
読み取り専用で触りたい場合のために、元の範囲を借用する span: Span<Element> プロパティも用意され、MutableSpan から Span の世界に戻れます。また、要素が swapAt(_:_:) で入れ替えられるなど、よく使う破壊的操作も提供されます。
コンテナからの取得と寿命関係
Array / ContiguousArray / ArraySlice / InlineArray / CollectionOfOne、および Foundation.Data に、次のような mutableSpan 計算プロパティが追加されます。
extension Array {
public var mutableSpan: MutableSpan<Element> {
mutating get { ... } // 返り値の寿命は inout self に紐付く
}
}
このプロパティは「呼び出し側のArrayに対する一回の変更アクセス」を表します。返された MutableSpan が生きている間は、元の Array へのあらゆるアクセスがコンパイル時に禁止されます。
func modify(_ ms: inout MutableSpan<Int>) { /* ... */ }
func sample(_ array: inout Array<Int>) {
var ms = array.mutableSpan
modify(&ms)
// array.append(2) // ここで array を触るのはエラー
_ = consume ms // ここで ms を消費し、array への変更アクセスが終わる
array.append(1) // これ以降は array を自由に使える
}
MutableSpan の寿命はコンテナへの inout アクセスそのものであり、MutableSpan を明示的に consume するか、スコープを抜けて破棄されたタイミングで、元のコンテナに再びアクセスできるようになります。
なお、この寿命関係は今回のProposal内では仮の @_lifetime(inout self) という書き方で説明されていますが、これはあくまで説明用のプレースホルダです。実際の Swift の寿命アノテーション構文は別のProposalで確定する予定で、標準ライブラリはその構文が入り次第切り替わる見込みです。
copy-on-write なコンテナでは、mutableSpan を取り出す時点がすでに「変更の開始」です。バッキングストレージが一意に参照されていなければ、そのタイミングで必要なコピーが行われ、以降は一意化されたバッファへのビューが返されます。
部分範囲の切り出し(extracting)
バルクコピーなどでは「先頭からではなく、途中の範囲だけを書き換えたい」という要求がよく出てきます。MutableSpan では extracting(_:) 系のメソッドでサブスパンを取り出します。
extension MutableSpan where Element: ~Copyable {
public mutating func extracting(_ range: Range<Index>) -> Self
public mutating func extracting(_ bounds: some RangeExpression<Index>) -> Self
public mutating func extracting(first maxLength: Int) -> Self
public mutating func extracting(droppingLast k: Int) -> Self
public mutating func extracting(last maxLength: Int) -> Self
public mutating func extracting(droppingFirst k: Int) -> Self
}
返された MutableSpan は、呼び出し元の MutableSpan と同じメモリに対する変更アクセスを引き継ぎます。そのため、サブスパンが生きている間は元の MutableSpan にも触れません。
var array = [1, 2, 3, 4, 5]
var span1 = array.mutableSpan
var span2 = span1.extracting(3..<5)
// ここでは array も span1 もアクセス不可
span2.swapAt(0, 1)
_ = consume span2 // span2 のスコープを明示的に終える
span1.swapAt(0, 1)
_ = consume span1
print(array) // [2, 1, 3, 5, 4]
また、SE-0437 の方針に沿って、extracting で得たサブスパンは親とインデックスを共有しません。サブスパン側ではインデックスは 0 から振り直されます。
バルク更新
MutableSpan の中身をまとめて上書きするためのメソッドが用意されます。サイズが既知のソース(Collection や Span)から更新する場合、元のソースより短ければエラーになります。Sequence / IteratorProtocol から更新する場合は、ソースが尽きるか MutableSpan を埋め切るまでコピーされ、最後に書き込んだ次の位置が返ります。
extension MutableSpan where Element: Copyable {
public mutating func update(repeating repeatedValue: Element)
public mutating func update<S: Sequence>(from source: S)
-> (unwritten: S.Iterator, index: Index) where S.Element == Element
public mutating func update(fromContentsOf source: some Collection<Element>)
-> Index
}
extension MutableSpan where Element: ~Copyable {
public mutating func update(fromContentsOf source: Span<Element>) -> Index
public mutating func update(fromContentsOf source: borrowing MutableSpan<Element>)
-> Index
public mutating func moveUpdate(
fromContentsOf source: UnsafeMutableBufferPointer<Element>
) -> Index
}
moveUpdate(fromContentsOf:) はソース側を未初期化状態にしつつ移動コピーを行います。~Copyable な要素でも扱えるのがポイントです。
MutableRawSpan
MutableRawSpan は MutableSpan<T> の型なし・バイト単位版で、エンコーダやデコーダのように複数の型のビットパターンを書き込むような用途を想定しています。
@frozen
public struct MutableRawSpan: ~Copyable, ~Escapable { ... }
extension MutableRawSpan: @unchecked Sendable
byteCount / byteOffsets といったバイト単位のプロパティに加えて、
BitwiseCopyableな値のビット列を指定オフセットに書き込むstoreBytes(of:toByteOffset:as:)- 指定オフセットから任意の型の値として読み出す
unsafeLoad(fromByteOffset:as:)/unsafeLoadUnaligned(_:as:) Sequence/Collection/Span/MutableSpan/RawSpan/MutableRawSpanからのバルク更新update(from:)/update(fromContentsOf:)extracting(_:)によるサブスパン切り出し
といった API を持ちます。unsafeLoad(fromByteOffset:as:) はメモリ上のビットパターンが読み出す型の不変条件を満たすかどうかをチェックしないため、@unsafe が付いています。
MutableSpan<Element> から MutableRawSpan を取り出す経路も用意されます。ただし、要素の型の不変条件をビット単位の変更で壊し得るため、こちらも @unsafe です。
extension MutableSpan where Element: BitwiseCopyable {
@unsafe
public var mutableBytes: MutableRawSpan { mutating get }
}
Foundation.Data にも、UInt8 の要素を見る mutableSpan: MutableSpan<UInt8> と、バイト列として見る mutableBytes: MutableRawSpan の両方が追加される予定です。
境界チェックを外す unchecked 版
通常の添字・swapAt ・extracting はすべて境界チェック付きですが、事前に境界が保証されている場面ではコストを避けたいこともあります。そのため、同じ操作の unchecked 版(subscript(unchecked:)、swapAt(unchecked:unchecked:)、extracting(unchecked:) など)が @unsafe として提供されます。境界チェックを外すので、範囲外を渡すと未定義動作になります。
unsafe な世界との相互運用
既存の UnsafeMutableBufferPointer ベースの API と橋渡しするためのクロージャ型 API も用意されます。
extension MutableSpan where Element: ~Copyable {
public func withUnsafeBufferPointer<E: Error, Result: ~Copyable>(
_ body: (_ buffer: UnsafeBufferPointer<Element>) throws(E) -> Result
) throws(E) -> Result
public mutating func withUnsafeMutableBufferPointer<E: Error, Result: ~Copyable>(
_ body: (_ buffer: UnsafeMutableBufferPointer<Element>) throws(E) -> Result
) throws(E) -> Result
}
MutableRawSpan にも同様に withUnsafeBytes(_:) / withUnsafeMutableBytes(_:) が用意されます。これらは Array の同名メソッドと同じ役割で、クロージャのスコープ内で裏にあるバインディングを生かし続けるために使います。
逆向きの橋渡しとして、UnsafeMutableBufferPointer / UnsafeMutableRawBufferPointer からそれぞれ MutableSpan / MutableRawSpan を取り出す @unsafe な mutableSpan / mutableBytes プロパティも追加されます。返り値の寿命はポインタのバインディングに紐付くだけで、指しているメモリ自体の生存は保証しません。UnsafePointer 族を扱うときと同様に、プログラマ側で以下を守る必要があります。
- スパンを使っている間、メモリは初期化されたまま保たれていること
- スパンを使っている間、そのメモリに他の経路からアクセスしないこと
これを守らないと未定義動作になります。
性能
mutableSpan / mutableBytes は基本的に O(1) で、ポインタと長さを詰めた値を返すだけの軽い操作になる想定です。ただし copy-on-write 型では、バッキングストレージが一意でなければ、MutableSpan を返す直前にフルコピーが発生します。ブリッジされたコンテナ(Objective-C 由来の構造体など)については、もともと変更アクセスの度に防御的コピーが必要なので、MutableSpan に固有のオーバーヘッドはありません。
Future Directions
これらは speculative な見通しで、今回の提案に含まれるものではなく、実現を約束するものでもありません。
MutableSpanを 2 つに分割するsplit(at:)のような API。ただし non-copyable / non-escapable をタプルで返すのがまだ難しく、解法の検討が必要です。MutableCollectionで定義されているsort(by:)/partition(by:)といったアルゴリズム群を、一般化されたコンテナプロトコルの上でMutableSpanにも提供すること。Array.init(unsafeUninitializedCapacity:initializingWith:)の安全版となるOutputSpan<T>。部分的に初期化された連続メモリを表し、追記によって初期化領域を伸ばしていくモデルを想定しています。- 寿命アノテーション構文が確定した後の、
MutableSpan/MutableRawSpanの初期化 API の整備。