Swift Digest
SE-0138 | Swift Evolution

UnsafeRawBufferPointer

Proposal
SE-0138
Authors
Andrew Trick
Review Manager
Dave Abrahams
Status
Implemented (Swift 3.0.1)

01 何が問題だったのか

Swift 3 では SE-0107 によって UnsafeRawPointer が導入され、UnsafePointer<T> 同士の自由な型変換が禁止されました。これにより strict aliasing 違反になりかねないコードが正されましたが、同時に、型に紐付かない「生のメモリ」を扱うための手段が UnsafeRawPointer しかない状態になりました。

しかし多くのローレベル API は、生のポインタと長さをセットで受け渡しします。たとえば read(_:_:_:) のような C 由来の API に [UInt8] として読み込んだバイト列を渡したり、そのバイト列から構造体を取り出してネットワークメッセージを組み立てたりする、といったユースケースです。SE-0107 の時点では、このような「生のメモリのバッファ(ポインタと長さの組)」を表す専用の型が用意されていませんでした。

その結果、次のような問題が起きていました。

  • 生のメモリを UInt8 の列として扱うために、利用側で UnsafeBufferPointer<UInt8> を使い回すことが多くありました。しかし、実際にそのメモリには構造体などほかの型の値が入っていることもあり、UInt8 としてのアクセスと他の型としてのアクセスが混在すると、bindMemory(to:count:) による型束縛をAPI境界ごとに正しく行う必要があります。これを安全に行うのは難しく、結果として unsafeBitCastassumingMemoryBound(to:) で無理やり変換して「コンパイルだけ通す」移行コードが量産されていました。
  • Array<UInt8> を型消去されたバッファとして使い、API 境界を越えるたびにメモリを再束縛する回避策もありましたが、これは利用者に生のポインタとメモリ束縛の細かなセマンティクスの理解を強いるもので、現実的ではありませんでした。
  • Unsafe[Mutable]RawPointer だけでは長さを一緒に持ち運べないため、API ごとに長さを別引数として渡す必要があり、デバッグ時の境界チェックも行えませんでした。

要するに、SE-0107 で土台は整ったものの、「長さ付きの生メモリ」を安全かつ自然に扱う高レベル API が欠けていたために、Swift 3 への移行時にメモリモデル違反を温存したままのコードが生まれやすい、という移行体験上の問題がありました。

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

生のメモリ領域をポインタと長さの組として表す UnsafeRawBufferPointerUnsafeMutableRawBufferPointer を標準ライブラリに追加します。これらは UInt8Collection(可変版は MutableCollection)として振る舞い、かつ Unsafe[Mutable]RawPointer が提供する生メモリアクセス API の一部(load(fromByteOffset:as:)storeBytes(of:toByteOffset:as:)copyBytes(from:count:))をデバッグビルドでの境界チェック付きで提供します。

これは純粋な追加 API であり、既存コードのコンパイルには影響しません。

生メモリのバッファを表す型

UnsafeRawBufferPointer はメモリを所有せず、単にある領域へのビューを提供します。型はおおむね次のようなイメージです。

public struct UnsafeRawBufferPointer: Collection, RandomAccessCollection {
    public typealias Index = Int

    public var baseAddress: UnsafeRawPointer? { get }
    public var count: Int { get }

    // i 番目のバイトを UInt8 として参照
    public subscript(i: Int) -> UInt8 { get }

    // offset 位置から T 型の値を読み出す(型束縛は不要)
    public func load<T>(fromByteOffset offset: Int = 0, as type: T.Type) -> T
}

UnsafeMutableRawBufferPointer はこれに加えて、allocate(count:) による確保、deallocate() による解放、storeBytes(of:toByteOffset:as:) による値の書き込み、copyBytes(from:) によるバイト列のコピーなどを提供します。

// 4096 バイトのバッファを確保して使う
let buffer = UnsafeMutableRawBufferPointer.allocate(count: 4096)
defer { buffer.deallocate() }

重要なのは、load(fromByteOffset:as:) で任意の trivial な型 T の値を読み出せる点です。これにより、バイト列として扱っているメモリから構造体を取り出すときに、毎回 bindMemory(to:count:) を呼ぶ必要がなくなります。

値のバイト列ビュー: withUnsafeBytes(of:_:)

任意の値を一時的にバイト列として見るためのフリー関数が追加されます。

