ハイライト
Swift は、前提条件に違反した後に未定義の動作に入る可能性のあるインターフェイスを次のようにマークします。
Unsafe、開発者はバッファ ポインタを使用できます。withUnsafe*Swift 5.3 のクロージャと診断により、ポインターの有効期間がより狭い範囲に制限されます。
主要内容
迅速な安全性とは、決して衝突しないことを意味するものではありません。よくある強制解凍の遭遇nil明らかな致命的なランタイム エラーがトリガーされ、クラッシュ レポートによって制約が破られた場所がわかります。unsafelyUnwrappedまた、値を次のようにすることはできません。nilですが、最適化された構築では呼び出し元を信頼し、値を直接読み取ります。前提条件が満たされないと、その結果、クラッシュ、ガベージ値、または追跡が困難な状態の汚染が発生する可能性があります。
ポインタはリスクを増大させます。 Swift のランタイムは通常、オブジェクト、スタック変数、動的メモリの場所を管理するため、開発者が手動でアドレスを追跡する必要はありません。安全でないポインタの場合、ポインタは単なるアドレスです。メモリがまだ生きているのか、それともそのメモリが同じ Swift 値になっているのかはわかりません。解放されたダングリング ポインタは依然として有効なポインタのように見えるため、これに書き込むとアプリケーションの他の状態が破壊される可能性があります。
C と Objective-C の相互運用は、ポインタを操作する必要がある主なシナリオです。 C API は多くの場合、配列、文字列、および単一の値をポインターとカウントとして表し、Swift は独自の値をこれらの形式に一時的に公開する必要があります。 Apple は、配列、文字列、withUnsafeBufferPointer、withUnsafeMutablePointerこのタイプのクロージャ API は一時ポインタを生成し、安全でないコードを関数呼び出しまたはクロージャにプッシュします。
Session の後半では、より実践的な判断を示します。暗黙的な変換を使用して C API を呼び出すことができる場合は、自分でメモリを割り当てたり解放したりしないでください。ライフサイクルを表示する必要がある場合は、クロージャベースの API を使用してコード構造にポインタの有効期間を書き込みます。直接構築するだけですArrayまたはString新しいものは、基礎となるストレージが次の場合にのみ使用されます。unsafeUninitializedCapacity初期化し、戻る前にすべての境界チェックを完了します。
詳細
安全な操作は一定の結果をもたらします
(00:52) 通常の強制解凍も致命的なエラーになる可能性がありますが、Swift はエラーを明確に定義された致命的なランタイム エラーに変換します。この違いが分かるUnsafeプレフィックスの開始点。
let value: Int? = nil
print(value!) // Fatal error: Unexpectedly found nil while unwrapping an Optional value
キーポイント:
valueはいnil、強制解凍の前提条件を満たしていません。value!無効な入力に対する明示的な動作があるため、依然として安全な操作です。- ランタイムは実行を停止してエラーの場所を報告し、開発者はクラッシュ情報に基づいてコードを修正できます。
(01:58)unsafelyUnwrapped最適化されたビルドのチェックは省略されます。これは、呼び出し元が値が null ではないことをすでに証明できる場合の使用にのみ適しています。
let value: String? = "Hello"
print(value.unsafelyUnwrapped) // Hello
キーポイント:
- このコードは次の理由で機能します。
value文字列が含まれています。 unsafelyUnwrapped非 null 証明を呼び出し元に渡します。- トランスクリプトには、これはパフォーマンス シナリオであり、測定後にコストへの影響が確認されるコード パスにのみ配置する必要があると明確に記載されています。
(02:25) 前提条件が true でない場合、安全でない操作により存在しない値が読み取られる可能性があります。ここで焦点を当てているのは、クラッシュの形式ではなく、動作が未定義であるという事実です。
let value: String? = nil
print(value.unsafelyUnwrapped) // ?!
キーポイント:
nil読むものはありませんString価値。- 最適化された構造では、この読み取りによって同じエラーが発生することは保証されません。
- 同じコード部分でも、実行や最適化条件が異なると動作が異なる場合があります。
手動で割り当てられたポインタはライフサイクルを管理しません
(07:37)UnsafeMutablePointerメモリの直接割り当て、値の初期化、読み取りが可能pointee, ただし、リリース後のさらなるアクセスは妨げられません。
let ptr = UnsafeMutablePointer<Int>.allocate(capacity: 1)
ptr.initialize(to: 42)
print(ptr.pointee) // 42
ptr.deallocate()
ptr.pointee = 23 // UNDEFINED BEHAVIOR
キーポイント:
allocate(capacity:)保存できるセクションを指定してくださいInt動的メモリの。initialize(to:)このメモリ内の値のライフサイクルを開始します。deallocate()ストレージを解放した後、ptr変数自体は元のアドレスを保持したままです。- 書き続けます
ptr.pointee期限切れのメモリにアクセスすると、他のオブジェクトが汚染されたり、後でそのアドレスの値が再利用されたりする可能性があります。
バッファ ポインタは開始アドレスと長さを結合します。
(12:33) 開始アドレスが 1 つだけ渡されると、その長さがコード内で繰り返し表示されます。 Swiftはアドレスと要素数をオブジェクトとして扱うバッファポインタ型を提供しています。
UnsafeBufferPointer<Element>
UnsafeMutableBufferPointer<Element>
UnsafeRawBufferPointer
UnsafeMutableRawBufferPointer
キーポイント:
- 型付きバッファ ポインタは、要素の型がすでにわかっている連続したメモリに適しています。
- raw バッファ ポインタは、外部データをバイト単位で処理するのに適しています。
- デバッグビルドでは、バッファポインタの添え字が制限されます。
- 基盤となるメモリのタイプとライフサイクルが正しいかどうかはまだ検証されておらず、長さの欠落エラーが軽減されるだけです。
クロージャを使用して一時ポインタを生成する
(13:48) Swift 配列を C 関数に渡すときは、クロージャで一時バッファ ポインタを公開することを優先します。この方法では、メモリは依然として配列によって管理され、ポインタはクロージャ内でのみ有効です。
let values: [CInt] = [0, 2, 4, 6]
values.withUnsafeBufferPointer { buffer in
print_integers(buffer.baseAddress!, buffer.count)
}
キーポイント:
values通常の Swift 配列セマンティクスを維持します。withUnsafeBufferPointer基礎となる連続ストレージをクロージャに提供します。buffer.baseAddress!C 関数の開始ポインタです。buffer.countポインタと同じバッファから取得されるため、誤った書き込みの可能性が低くなります。
(14:25) このシナリオは非常に一般的であるため、Swift は C 関数呼び出しに対して同等の一時ポインターを生成することもできます。
let values: [CInt] = [0, 2, 4, 6]
print_integers(values, values.count)
キーポイント:
- 配列値は、安全でないポインタを期待する C パラメータに直接渡すことができます。
- コンパイラは一時的なポインタ変換を生成します。
- ポインタは関数呼び出し中にのみ有効であり、C 関数は後でアクセスできるようにポインタを保存できません。
複雑な C API は暗黙的な変換を使用して安全でない領域を分離できます
(16:32)sysctl入力バッファ、出力バッファ、長さポインタ、およびオプションの新しい値バッファを含む 6 つのパラメータがあります。 Swift は配列をポインターに使用できます。inoutポインタに変換することで、安全でない部分を 1 回の呼び出しに集中させることができます。
import Darwin
func cachelineSize() -> Int {
var query = [CTL_HW, HW_CACHELINE]
var result: CInt = 0
var resultSize = MemoryLayout<CInt>.size
let r = sysctl(&query, CUnsignedInt(query.count), &result, &resultSize, nil, 0)
precondition(r == 0, "Cannot query cache line size")
precondition(resultSize == MemoryLayout<CInt>.size)
return Int(result)
}
print(cachelineSize()) // 64
キーポイント:
queryC API で必要な整数の識別子バッファーです。&query配列からポインターへの一時的な変換をトリガーします。&resultそして&resultSizeC 関数の書き込み可能な出力場所を提供します。- 二
precondition実行時チェックとしてセッションで言及された仮定を書き込みます。呼び出しは成功し、返されるバイト数は次のとおりです。CIntサイズ。
(19:19) 純粋な Swift コードでは、クロージャベースの API がポインタの有効期間をより明確にマークします。一時ポインタを挿入するUnsafeMutablePointerイニシャライザはダングリング ポインタを生成する可能性があり、Swift 5.3 はこの検出可能な状態について警告します。
var value = 42
withUnsafeMutablePointer(to: &value) { p in
p.pointee += 1
}
print(value) // 43
var value2 = 42
let p = UnsafeMutablePointer(&value2) // BROKEN -- dangling pointer!
p.pointee += 1
print(value2)
キーポイント:
- 最初の段落では、
p有効期間はクロージャで囲まれています。 - 2 番目の段落では、一時ポインターの値を保存します。
p、イニシャライザの呼び出しが終了した後、基礎となるポインタの有効期限が切れています。 - フォローアップ訪問
p.pointee未定義の動作です。 - トランスクリプトでは、ライフタイムが発音しやすいため、純粋な Swift でのクロージャを優先することを推奨しています。
新しいイニシャライザにより一時バッファが削減されます
(20:02) Swift 標準ライブラリに新たに追加されましたString.init(unsafeUninitializedCapacity:initializingUTF8With:)C 関数が文字列ストレージとなるバッファに直接書き込むことができるようにします。これにより、一時メモリを手動で割り当てる必要がなくなります。
import Darwin
func kernelVersion() -> String {
var query = [CTL_KERN, KERN_VERSION]
var length = 0
let r = sysctl(&query, 2, nil, &length, nil, 0)
precondition(r == 0, "Error retrieving kern.version")
return String(unsafeUninitializedCapacity: length) { buffer in
var length = buffer.count
let r = sysctl(&query, 2, buffer.baseAddress, &length, nil, 0)
precondition(r == 0, "Error retrieving kern.version")
precondition(length > 0 && length <= buffer.count)
precondition(buffer[length - 1] == 0)
return length - 1
}
}
print(kernelVersion())
// Darwin Kernel Version 19.5.0: Thu Apr 30 18:25:59 PDT 2020; root:xnu-6153.121.1~7/RELEASE_X86_64
キーポイント:
- 初め
sysctl使用nil出力バッファクエリに必要な長さ。 Stringイニシャライザは、初期化されていない UTF-8 バッファを提供します。- 2回目
sysctlバージョン文字列をこのバッファに直接コピーします。 - 三つ
precondition呼び出しが成功したこと、書き込み長が正当であること、および末尾が C 文字列の NUL バイトであることを確認してください。 - 戻る
length - 1NUL バイトは破棄され、有効な UTF-8 コンテンツのみを含む文字列が残ります。
重要ポイント
-
やるべきこと: 安全な C API ラッパーを作成します。 実行する価値がある理由: セッションの説明 Swift は配列、文字列、および
inout値は一時的に C ポインタに変換されます。 開始方法: まず、各 C 関数を Swift 関数にラップし、ラッピング層で呼び出します。withUnsafeBufferPointerまたは、配列値を渡して使用しますprecondition戻りコードと出力長を確認してください。 -
やるべきこと: バイナリ配列読み取りツールを作成します。 実行する価値がある理由: バッファ ポインタは開始アドレスと長さをまとめて、ファイル ヘッダー、ネットワーク パケット、またはデバイス データの処理に適しています。 開始方法: を使用します
Array.withUnsafeBytes生のバッファを取得し、読み取る前にバイト数を確認し、オフセットとターゲットのタイプをいくつかの解析関数にグループ化します。 -
何をすべきか: プロジェクト内のダングリング ポインターのリスクを監査します。 実行する価値がある理由: Swift 5.3 はすでに、いくつかの一時的なポインター エスケープ シナリオについて警告することができ、セッションの壊れた例によって明確な検索ターゲットが提供されます。 開始方法: 検索
UnsafeMutablePointer(&、UnsafePointer(&安全でないポインタがプロパティに保存されている場合は、それらを次のように変更します。withUnsafePointerまたはwithUnsafeMutablePointer閉鎖。 -
内容: 新しい文字列初期化子を使用して C 文字列出力を読み取ります。 実行する価値がある理由:
String.init(unsafeUninitializedCapacity:initializingUTF8With:)C 関数で一時バッファを回避して文字列ストレージに直接書き込むことができます。 開始方法: まず C API を呼び出して長さをクエリし、次にイニシャライザ クロージャにバッファを書き込み、NUL 末尾を削除した後の UTF-8 バイト数を返します。
関連セッション
- Swift でポインターを安全に管理する — 型付きポインター、生のポインター、メモリ バインディングの境界を引き続き調査します。
- Swift の新機能 — Swift 5.3 のランタイム パフォーマンス、言語機能、および標準ライブラリの更新の概要。
- Swift 用の Objective-C フレームワークを洗練する — Objective-C 相互運用性を扱う前に、ヘッダー アノテーションを使用して Swift API サーフェスを改善します。
- Objective-C ランタイムの進歩 - 低レベルのレイアウトとタグ付きポインターから、ランタイムの変更が安全でない仮定に影響を与える理由を理解します。
コメント
GitHub Issues · utterances