WWDC Quick Look 💓 By SwiftGGTeam
Safely manage pointers in Swift

Safely manage pointers in Swift

元の動画を見る

ハイライト

Swift は、安全でないポインタ操作を 3 つの層 (型付きポインタ、生ポインタ、メモリ バインディング) に分割します。開発者は、型バインディング、ライフサイクル、境界に関して徐々に責任を負う必要があります。

主要内容

ほとんどの Swift コードはポインターに触れるべきではありません。配列、スライス、イテレータ、および標準ライブラリ コレクションは、以前はポインタを使用して解決されていたであろう多くの問題をすでにカバーしています。 Session は冒頭でこのことを明確にしています。最も安全な戦略はポインタを使用しないことです。一度使用するとUnsafe接頭辞付き API の場合、コンパイラーとランタイムはそれほど心配する必要がなく、開発者は対応する正当性の証明を提供する必要があります。

通常、問題は相互運用性に起因します。 Swift データを C API に渡すことも、C API から渡すこともできます。Dataこのようなバイト ストリームから構造化された値をデコードします。 C コードでは、問題を解決するためにポインター変換がよく使用されますが、Swift の型付きポインターの規則はより厳密です。メモリが特定の型にバインドされた後、UnsafePointer<T>そしてUnsafeMutablePointer<T>このタイプのみが読み書き可能です。ポインター アドレスは正しいように見えますが、型が正しいという意味ではありません。

Appleはこのセッションで下降のはしごを与えた。ジェネリック パラメーターの型の安全性を維持するには、最初に型付きポインターを使用します。データをバイト単位で解釈する必要がある場合は、次を使用します。UnsafeRawPointerまたはUnsafeRawBufferPointer;元のポインタがその型を失った場合、またはバインドされた型を一時的に書き換える必要がある場合にのみ入力されます。assumingMemoryBoundbindMemorywithMemoryReboundこの層。レベルが下がるごとに、コードはより柔軟になり、証明コストが高くなります。

デバッグもクラッシュだけに依存することはできません。トランスクリプト内の画像カウントの例は、間違ったポインター型によりコンパイラーが誤った仮定を立て、最終的にはデータのサイレント損失として現れる可能性があることを示しています。アドレス サニタイザーとデバッグ アサーションはトラブルシューティングの時間を短縮できますが、すべての未定義の動作をカバーするわけではありません。より信頼性の高いアプローチは、安全でない範囲を最小限に抑え、コード内で境界、位置合わせ、およびバインディングの型チェックを記述することです。

詳細

型付きポインタの型はメモリ バインディングと一致する必要があります

(05:44) セッションでは、C スタイルのインターフェイスからの移行の例を使用して、一貫性のない型を持つポインターがエラーをコンパイラの最適化フェーズに引き渡すことを示しています。collage.imageCountはいInt、C スタイルの関数には次のものが必要です。UnsafeMutablePointer<UInt32>、コードは生のポインターを介して Swift の型チェックをバイパスします。

struct Image {
    // elided...
}

struct Collage {
    var imageData: UnsafeMutablePointer<Image>?
    var imageCount: Int = 0
}

func addImages(_ countPtr: UnsafeMutablePointer<UInt32>) -> UnsafeMutablePointer<Image> {
    // ...
    let imageData = UnsafeMutablePointer<Image>.allocate(capacity: 1)
    imageData[0] = Image()
    countPtr.pointee += 1
    return imageData
}

func saveImages(_ imageData: UnsafeMutablePointer<Image>, _ count: Int) {
    // Arbitrary function body...
    print(count)
}

var collage = Collage()
collage.imageData = withUnsafeMutablePointer(to: &collage.imageCount) {
    addImages(UnsafeMutableRawPointer($0).assumingMemoryBound(to: UInt32.self))
}
saveImages(collage.imageData!, collage.imageCount) // May see imageCount == 0

キーポイント:

  • withUnsafeMutablePointer(to:)与えられたものが指しているInt格納された型付きポインタ。 -UnsafeMutableRawPointer($0)タイプ情報は消去されますが、メモリの実際のバインドタイプは変更されません。 -assumingMemoryBound(to: UInt32.self)呼び出し元はメモリがバインドされていることを確認する必要があります。UInt32;ここでは満足できません。
  • コンパイラが考えることができるIntプロパティは変更されていないため、後続の読み取りでは初期値が取得される可能性があります。

