Cell、ConstCell、Volatile
Cell, ConstCell, and Volatile
このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら↗。
01 何が問題だったのか
Span(SE-0447)、MutableSpan(SE-0467)、OutputSpan(SE-0485)、Ref(SE-0519)などの Span / Ref 系の型は、連続メモリや値への安全なアクセスを提供します。参照先のメモリが生きている間しかアクセスできないライフタイム安全性と、インデックスアクセスが範囲内に収まる境界安全性を保証します。
Law of Exclusivity への依存
これらの型は、Swiftの 排他性の法則(Law of Exclusivity) を前提にしています。同じメモリへのアクセスが2つ同時に存在するなら、その両方が読み取りでなければならない、というルールです。Swiftは non-copyable 型による一意な所有、non-escapable 型によるアクセス範囲の制限、Copy-on-Write や動的な排他性チェックなどを組み合わせてこの法則を守っています。違反するコードはコンパイルエラーになります。
func f() {
var array = [1, 2, 3, 4, 5]
let data = array.span // note: conflicting access is here
array.append(6) // error: overlapping accesses to 'array', but modification requires
// exclusive access; consider copying to a local variable
print(data[0])
}
Span の subscript がコピーなしで値を直接 borrow できるのも、MutableSpan が mutate アクセサで直接書き換えられるのも、この法則で「他に書き換える者がいない」ことが保証されているからです。逆に、法則が成り立たないメモリを Span で扱うと、参照先のオブジェクトがアクセス中に解放されるなど、メモリ安全性が壊れてしまいます。
ライフタイムは保証できても排他性は保証できないケース
一方で、ライフタイムと境界の安全性は欲しいものの、排他性は原理的に保証できない場面があります。
- 共有メモリ: 別プロセスや、GPU・ネットワークコントローラなどのハードウェアからも同じメモリ領域にアクセスされる場合、プログラムからは見えないエイリアスが必ず存在します。
- C / C++ との相互運用:
__attribute__((lifetimebound))のような注釈からライフタイムは分かっても、そのポインタを他のC / C++コードが書き換えないことまでは保証できません。
class MyBuffer {
public:
std::span<const double> getContents() const __attribute__((lifetimebound));
};
このようなケースで使える標準ライブラリの型は Unsafe(Mutable)(Raw)BufferPointer しかなく、メモリ安全性のすべてを手放すことになっていました。
同期プリミティブと並行データ構造
Atomic や Mutex のような同期プリミティブは、複数のスレッドから同時に呼ばれるため、withLock のようなメソッドを mutating にはできず borrowing にする必要があります。しかし、共有アクセスしか持たない状態で内部の値を書き換えることは、Law of Exclusivity の下では許されません。
struct Mutex<Value: ~Copyable>: ~Copyable {
var value: Value
var lockImplementation: ...
borrowing func withLock(...) {
lockImplementation.lock()
defer {
lockImplementation.unlock()
}
// error: cannot pass immutable value as inout argument: 'self' is immutable
try body(&value)
}
}
共有アクセスしかない状態でメモリを書き換える能力、すなわち interior mutability を持つストレージを、クラス以外のSwiftの型では表現できませんでした。
02 どのように解決されるのか
標準ライブラリに Cell、ConstCell、Volatile の3つの non-copyable 型を追加します。
Cell
Cell<Value> は値を包む薄いラッパーで、borrowing な(共有アクセスしか持たない)状態からでも、中の値へのミュータブルなポインタを取得できます。レイアウトは包んでいる型とまったく同じで、レイアウト上・パフォーマンス上のオーバーヘッドはありません。
これを使うと、先ほどの Mutex を次のように実装できます。
struct Mutex<Value: ~Copyable>: ~Copyable {
var value: Cell<Value>
var lockImplementation: ...
borrowing func withLock(...) {
lockImplementation.lock()
defer {
lockImplementation.unlock()
}
try value.withUnsafeMutablePointer {
try body(&$0.pointee)
}
}
}
Cell を Span や Ref と組み合わせ、Span<Cell<T>> や Ref<Cell<T>> とすると、ライフタイム安全性は保ったまま、要素ごとに排他性の保証を外せます。読み取り専用の Span 越しでも、Cell の中身は書き換えられます。
struct Person {
var age: Int
}
func foo(people: Span<Cell<Person>>) {
people[0].value.age += 1
}
Cell のAPIは次のとおりです。
public struct Cell<Value: ~Copyable>: ~Copyable {
public init(_ initialValue: consuming Value)
// 中の値へのミュータブルなポインタをクロージャに渡す
public func withUnsafeMutablePointer<Result: ~Copyable, E>(
_ body: (UnsafeMutablePointer<Value>) throws(E) -> Result
) throws(E) -> Result
// 新しい値に置き換え、古い値を返す
@discardableResult
public func replace(with newValue: consuming Value) -> Value
// 新しい値を設定する
public func set(_ newValue: consuming Value)
}
extension Cell where Value: ~Copyable {
// borrow による直接アクセス(unsafe)
@unsafe
public var unsafeBorrow: Value {
borrow
nonmutating mutate
}
}
extension Cell where Value: Copyable {
// コピーによるアクセス
public var value: Value {
get
nonmutating set
}
}
valueはValueがCopyableの場合のみ使えます。get/setでコピーアウト・コピーインするため、排他性の問題が起きません。unsafeBorrowはコピーせずに直接アクセスできますが、borrow している間に中身が書き換えられると Law of Exclusivity に反するため@unsafeです。- メソッドはすべて
mutatingではなく、共有アクセスから呼び出せます。
ConstCell
ConstCell<Value> は Cell と同じ保証を持ちますが、Swift側からの書き込みを禁止します。ただし、Swiftの外側(Cコードや他プロセス、ハードウェア)からはメモリが書き換えられる可能性があります。
func foo(people: Span<ConstCell<Person>>) {
people[0].value.age += 1 // error: 'value' is get-only
}
APIは Cell の読み取り側に相当し、withUnsafePointer(_:)、Copyable な Value 向けの get のみの value、@unsafe な unsafeBorrow を持ちます。また Value が Sendable かつ FullyInhabited の場合に Sendable に適合します。
Volatile
Volatile<Value> は、Cの volatile と同様に、値へのアクセスを volatile なロード / ストア で行う型です。メモリマップドI/O(MMIO)のように、他のプロセスやハードウェア自体がメモリを書き換える領域では、コンパイラが読み書きを省略したり並べ替えたりしてはいけません。Volatile はこれを保証します。
let controlRegister = UnsafePointer<Volatile<Bool>>(bitPattern: 0x40000100)!
print(controlRegister.pointee.value) // true
print(controlRegister.pointee.value) // maybe true, maybe false
Value は BitwiseCopyable である必要があります。
public struct Volatile<Value: BitwiseCopyable>: ~Copyable {
public init(_ initialValue: consuming Value)
// volatile ロードで値を読み出す
public func read() -> Value
// volatile ストアで値を書き込む
public func write(_ newValue: Value)
// volatile に読み出した値をクロージャで変更する
public func modify<Result: ~Copyable>(_ body: (inout Value) -> Result) -> Result
public func withUnsafeMutablePointer<Result: ~Copyable, E>(
_ body: (UnsafeMutablePointer<Value>) throws(E) -> Result
) throws(E) -> Result
}
Cell 系の型と異なり、Volatile の value への書き込みには排他アクセスが必要です。共有アクセスから書き込みたい場合は、Cell<Volatile<T>> のように組み合わせます(Volatile は non-copyable なので、Cell.value ではなく replace(with:) を使います)。
func write(to x: borrowing Cell<Volatile<Bool>>) -> Volatile<Bool> {
x.replace(with: Volatile(false))
}
既存の型からの変換
Ref / Span 系の型から、Cell を要素とする型に変換するAPIも追加されます。
extension MutableRef where Value: ~Copyable {
public consuming func cell() -> Ref<Cell<Value>>
}
extension Span where Element: ~Copyable {
public func constCells() -> Span<ConstCell<Element>>
@unsafe
public func unsafeCells() -> Span<Cell<Element>>
}
extension MutableSpan where Element: ~Copyable {
public consuming func cells() -> Span<Cell<Element>>
}
MutableRef<T>→Ref<Cell<T>>とMutableSpan<T>→Span<Cell<T>>は安全です。排他アクセスを持っていたものを、共有メモリとしての共有アクセスに格下げするだけだからです。Span<T>→Span<ConstCell<T>>も安全です。Swiftから見ればメモリは読み取り専用のままです。Span<T>→Span<Cell<T>>は本質的に unsafe です。共有されていない読み取り専用のメモリを、Swiftから直接書き換えられる共有メモリとして扱うことになるためです。
採用にあたっての注意
Cell 系の型は、共有メモリ、並行データ構造、Cとの相互運用など、エイリアスがないことを保証できない、または保証が現実的でない場面のためのものです。便利だからといって Span<T> の代わりに Span<Cell<T>> を返すAPIが広まると、どちらを使うべきか混乱を招き、本来不要な場所で Cell が使われることで性能を損なうおそれがある、と注意喚起されています。
03 今後の見通し
C / C++ との相互運用
strict memory safety の Vision で述べられているように、境界とライフタイムが分かっているCのポインタを、Cell の Span として表現することが考えられています。たとえば次のC++ APIでは、std::span がポインタと境界を、lifetimebound 属性がライフタイムを示しています。
class MyBuffer {
public:
std::span<double> getContents() __attribute__((lifetimebound));
};
この情報があれば、次のようなシグネチャでSwiftにインポートできる可能性があります。
func getContents() -> Span<Cell<Double>>
具体的なC / C++の注釈や、それをSwiftにどう対応付けるかはこのProposalの範囲外で、将来の検討課題とされています。