Swift Digest
SE-0334 | Swift Evolution

Pointer API Usability Improvements

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

01 何が問題だったのか

UnsafePointer 系の API は、低レイヤなメモリ操作や C ライブラリとの橋渡しに欠かせません。しかし Swift 5.6 までの API では、Unsafe そのものの難しさとは別のところで、日常的な使い勝手を損なう場面がいくつかありました。このProposalはそのうち3つの不便さを取り上げています。

型のアラインメントに合わせた位置まで進める手段がない

生メモリ(raw memory)上に複数の型を詰めて配置したいとき、「現在のポインタを、次に T を置いてよいアドレスまで進める」操作が必要になります。しかし既存 API はバイト単位の advanced(by:) しか提供しておらず、アラインメントを揃えるビット演算は利用者が自分で書くしかありませんでした。

たとえば、アトミックなポインタと任意の T をひとつのアロケーションに詰めるノードを作る場合、T の位置は次のように計算することになります。

// tMask/tOffset をビット演算でひねり出す
let tMask   = MemoryLayout<T>.alignment - 1
let tOffset = (MemoryLayout<AtomicOptionalRepresentation>.size + tMask) & ~tMask
let t = rawValue.advanced(by: tOffset)
                .initializeMemory(as: T.self, repeating: element, count: 1)

このビット演算自体は定型処理ですが、教科書を引かないと書けない上に間違えやすく、読み手にも意図が伝わりにくいという問題がありました。

集約型の stored property へのポインタを素直に取れない

C の API には、構造体の複数のフィールドを同時にポインタで受け取るものが多数あります。たとえば pthreads の pthread_getschedparam はスケジューリングポリシーとパラメータを別々のポインタ引数で書き戻します。

int pthread_getschedparam(pthread_t tid, int *policy, struct sched_param *param);

これを Swift 側で、関連するデータをまとめた struct として表現したとします。

struct ThreadSchedulingParameters {
  var policy: Int
  var parameters: sched_param
  var priority: Int { parameters.sched_priority }
}

このインスタンスの policyparameters にそれぞれポインタを渡すには、既存 API では withUnsafeMutableBytes で生バイト列として開き、MemoryLayout.offset(of:) でオフセットを取って assumingMemoryBound(to:) で型を付け直す、という手順が必要でした。

var e = withUnsafeMutableBytes(of: &scheduling) { bytes in
  let o1 = MemoryLayout<ThreadSchedulingParameters>.offset(of: \.policy)!
  let policy_p = bytes.baseAddress!.advanced(by: o1).assumingMemoryBound(to: Int32.self)
  let o2 = MemoryLayout<ThreadSchedulingParameters>.offset(of: \.parameters)!
  let params_p = bytes.baseAddress!.advanced(by: o2).assumingMemoryBound(to: sched_param.self)
  return pthread_getschedparam(thread, policy_p, params_p)
}

静的には型が分かっているのに一度生バイト列に落とし、直後に assumingMemoryBound で型を主張し直す、という遠回りを強いられる上に、ミスをしても型システムが助けてくれません。

異なる型のポインタ同士を直接比較できない

ポインタは型を問わず「メモリ上の一意な位置」を表す値です。同じバッファの始端と終端を指していても、片方が UnsafePointer<T>、もう片方が UnsafeMutablePointer<T> だったり、そもそも Pointee が違ったりすると、既存の ==< では比較できず、一度 UnsafeRawPointer に変換してから比べる必要がありました。こうした変換は生成コードに影響しない純粋な型合わせであり、本質的なコードを埋もれさせる雑音になっていました。

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

UnsafePointer まわりに、上記3つの課題に対応する API を追加します。いずれも追加のみで既存コードに影響はなく、実装は @_alwaysEmitIntoClient として提供されるため古い OS にも back-deploy できます。

アラインメントを揃えるポインタ操作

UnsafeRawPointer / UnsafeMutableRawPointer に、次に T を置けるアドレス、または直前の T を置けるアドレスまでポインタを動かすメソッドが生えます。すでに T のアラインメントに合っている場合は self をそのまま返します。

extension UnsafeRawPointer {
  public func alignedUp<T>(for type: T.Type) -> UnsafeRawPointer
  public func alignedDown<T>(for type: T.Type) -> UnsafeRawPointer

  public func alignedUp(toMultipleOf alignment: Int) -> UnsafeRawPointer
  public func alignedDown(toMultipleOf alignment: Int) -> UnsafeRawPointer
}

UnsafeMutableRawPointer にも同名のメソッドが追加されます。整数版 alignedUp(toMultipleOf:) / alignedDown(toMultipleOf:)alignment は 2 の冪である必要があります。

これにより先ほどのノード初期化は、ビット演算なしで書けるようになります。

rawValue.advanced(by: MemoryLayout<AtomicOptionalRepresentation>.size)
        .alignedUp(for: T.self)
        .initializeMemory(as: T.self, repeating: element, count: 1)

stored property へのポインタ取得

UnsafePointer / UnsafeMutablePointer に、KeyPath を渡して stored property へのポインタを得るメソッド pointer(to:) が追加されます。

extension UnsafePointer {
  public func pointer<Property>(
    to property: KeyPath<Pointee, Property>
  ) -> UnsafePointer<Property>?
}

extension UnsafeMutablePointer {
  public func pointer<Property>(
    to property: KeyPath<Pointee, Property>
  ) -> UnsafePointer<Property>?

  public func pointer<Property>(
    to property: WritableKeyPath<Pointee, Property>
  ) -> UnsafeMutablePointer<Property>?
}

KeyPath が computed property を指している場合には対応するポインタが存在しないため、戻り値は Optional になっており、その場合は nil が返ります。WritableKeyPath を受けるオーバーロードだけがミュータブルなポインタを返すため、読み取り専用の stored property からはミュータブルなポインタを作れません。

これで先ほどの pthread_getschedparam は、生バイト列に落とすことなく書けます。

var e = withUnsafeMutablePointer(to: &scheduling) {
  pthread_getschedparam(thread,
                        $0.pointer(to: \.policy)!,
                        $0.pointer(to: \.parameters)!)
}

型の異なるポインタ同士の比較

UnsafePointer / UnsafeMutablePointer / UnsafeRawPointer / UnsafeMutableRawPointer が共通して適合する内部プロトコル _Pointer に、異種ポインタ間の比較演算子が追加されます。

extension _Pointer {
  public static func == <Other: _Pointer>(lhs: Self, rhs: Other) -> Bool
  public static func != <Other: _Pointer>(lhs: Self, rhs: Other) -> Bool

  public static func <  <Other: _Pointer>(lhs: Self, rhs: Other) -> Bool
  public static func <= <Other: _Pointer>(lhs: Self, rhs: Other) -> Bool
  public static func >  <Other: _Pointer>(lhs: Self, rhs: Other) -> Bool
  public static func >= <Other: _Pointer>(lhs: Self, rhs: Other) -> Bool
}

比較はアドレスそのもので行われるため、Pointee の型が違っていても、Mutable の有無が違っていても、そのまま ==< で比べられます。これまで UnsafeRawPointer への変換を挟んでいた箇所は、変換なしでそのまま比較するだけで済みます。