型付きポインターの割り当ては型をバインドします

(10:06) 本当にメモリを直接割り当てたい場合は、UnsafeMutablePointer<T>.allocate新しいメモリをバインドしますT。これは、タイプが割り当ての瞬間から固定されるため、生の割り当てよりも証明のレベルが 1 つ低くなります。

func directAllocation<T>(t: T, count: Int) {
    let tPtr = UnsafeMutablePointer<T>.allocate(capacity: count)
    tPtr.initialize(repeating: t, count: count)
    tPtr.assign(repeating: t, count: count)
    tPtr.deinitialize(count: count)
    tPtr.deallocate()
}

キーポイント:

  • allocate(capacity:)戻るUnsafeMutablePointer<T>、メモリはジェネリックパラメータにバインドされていますT
  • initialize初期化されていないメモリを有効な値に設定する責任を負います。 -assign同じバインディング タイプの既存の値を置き換えます。 -deinitialize値の有効期間を終了し、deallocateストレージを解放します。どちらのステップも欠かすことはできません。

raw ポインタは外部データをバイト単位で読み取るのに適しています

(14:24) バイト ストリームから値を読み取ることが目的の場合は、生のポインタの方が適しています。メモリをバイト列として扱い、読み取り時にターゲットのタイプを指定します。開発者は、バイト オフセット、型レイアウト、アライメント、およびエンディアンを担当します。

import Foundation

func readUInt32(data: Data) -> UInt32 {
    data.withUnsafeBytes { (buffer: UnsafeRawBufferPointer) in
        buffer.load(fromByteOffset: 4, as: UInt32.self)
    }
}

let data = Data(Array<UInt8>([0, 0, 0, 0, 1, 0, 0, 0]))
print(readUInt32(data: data))

キーポイント:

  • Data.withUnsafeBytes基盤となるストレージを次のように公開しますUnsafeRawBufferPointer、ポインタはクロージャ内でのみ有効です。 -fromByteOffset: 45バイト目から読み出しを開始することを示します。 -as: UInt32.selfこれはこのロードの解釈方法にのみ影響し、メモリ全体を再バインドすることはありません。
  • このタイプのコードは、読み取る前に長さと位置合わせを確認する必要があります。空のデータや短すぎるデータは直接ロードできません。

raw 割り当ては、連続レイアウトを自分で管理するために使用されます

(15:43) raw 割り当ての価値は、ヘッダーと可変数の要素など、さまざまなタイプを連続メモリに配置することにあります。この時点で、各領域のストライド、アライメント、開始アドレスを自分で計算する必要があります。

func contiguousAllocate<Header>(header: Header, numValues: Int) -> (UnsafeMutablePointer<Header>, UnsafeMutablePointer<Int32>) {
    let offset = MemoryLayout<Header>.stride
    let byteCount = offset + MemoryLayout<Int32>.stride * numValues
    assert(MemoryLayout<Header>.alignment >= MemoryLayout<Int32>.alignment)
    let bufferPtr = UnsafeMutableRawPointer.allocate(
            byteCount: byteCount, alignment: MemoryLayout<Header>.alignment)
    let headerPtr = bufferPtr.initializeMemory(as: Header.self, repeating: header, count: 1)
    let elementPtr = (bufferPtr + offset).initializeMemory(as: Int32.self, repeating: 0, count: numValues)
    return (headerPtr, elementPtr)
}

キーポイント:

  • byteCountヘッダーとすべてをオーバーライドする必要がありますInt32要素。 -alignment気軽には受け取れません。この例では、アサーションを使用して、ヘッダーの位置合わせ要件が要素領域をカバーするのに十分であることを確認します。 -initializeMemory(as:)対応する領域は指定された型にバインドされ、型付きポインターが返されます。
  • この手法は内部コンテナの実装に適していますが、ビジネス コードのデフォルトの選択肢としては適していません。

メモリ バインディング API は最後の層です

21:17bindMemory(to:capacity:)メモリバインディングタイプを変更します。魔法のように値を変換するのではなく、このメモリが別の型に属していることをコンパイラに宣言するだけです。古い型付きポインタは無効になりました。

