Swift Digest
SE-0333 | Swift Evolution

Expand usability of withMemoryRebound

Proposal
SE-0333
Authors
Guillaume Lessard, Andrew Trick
Review Manager
Ben Cohen
Status
Implemented (Swift 5.7)

01 何が問題だったのか

Swift で C のライブラリを呼び出したり、低レイヤなメモリ操作をしたりする場面では、「あるメモリ領域を一時的に別の型として扱いたい」ことがあります。そのための道具が UnsafePointer<Pointee> などに生えている withMemoryRebound(to:capacity:_:) です。クロージャの中だけ、メモリの型バインディングを T に差し替え、抜けるときに元の Pointee に戻してくれます。

func withMemoryRebound<T, Result>(
  to type: T.Type,
  capacity count: Int,
  _ body: (UnsafePointer<T>) throws -> Result
) rethrows -> Result

しかし Swift 5.6 までの withMemoryRebound には、PointeeT の stride が等しくなければならないという強い制約がありました。このため、実際には安全に rebind できる組み合わせでも、技術的に違反になってしまうケースが多数あります。

たとえば C 由来の Double の配列が「(x, y) ペアの列」を表していて、それを CGPoint の配列として読み取りたいとします。64-bit 環境では CGFloatDouble と layout-equivalent で、CGPoint はその CGFloat 2 つからなる homogeneous aggregate です。直感的には UnsafePointer<Double>CGPoint に rebind できてよさそうなのに、stride が倍違うため旧 API では許されず、ループで 1 要素ずつコピーして作るしかありませんでした。

var count = 0
let pointer: UnsafePointer<Double> = calculation(&count)

var points = Array<CGPoint>(unsafeUninitializedCapacity: count/2) {
  buffer, initializedCount in
  var p = pointer
  for i in buffer.indices where p+1 < pointer+count {
    buffer.baseAddress!.advanced(by: i).initialize(to: CGPoint(x: p[0], y: p[1]))
    p += 2
  }
  initializedCount = pointer.distance(to: p)/2
}

また、UnsafeRawPointer / UnsafeRawBufferPointer には withMemoryRebound 自体が存在していませんでした。ネットワークから受け取ったバイト列を Data でラップし、その中身を CGPoint として読みたい、といった生メモリ側からの rebind は、load(fromByteOffset:as:) を手で回す冗長なコードになっていました。

let data: Data = ...

var points = Array<CGPoint>(unsafeUninitializedCapacity: data.count/MemoryLayout<CGPoint>.stride) {
  buffer, initializedCount in
  data.withUnsafeBytes { data in
    var read = 0
    for i in buffer.indices where (read+2*MemoryLayout<CGFloat>.stride)<=data.count {
      let x = data.load(fromByteOffset: read, as: CGFloat.self)
      read += MemoryLayout<CGFloat>.stride
      let y = data.load(fromByteOffset: read, as: CGFloat.self)
      read += MemoryLayout<CGFloat>.stride
      buffer.baseAddress!.advanced(by: i).initialize(to: CGPoint(x: x, y: y))
    }
    initializedCount = read / MemoryLayout<CGPoint>.stride
  }
}

要するに、本来なら withMemoryRebound ひとつで書けるはずのコードに不必要な冗長さを強いる状態で、Unsafe ポインタ系 API の使い勝手を損ねていました。

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

withMemoryRebound の stride 制約を緩め、加えて生メモリ側にも同名 API を追加します。

許される TPointee の関係