func write(bytes: UnsafeRawBufferPointer) { /* ... */ }

struct Header {
    var channel: Int32
    var payloadSize: Int32
}

var header = Header(channel: 1, payloadSize: 128)
withUnsafeBytes(of: &header) { bytes in
    // bytes は UnsafeRawBufferPointer で、header のバイト表現を指す
    write(bytes: bytes)
}

可変版の withUnsafeMutableBytes(of:_:) もあり、バッファに対して書き込みを行うクロージャに渡せます。

配列のバイト列ビュー: Array.withUnsafeBytes

ArrayArraySliceContiguousArray には、要素の型によらず内部ストレージをバイト列として見る withUnsafeBytes(_:)withUnsafeMutableBytes(_:) が追加されます。要素の型は trivial である必要があります。

let numbers = [1, 2, 3]
var byteBuffer: [UInt8] = []
numbers.withUnsafeBytes { rawBytes in
    // rawBytes は UnsafeRawBufferPointer
    byteBuffer += rawBytes
}

逆方向に、バイト列として持っている [UInt8] から構造体を読み出すこともできます。

let array: [UInt8] = // ... ヘッダのバイト列
let header = array.withUnsafeBytes { bytes in
    bytes.load(as: Header.self)
}

ネットワークメッセージの例

SE-0107 直後によく書かれていた「UInt8 バッファからヘッダ構造体を取り出す」コードは、これまで unsafeBitCast で型を付け替える必要がありました。UnsafeRawBufferPointer があれば、型束縛も未定義動作もなしに自然に書けます。

struct Header {
    var channel: Int32
    var payloadSize: Int32
}

func handleMessages(_ bytes: UnsafeRawBufferPointer) -> Int {
    var index = 0
    while true {
        let payloadIndex = index + MemoryLayout<Header>.stride
        if payloadIndex > bytes.count { break }
        // 生メモリから直接 Header を読み出す
        let header = bytes.load(fromByteOffset: index, as: Header.self)
        index = payloadIndex + Int(header.payloadSize)
        if index > bytes.count { break }
        // ペイロード部分をスライスとして渡す
        handle(header.channel, bytes[payloadIndex ..< index])
    }
    return bytes.count - index
}

送信側も、withUnsafeBytes(of:) を使えばヘッダ構造体をそのままバイト列として書き出せます。

func send(to fd: Int32, onChannel channel: Int32, message: UnsafeRawBufferPointer) {
    var header = Header(channel: channel, payloadSize: Int32(message.count))
    withUnsafeBytes(of: &header) { bytes in
        write(fd, bytes.baseAddress!, bytes.count)
    }
    write(fd, message.baseAddress!, message.count)
}

スライスの扱い

Unsafe[Mutable]RawBufferPointerSubSequence は自身ではなく [Mutable]RandomAccessSlice<Unsafe[Mutable]RawBufferPointer> です。Collection のセマンティクスに従い、スライスのインデックスは元のバッファのインデックスを維持します(ゼロにリベースされません)。

スライスを「先頭が 0 の UnsafeRawBufferPointer」として別の API に渡したい場合は、rebasing: イニシャライザで明示的に変換します。

func takesRawBuffer(_ buffer: UnsafeRawBufferPointer) { /* ... */ }

// 部分領域をそのまま渡すことはできない
// takesRawBuffer(buffer[i..<j])  // コンパイルエラー

// rebasing: で明示的にリベースした新しいバッファを作る
takesRawBuffer(UnsafeRawBufferPointer(rebasing: buffer[i..<j]))

また、Unsafe[Mutable]BufferPointer<T> 側にも同様の rebasing: イニシャライザが追加され、型付きバッファのスライスからもゼロ始まりのバッファを作れるようになります。

使いどころ

UnsafeRawBufferPointer は、型に依存しないバイト列を扱うローレベル API の共通通貨として使えます。ネットワークメッセージのデコード、FlatBuffers のようなバイナリフォーマットの読み書き、OutputByteStream のような「型を問わず受け取って書き出す」ストリーム、Data から生ポインタに降りるときの橋渡しなどが代表例です。Foundation.Data のような所有権付きの高レベル API が使える場面ではそちらを優先し、どうしても生ポインタを扱う必要がある局面で UnsafeRawBufferPointer を使う、という住み分けになります。