Swift Digest

ContiguousBytes での Span 採用

Adopting Span in ContiguousBytes

Proposal
SF-0034
Authors
Doug Gregor
Review Manager
Tina L
Status
Accepted; pending implementation

このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら

01 何が問題だったのか

ContiguousBytes プロトコルは、連続した生のバイト列にアクセスできる型を抽象化するために用意されたものです。Array<UInt8>UnsafeBufferPointer 系の型などが適合し、withUnsafeBytes を通じて UnsafeRawBufferPointer として中身を読めるようにしています。

public protocol ContiguousBytes {
    func withUnsafeBytes<R>(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R
}

しかし、SE-0447 で導入された Span / RawSpan をはじめとする Span 系の型はこのプロトコルに適合できませんでした。ContiguousBytes は暗黙に Self: Copyable, Escapable を要求しているのに対し、Span 系は連続メモリへの安全な借用を表すために ~Copyable~Escapable で設計されているからです。

その結果、

  • Span 系の型を ContiguousBytes を介して扱うジェネリック API に渡せません。
  • ContiguousBytes を通じて得られる UnsafeRawBufferPointer はクロージャの寿命を超えて生かしてはいけませんが、それは型では強制されず、利用側のお作法でしか守れません。

SpanUnsafe(Mutable)(Raw)BufferPointer の置き換えとして広く使われるようになっていく中で、「連続したバイト列を提供する型」という抽象化のほうもこれに追従する必要が出てきました。

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

ContiguousBytes プロトコルを ~Copyable かつ ~Escapable に拡張し、RawSpan を用いた安全な版である withBytes を新たに追加します。あわせて Span / MutableSpan / RawSpan / MutableRawSpan / OutputSpan / OutputRawSpan / UTF8Span / InlineArrayContiguousBytes に適合させます。

ContiguousBytes の一般化と withBytes の追加

プロトコル定義は次のようになります。

public protocol ContiguousBytes: ~Escapable, ~Copyable {
    func withUnsafeBytes<R>(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R

    func withBytes<R, E>(_ body: (RawSpan) throws(E) -> R) throws(E) -> R
}

withBytes はクロージャに RawSpan を渡します。RawSpan は non-escapable なので、その寿命がクロージャ呼び出しの範囲を越えないことが型レベルで保証されます。withUnsafeBytes と違って利用側のお作法に頼らずに済むため、より安全です。エラーの扱いも rethrows ではなく typed throws にしてあり、エラー型を正確に伝えられ、Embedded Swift とも相性がよくなっています。

既存の適合型のソース互換性のため、withBytes には withUnsafeBytes を経由するデフォルト実装が用意されます。

extension ContiguousBytes where Self: ~Escapable, Self: ~Copyable {
    public func withBytes<R, E>(_ body: (RawSpan) throws(E) -> R) throws(E) -> R { ... }
}

このため、すでに ContiguousBytes に適合している型は何もしなくても新しい withBytes を呼べるようになります。

Span 系と InlineArray の適合

RawSpan / MutableRawSpan / OutputRawSpan / UTF8Span は無条件で ContiguousBytes に適合します。

extension RawSpan: ContiguousBytes { }
extension MutableRawSpan: ContiguousBytes { }
extension OutputRawSpan: ContiguousBytes { }
extension UTF8Span: ContiguousBytes {
    public func withUnsafeBytes<R>(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R { ... }
}

要素を持つ Span / MutableSpan / OutputSpan / InlineArray は、ArrayUnsafeBufferPointer が以前からそうであったように、Element == UInt8 のときだけ条件付きで適合します。

extension Span: ContiguousBytes where Element == UInt8 { }
extension MutableSpan: ContiguousBytes where Element == UInt8 { }
extension OutputSpan: ContiguousBytes where Element == UInt8 { }
extension InlineArray: ContiguousBytes where Element == UInt8 {
    public func withUnsafeBytes<R, E>(_ body: (UnsafeRawBufferPointer) throws(E) -> R) throws(E) -> R { ... }
}

これにより、Span 系の値をそのまま ContiguousBytes を介したジェネリック API に渡せるようになります。

各型ごとの withBytes 実装

各具体型には、プロトコル要件よりわずかに一般的な withBytes 実装が用意されます。結果型 R を non-copyable にできる点が違いです。

public func withBytes<R: ~Copyable, E>(_ body: (RawSpan) throws(E) -> R) throws(E) -> R

プロトコル要件自体で R: ~Copyable を許してしまうと、既存の withUnsafeBytes ベースのデフォルト実装が成立せずソース互換性が壊れるため、プロトコル要件は R を copyable のままに保ち、具体型側でだけ non-copyable な結果を扱えるようにしています。

既存 API の一般化

ContiguousBytes を制約に持つ既存のジェネリック API は、Bytes: ~Copyable, ~Escapable を加えることで Span 系も受け付けられるようになります。

func encrypt<Bytes: ContiguousBytes>(_ bytes: Bytes) -> [UInt8] { ... }

たとえば上の API は次のように書き換えられます。

func encrypt<Bytes: ContiguousBytes>(_ bytes: Bytes) -> [UInt8]
    where Bytes: ~Copyable, Bytes: ~Escapable {
    ...
}

この変更はソース互換ですが、ABI 安定な API では注意が必要です。実装が値の暗黙コピーを行っていた場合、non-copyable な型を受け取ると実行時にクラッシュする可能性があります。ABI 互換を保ちつつ拡張したい場合は、SE-0476 で導入された @abi 属性を使い、~Escapable のみを許して Copyable 要件は保つのが安全です。

@abi(func encrypt<Bytes: ContiguousBytes>(_ bytes: Bytes) -> [UInt8])
func encrypt<Bytes: ContiguousBytes>(_ bytes: Bytes) -> [UInt8]
    where Bytes: ~Escapable {
    ...
}

この場合、MutableSpan のような non-copyable な型は受け取れませんが、実行時のクラッシュを避けられます。

なお、SE-0458 で導入された strict memory safety モードを有効にすると、UnsafeBufferPointer 系の利用箇所には unsafe 指定が必要になります。これに合わせて、withUnsafeBytes を使っているコードを withBytes に置き換えていくことが推奨されます。

03 今後の見通し

Proposal にはいくつかの将来の方向性が示されています。いずれも構想の段階で、実現を約束するものではありません。

withUnsafeBytes の deprecate

ソース・バックワード互換性を別にすれば、withUnsafeBytes を新たに使う理由はほぼなくなり、より安全な withBytes のほうが望ましくなります。将来的には withUnsafeBytes を deprecate することが議論されています。

ContiguousBytes を標準ライブラリへ移す

ContiguousBytes の概念は十分一般的で、適合する型の多くは標準ライブラリ側にあります。Span 系の導入により、連続メモリへの安全なアクセスはますます重要になっており、ContiguousBytes をソース・バイナリ互換を保ったまま標準ライブラリに移すことも検討されています。

ただし、本 Proposal の変更後でも ContiguousBytes は理想的な抽象とは言えません。本来は RawSpan を返すプロパティのほうが扱いやすいのですが、これは ContiguousBytes にソース互換のまま追加できません。

var bytes: RawSpan { get }

そのため、ContiguousBytes に加えて、別のプロトコルを標準ライブラリに導入する案も示されています。たとえば次のように ContiguousBytes の refinement として定義し、bytes から ContiguousBytes の要件を満たすデフォルト実装を提供する形です。

protocol RawBytes: ContiguousBytes {
    @_lifetime(self)
    var bytes: RawSpan { get }
}

extension ContiguousBytes where Self: RawBytes {
    public func withUnsafeBytes<R>(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R {
        try bytes.withUnsafeBytes { try body($0) }
    }

    public func withBytes<R, E>(_ body: (RawSpan) throws(E) -> R) throws(E) -> R {
        try body(bytes)
    }
}

新しい API は RawBytes を使い、既存 API は互換性のために ContiguousBytes を使い続ける、という棲み分けにすることで、互換性を保ちつつ進化させていく余地があります。