rebind できる型の組み合わせを次のいずれかまで広げます。

  • TPointeelayout equivalent である(同一型、typealias 関係、同じサイズ・アラインメントの trivial なスカラー型、クラスとそのスーパークラス、layout equivalent な要素からなる optional 参照やポインタ、1つの stored property だけを持つ struct とそのプロパティ型、など)
  • TPointee の homogeneous aggregate である(例: PointeeCGFloatT(CGFloat, CGFloat)CGPoint
  • PointeeT の homogeneous aggregate である(上の向きが逆のケース)

homogeneous aggregate とはタプル・配列ストレージ・frozen struct のように、同じ要素が連続して並んだ集合型を指し、要素数と各要素の layout が一致していれば互いに layout equivalent とみなされます。

この結果、従来の「stride が等しい」要件は「T の stride が Pointee の stride の整数倍、もしくはその整数分の 1 である」へと緩和されます。アラインメントが異なる場合は、ポインタ側が大きい方のアラインメントを満たしている必要があります。

また、この条件に当てはまらない rebind(まったく無関係な型への付け替え)をしたい場合は、一度生ポインタに変換してから後述の生メモリ版 withMemoryRebound を使うことになります。

これにより先ほどの DoubleCGPoint の例はそのまま書けます。

var points = Array<CGPoint>(unsafeUninitializedCapacity: data.count/2) {
  buffer, initializedCount in
  pointer.withMemoryRebound(to: CGPoint.self, capacity: buffer.count) {
    buffer.baseAddress!.initialize(from: $0, count: buffer.count)
  }
  initializedCount = buffer.count
}

なお、capacity の意味は「rebind 後の T のインスタンス数」と明確化されます。stride が違ってもよくなった以上、Pointee の個数ではなく T の個数で数えるのが自然だからです。rebind 領域内の T は初期化済みでも未初期化でもかまいませんが、ある T に対応する Pointee の範囲は初期化状態が揃っている必要があります(混在は未定義動作)。

UnsafeBufferPointer / UnsafeMutableBufferPointer

typed buffer 側も同じ条件緩和を受けます。buffer 版はそもそも capacity 引数を取らず、T の要素数はもとの buffer のバイト数と T の stride からランタイムで自動計算されます。

extension UnsafeBufferPointer {
  public func withMemoryRebound<T, Result>(
    to type: T.Type,
    _ body: (_ buffer: UnsafeBufferPointer<T>) throws -> Result
  ) rethrows -> Result
}

UnsafeRawPointer / UnsafeMutableRawPointer への追加

生ポインタにも withMemoryRebound(to:capacity:_:) が生えます。生メモリを対象にするため、T には原則として型の制約が課されません(型安全性の担保は利用者の責任)。

ただし、その生ポインタが指すメモリがすでに何らかの型にバインドされている場合は、上で述べた UnsafePointer.withMemoryRebound のルールを満たす必要があります。つまり prebound な型と T が layout equivalent か、互いに homogeneous aggregate の関係にある、という条件です。一度もバインドされていない生メモリに対してはこの条件は適用されません。アラインメントについては、T のアラインメントに合っていることが常に必要です。

extension UnsafeRawPointer {
  public func withMemoryRebound<T, Result>(
    to type: T.Type,
    capacity count: Int,
    _ body: (_ pointer: UnsafePointer<T>) throws -> Result
  ) rethrows -> Result
}

extension UnsafeMutableRawPointer {
  public func withMemoryRebound<T, Result>(
    to type: T.Type,
    capacity count: Int,
    _ body: (_ pointer: UnsafeMutablePointer<T>) throws -> Result
  ) rethrows -> Result
}

UnsafeRawBufferPointer / UnsafeMutableRawBufferPointer への追加

生 buffer にも withMemoryRebound が追加されます。要素数は、もとの buffer のバイト数と T の stride から計算されます。もとのバイト数が T の stride の倍数でない場合、rebind 後の buffer はその分だけ短くなります。prebound な型がある場合の条件は生ポインタ版と同じです。

さらに「このメモリはすでに T にバインドされている」と分かっている場面向けに、assumingMemoryBound(to:) も追加されます。これは型付き buffer を返すだけの軽量な版で、count を自分で計算しなくて済みます。

extension UnsafeRawBufferPointer {
  public func withMemoryRebound<T, Result>(
    to type: T.Type,
    _ body: (_ buffer: UnsafeBufferPointer<T>) throws -> Result
  ) rethrows -> Result

  public func assumingMemoryBound<T>(to type: T.Type) -> UnsafeBufferPointer<T>
}

extension UnsafeMutableRawBufferPointer {
  public func withMemoryRebound<T, Result>(
    to type: T.Type,
    _ body: (_ buffer: UnsafeMutableBufferPointer<T>) throws -> Result
  ) rethrows -> Result

  public func assumingMemoryBound<T>(to type: T.Type) -> UnsafeMutableBufferPointer<T>
}

これらの追加のおかげで、Data 経由で受け取ったバイト列から CGPoint の配列を作る例も、load を回すコードではなく rebind 一回で書けるようになります。

var points = Array<CGPoint>(unsafeUninitializedCapacity: data.count/MemoryLayout<CGPoint>.stride) {
  buffer, initializedCount in
  data.withUnsafeBytes {
    $0.withMemoryRebound(to: CGPoint.self) {
      (_, initializedCount) = buffer.initialize(from: $0)
    }
  }
}

使う側への影響

既存の正しい利用コードはそのまま動きます。新しい緩和された意味論に依存するコードを古い標準ライブラリ上で動かすことはできませんが、更新後の実装は @_alwaysEmitIntoClient で提供されるため、古い OS にも back-deploy できます。