func testBindMemory() {
    let uint16Ptr = UnsafeMutablePointer<UInt16>.allocate(capacity: 2)
    uint16Ptr.initialize(repeating: 0, count: 2)
    let int32Ptr = UnsafeMutableRawPointer(uint16Ptr).bindMemory(to: Int32.self, capacity: 1)
    // Accessing uint16Ptr is now undefined
    int32Ptr.deallocate()
}

キーポイント:

  • uint16Ptr元々は 2 つの段落を指していたUInt16ストレージ。 -bindMemory(to: Int32.self, capacity: 1)同じアドレス範囲を 1 つに再バインドしますInt32
  • 再バインド後もアクセスを継続するuint16Ptr未定義の動作に属します。
  • 異なる型を読み込むだけの場合は、最初に生のポインタを使用します。load、読み取り用のバインド状態を上書きしないでください。

(23:13) 異なる型を必要とする C API を呼び出すだけの場合は、withMemoryRebound通常はより安全です。再バインドをクロージャに制限し、完了後に元の型を復元します。

func takesUInt8Pointer(_: UnsafePointer<UInt8>) { /* elided */ }

func testWithMemoryRebound(int8Ptr: UnsafePointer<Int8>, count: Int) {
    int8Ptr.withMemoryRebound(to: UInt8.self, capacity: count) {
        (uint8Ptr: UnsafePointer<UInt8>) in
        // int8Ptr cannot be used within this closure
        takesUInt8Pointer(uint8Ptr)
    }
    // uint8Ptr cannot be used outside this closure
}

キーポイント:

  • withMemoryRebound型の再バインドの影響をクロージャーのスコープに制限します。 ・純正品はクロージャー内に使用できません。int8Ptr同じメモリにアクセスします。 -uint8Ptr閉鎖空間の外には逃げることができません。
  • 該当するシナリオは、特に C API が同じバイト バッチに対して異なるポインター タイプを使用する場合、外部 API への短期間の呼び出しです。

重要ポイント

  • バイナリ形式デコーダ: カスタム ファイル ヘッダーまたはネットワーク パケットを読み取る解析層を作成します。 raw バッファではオフセットによる読み取りが可能UInt32、フラグビットと長さフィールド。からData.withUnsafeBytesまず、長さ、アライメント、エンディアンのチェックを記述してから、次のメソッドを呼び出します。load(fromByteOffset:as:)

  • C コールバック コンテキストのカプセル化: putpthread_createこの型は返すだけですvoid *インターフェイスは Swift API にパッケージ化されています。セッションプルーフassumingMemoryBoundバインドしたばかりの型を復元する場合にのみ適しています。からのThreadContextポインタを起点として、割り当て、初期化、コールバックの回復、解放といったライフサイクルが集中管理されます。

  • 内部連続ストレージ コンテナ: パフォーマンス重視のデータ構造用に、ヘッダーと要素領域の単一ブロック メモリ レイアウトを実装します。 raw 割り当ては、分散された割り当てを減らすことができます。からMemoryLayout<T>.stridealignmentそしてinitializeMemory(as:)最初は、安全でない詳細を小さな型内に隠し、通常のコレクション インターフェイスを外部に公開します。

  • レガシー ポインター コード監査ツール: C から移行された Swift コードの危険な移行ポイントを見つけます。このセッションの核となるリスクはbindMemoryそして間違っていますassumingMemoryBound古い型付きポインタは無効になります。から検索bindMemoryassumingMemoryBoundUnsafeMutableRawPointerまず、読み取り専用シーンを raw に変更します。load、短期の再バインド シナリオを次のように変更します。withMemoryRebound

関連セッション

  • 安全でない Swift — このセッションの前奏部では、最初に Swift の安全でない操作、C API ポインター、および未定義の動作の基本的な境界について説明します。
  • Swift 型推論を採用する — コンパイラーが型推論の観点から型ファクトをどのように使用するかを理解し、ポインター型を自由に偽装できない理由を判断するのに役立ちます。
  • Objective-C ランタイムの進歩 — 引き続き、Objective-C クラスと Swift クラスの背後にある低レベルのレイアウト、タグ付きポインター、ランタイムの変更を確認します。
  • Refine Objective-C Frameworks for Swift - 安全でないポインターが Objective-C 相互運用に由来する場合は、まずヘッダー ファイルの注釈と名前付けを通じて Swift API サーフェスをクリアします。

コメント

GitHub Issues · utterances