ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
Swiftで安全にポインタを管理する
Swiftにおける安全でないポインタの型について一緒に掘り下げて考えてみましょう。各型に対する要求事項やその正しい使用方法をお伝えします。型指定されたポインタについて話し合い、生のポインタにドロップダウンし、最後にメモリーを結合することのみでポインタの型安全性から免れることができるでしょう。 このセッションは、WWDC20の "Unsafe Swift" の続きです。このセッションを有効に活用するためには、SwiftおよびC言語に慣れていることが望ましいです。
リソース
関連ビデオ
WWDC20
-
ダウンロード
こんにちは WWDCへようこそ “Swiftで安全にポインタを管理する” アンディーです “Swiftで安全にポインタを管理する” 方法をお話しします このセッションの基盤となっている WWDC 2020の“Unsafe Swift”では 一部の入力で未定義動作がある状態を 安全でないオペレーションと定義しました 今回は 通常の安全領域外での Swiftのプログラミングについて より詳しく説明します これらは通常アプリ開発者が 気にかける必要のないことです ポインタを安全に管理するということは 安全でないものを理解することです 型安全性についての説明が 長くなると思います それは 理解しにくい傾向がある Cの未定義動作の原因です Swiftに同じ下位レベル機能を提供するAPIと それらを使用して未定義動作を 回避する方法を説明します ポインタの安全性は 一連のレベルとして見ることができます レベルが一段下がると コードの正確性に対して より多くの責任を負うことになります ですから 可能な限り最高の安全性レベルで コードを書くことが推奨されます 最初のレベルは安全なコードです Swiftの主な目的は安全でない構造を 必要としないコードを書く新しい方法の提供です Swiftには高い柔軟性とパフォーマンスを 提供する堅固な型システムがあります SwiftのコレクションAPIやスライス イテレータは 皆さんがポインタに望んでいた機能の 多くを提供します そして ポインタをまったく使用しないことは コードの安全性において優れた戦略です しかし Swiftの重要なもう1つの目的は 安全でない言語との効率的な相互運用性です それを実現するために Swiftは安全でないAPIの形式での 下位レベルの表現能力を 提供する必要があります これは 型や関数名では 接頭辞の“Unsafe”で示されています SwiftのUnsafePointerはポインタを使用すると 型安全性を気にせずに 危険性の一部について 皆さんが責任を持つことができます バイトのシーケンスとして 生メモリで作業する必要がある場合 Swift はUnsafeRawPointerを提供します 生メモリで値をロードして保存するためには 型のレイアウトを知っていなければいけません 最も詳細なレベルでは Swiftはメモリを型に バインドするためのいくつかのAPIを提供します ポインタ型の安全性を管理において Swiftから完全に責任を取り除けるのは 下位レベルAPIを使用することによってのみです 安全性のレベルが何を意味するのか 説明させてください 安全なコードは必ずしも正しいコードとは 限りませんが 予想通りに動作します 多くの場合 プログラミングエラーが 予測できない動作につながると コンパイラがそれを補足します コンパイル時に捕捉できないエラーについては プログラムが診断し即座にクラッシュすることを ランタイムチェックが保証します 誤ったassumptionを過ぎて 継続することはありません つまり 安全なコードとは エラーに関するものです 安全でないとマークされた Swift型またはAPIを使用せず 注意深くスレッドの安全性を管理すれば 予測可能な動作が 完全に実行されることが分かります 安全でないSwiftコードでは 予測可能な動作は完全に実行されないため 皆さんの責任が多くなります それでも テストは役立つ診断を提供します しかし 診断のレベルは 選択した安全性のレベルによって異なります 安全でない標準ライブラリのAPIには デバグビルドのアサーションがあります それは特定の種類の無効な入力を捕捉します 安全でないassumptionを検証するために 独自の前提条件を追加することも良い方法です sanitizerを有効にして ランタイムチェックでテストすることもできます sanitizer診断では バグを正確に示すことにより 大きく時間を節約できますが 未定義動作のすべてを 捕捉するわけではありません テスト中にエラーが発見されない場合 予期しないランタイム動作が起こり得ます それは 問題の原因から離れて発生する デバッグしにくいクラッシュかもしれません 更には ユーザデータの破損など プログラムが誤った動作をする可能性もあります データの破損や消失は クラッシュよりもさらに深刻な問題です 安全でない領域により深く入ると 間違いを見つけるのがより難しくなり 問題もより複雑になります バグが発生したかなり後まで 問題が発生しないこともあります コードが安全でない場合を理解するために ポインタを見てみましょう Swiftは ポインタを使用せずに プログラミングするよう設計されています 安全でない理由を考えれば 回避するべき理由が分かります 下位レベルAPIを使用して 直接メモリにアクセスすることがある場合には 安全性の異なる側面を管理する方法を 知っておくと便利です 変数のストレージや配列の要素 または直接割り当てたメモリに ポイントする必要があるかもしれません そのオブジェクトにポイントする前に 安定したメモリ位置が必要です ポイントする先の安定したストレージの 生存期間は限られています その理由は スコープ外になるか または 直接メモリの割り当てを解除するかです しかし ポインタの値には 独自の生存期間があります ポインタの生存期間が ストレージの生存期間を超える場合 アクセスしようとする試みは未定義です これがポインタが安全でない主な理由ですが それだけではありません オブジェクトは要素のシーケンスで 構成することもできます ポインタにオフセットを追加することにより ポインタを異なるメモリアドレスに移動できます それは異なる要素のアドレスを指定する 効率的な方法です しかし 加算または減算が 大きすぎるオフセットは 同じオブジェクトに属していないメモリに ポイントします オブジェクトの境界を越えたポインタへの アクセスは定義されていません このセッションでは 見落とされがちな 安全性の別の側面について説明します ポインタには メモリ内の値の型とは異なる 独自の型があります それらの型の一貫性をどのように保証しますか? また そうでない場合の影響は? Int16型のストレージへのポインタを要求すると Int16へのポインタが返されます ここまでは問題ないでしょう Swiftでは間違った型へのポインタを 取得することは かなり難しいです 同じメモリを異なる型 ここではInt32で上書きするとします その時点では 正しいInt32型へのポインタが作成されますが Int16のポインタも まだ残っているかもしれません ポインタ型とインメモリの型が合っていないので 元のInt16型のポインタにアクセスすることは 未定義動作となります 皆さんは思うでしょう “なぜ未定義動作が プログラムのクラッシュよりも問題なの?” “ポインタ型がそれを発生させるのは なぜ?” それを理解するために 非常に安全でないコードをいくつか見ていきます このようなコードを 書く人がいるとは思いませんが SwiftコードがCからポートされ 引き続き古いCコードの一部を呼び出す場合に Swiftコードがどのように見えるかに 驚かれるかもしれません しかし 問題を把握するために下位レベルの型の すべてを理解する必要はありません メモリ内のイメージデータへの スタンドアローンのポインタと イメージカウントの別のプロパティを持つ collage structがあると考えてください この型はCから インポートされたのかもしれません
また 関数addImagesは メモリにイメージデータを書き込み イメージカウントを増加させます
addImagesを呼び出す際 collage struct内の イメージカウントを更新する必要がありますが structのimageCount型とInt そしてUInt32への関数の引数ポインタの間に 不一致があります
正しい型の新しいカウント変数を作成し Swiftの整数変換を使うのが安全策です しかし この複雑なコードの行は 直接structへのポインタを作成しています その後コードはイメージカウントを再度読み取り saveImagesに渡す必要があります 問題は ランタイム時に このカウントがゼロになり つまりプログラムがすべてのイメージを失う 可能性があることです カウントプロパティにInt型を与え ポインタにUInt32型を与えることにより コンパイラにそれらの値が異なる メモリオブジェクトに存在することを通知します コンパイラはIntオブジェクトへの 更新を認識しないため 初期化の値0を再利用することができます コンパイラはある程度エラーに寛容なので 実際には そのような失敗はないでしょう しかし 何が起こるか予測はできません コンパイラにとって 型情報は assumptionの基になっている事実です コンパイラが不十分なassumptionを行うと それがコンパイラのパイプラインを通して浸透し 驚くような形で現れることも あり得えます コンパイラの2つのバージョンによって 引き起こされる異なるプログラムの動作などです ポインタ型のバグは クラッシュよりも悪い形で プログラムの誤作動を引き起こすことがあります 更に困ったことに それは見つけにくいのです プログラムは正常に動作しているように見えても バグが何年もコード内に残存することがあります 後からソースに対し安全そうな変更をすることで 問題が露見することがあるかもしれません または 定期的なコンパイラのアップデート後に 問題が現れ コード変更をしていないのに プログラムが異なる動作をするかもしれません ポインタ型の安全性については Swift以前から課題としてありました ポインタ型をCで正しく使用する方法を知るには 言語仕様の深い知識が必要です “strict aliasing”や “type punning”などの 用語を調べると詳しい説明がされています 幸いなことに そのようなルールを知らなくても Swiftのポインタは安全に使用できます SwiftからCにポインタを渡すことは一般的ですが 安全に相互運用するため Swiftポインタは 最低でもCと同じ程度 厳密である必要があります SwiftのUnsafePointerは Cポインタの下位レベル機能の ほとんどを提供します 皆さんは オブジェクトの生存期間と オブジェクトの境界を管理する必要があります セッション“Unsafe Swift”では その方法を説明します しかし 皆さんが型安全性に対して 責任を負う必要はありません UnsafePointerのジェネリック型パラメータは コンパイル時に適用され タイプセーフなAPIになります それを確認するために ポインタ型の 安全性に関するSwiftのルールを見てみましょう UnsafePointerの型パラメータは メモリ内に 保持されることが予想される値の型を示します これを型付きポインタと呼びます Swiftでは 型付きポインタのルールは 厳密で単純です 概念上は メモリ状態にメモリ位置が バインドされている型が含まれます そのメモリ位置は その型の値のみを保持できます タイプセーフAPIとして UnsafePointerは メモリから型の値を読み取りのみを UnsafeMutablePointerは その型の値の 読み取りまたは書き込みのみを行います
当然 バイトがメモリ内で 正しくレイアウトされている限り ポインタ型は問題にならないと考えられます また Cでは 両方のポインタが 引き続き同じメモリを参照している状態で ポインタを異なる型にキャストするのも 珍しくありません それが実際にCで正当であるかは 様々な特殊ケースによります 型パラメータがメモリ位置のバインドされた型と 一致しないポインタへのアクセスは Swiftでは 常に未定義動作です これを防ぐため Swiftでは同様のCスタイルに ポインタをキャストすることができません このようにしてSwiftの型システムにより コンパイル時にポインタ型が適用されます 追加のランタイム情報や型情報を メモリに保存したり 追加のランタイムチェックを 実施したりする必要もありません メモリが型にバインドされる方法と 型付きポインタが生じる場所を見てみましょう Int型の変数を宣言して 変数のストレージへのポインタを要求すると 変数宣言と一貫性のある pointer to Intが戻ります 配列ストレージは 配列要素型にバインドされます そして配列ストレージへのポインタを要求すると 配列要素型へのポインタが得られます UnsafeMutablePointer上で 静的な割り当てメソッドを呼び出せば 直接メモリを割り当てることもできます 割り当てはメモリを その型パラメーターにバインドし 新しいメモリへの 型付きポインタを返します これは変数や配列へのポインタとは異なります この状態遷移図が示すように 初期値を何も保持していなくても メモリは既に型にバインドされているからです 割り当てが与える型付きポインタを使用すると メモリを適切な型にのみ初期化できます 初期化状態でメモリは再アサインされます アサインメントは黙示的に 前のインメモリの値の初期化を解除し メモリを同じ型の新しい値に 再度初期化します 同じ型付きポインタを使うと メモリの初期化を解除できます メモリはまだ同じ型にバインドされていますが 割り当ての解除は安全です こうした手順は変数と配列のストレージで Swiftによって自動的に処理されます 直接的な割り当てでは メモリの初期化状態を 管理する責任は皆さんにありますが 型の安全性はSwiftが確保します 型付きポインタのルールは 単純で厳密なため 一般的に型が一致しない同じメモリの場所に 2つのアクティブポインタは持てません しかし複合型の例を見てみましょう 型MyStructの値を持つメモリの ブロックがあります 外側の構造体へのポインタまたは プロパティへのポインタいずれかが取得可能で それらは両方とも同時に有効です メモリがバインドされている先の型を変更せずに どちらか一方にアクセスできます これでもポインタの安全性の 基本ルールに従っています メモリが複合型にバインドされている場合 メモリ内でレイアウトされているため その型のメンバーにも有効にバインドされます
Swiftの型付きポインタを使えば メモリへ直接アクセスできますが それは型の安全性の範囲内のみです 型が一致しない同じメモリに 2つの型付きポインタは持てません そのためメモリのバイトを 異なる型として再解釈するには 下位レベルのAPIを使う必要があります UnsafeRawPointerを使うと バイトが表わす値の型を指定しなくても バイトのシーケンスを参照できます メモリレイアウトは皆さんが管理します 生ポインタではメモリからバイトをロードする際 バイトを型付きの値として解釈します 型付きポインタを使ってInt64に初期化された メモリのブロックで考えてみましょう 型付きポインタから生ポインタへの ダウンキャストは常に可能です 生ポインタでのオペレーションは メモリ内のバイトシーケンスのみを見ます メモリのバインドされる型は重要でないので 生ポインタはどの型でもロードできます 必要なバイト数を読み 要求されている型にそれを集合させます 例えばロードをUInt32として呼び出すと 4バイトが現在のアドレスからロードされ UInt32の値を生成します 対象とするプラットフォームのエンディアンに 責任を持てば より小さな型のロードも可能です また生ポインタを使って 値のバイトをメモリ内に書くこともできます バイトの保存はインメモリの値を修正するので ローディングとは非対称です 型付きポインタを使用した アサイメントとは異なり 生バイトの保存は メモリ内の前の値の初期化を解除しません メモリにオブジェクト参照が含まれていない ことの責任は皆さんが持ちます 例では 生ポインタ上でstoreBytesを呼び出して UInt32値から4つのバイトを抽出し それをインメモリのInt64値の 4つの上位バイトに書き込みます バイトはメモリ内に書き込まれる際 メモリのバインドされた型として再解釈されます つまり既にメモリ内の値にポイントしている 型付きポインタを使ってアクセスするのです 生ポインタを型付きポインタに キャストして戻すことはできません それを行うとメモリのバインドされた型と 矛盾するからです この場合はInt64へとUInt32への ポインタが両方とも 重複するメモリに対するものになります 型付きポインタからのキャストだけが 生ポインタの取得方法ではありません withUnsafeBytes APIは クロージャの継続時間に生バッファとして 可変ストレージを利用可能にします UnsafeBufferPointerが 型付き値のコレクションであるのと同様に UnsafeRawBufferPointerは バイトのコレクションです ここではバッファカウントは 変数の型のバイトでのサイズです コレクションインデックスは バイトのオフセットで インデックス付き要素を読むと そのバイトのUInt8値が付与されます また変数の生ストレージの修正もできます withUnsafeMutableBytesは ミュータブルバイトのコレクションを提供し UInt8の値を特定のバイトオフセットに 保存できるようにします 配列がwithUnsafeBufferPointerメソッドを 持つのと同様に 配列要素が生ストレージを利用できるようにする withUnsafeBytesメソッドも持ちます バッファサイズは要素ストライドで乗算される 配列のカウントになり そのバイトの一部は要素アライメントのための パディングにもなり得ます Foundationのデータ型は バイトのコレクションを回すのによく使われます データにはwithUnsafeBytesメソッドもあり クロージャの継続時間に 根底の生ポインタを利用可能にします ここではそれを使って UInt32を選択したバイトオフセットで読みます UnsafeMutableRawPointer上で 静的割り当てメソッドを呼び出すことにより 生メモリを直接割り当てることができます ここでは皆さんがバイトでのメモリサイズと アライメントを計算する責任を持ちます 生の割り当て後 メモリ状態は 初期化も型にバインドされてもいません 生ポインタでメモリを初期化するには メモリが保持する値の型の指定が必要です 初期化はメモリをその型にバインドし 型付きポインタを返します 初期化されたメモリへのトランジションは 一方向のみです メモリの初期化を解除するには インメモリ値の型を知っておく必要があります つまり生ポインタで 初期化を解除する方法はありません 初期化によって返された型付きポインタで 初期化を解除します 型付きポインタのメモリ状態遷移図は 先ほど見ました 初期化されていない状態であれば 生ポインタでメモリの割り当てを解除できます メモリが型にバインドされているかは 割り当ての解除には関係ありません 型付きポインタでのメモリの割り当ては より安全で便利なので推奨されるべきです しかし それでも生ストレージを 割り当てる理由を例で説明します 可変長の同じ連続したメモリのブロックに 関連のない型を保存したいとします バイトでの全体のサイズとアライメントを計算後 allocateの生バージョンを呼び出します これでバイトの連続ブロックへの 生ポインタが与えられます そのメモリの一部をヘッダーとして初期化すると ヘッダー型へのポインタが与えられます
ヘッダーのバイトオフセットを追加後 残りのバイトを整数に初期化します すると整数のみを保持するメモリのリージョンへ 個別の型付きポインタが与えられます このストレージの割り当てテクニックは 標準のライブラリ型の実装には優れていますが 通常は手を出したいものではありません 生ポインタはハイパフォーマンスなデータ構造を 実装するのには優れたツールですが あまり使いすぎたくはありません バイトオフセットやデータアライメントで 微調整を行うには注意が必要です 生ポインタの使用が好まれるケースとしては 外部で生成されたバイトのバッファを Swift型にデコードしたい場合です UnsafeRawBufferPointerの ロードAPIを使い 最初にディスクリプタを読んで 順次データのサイズと型を判断します 増加のバイトオフセットでロードAPIへの より多くのコールを用いてフォローアップし ストリームからデコードしたい型を 毎回指定します 生ポインタは型の安全性の 重要な水準を保ちます 生ポインタが使用されるポイントでの メモリレイアウトの責任は皆さんが持ちますが いつ型付きポインタを使うのが適切かに 生ポインタは影響を与えません 生ポインタを使っても 同じメモリや型付きポインタを使う 危険性は変わりません 最も深いレベルはメモリのバインドされた型を 利用可能にするAPIです これを使う場合 ポインタ型の安全性の責任は 皆さんがすべて持ちます その前に より上位レベルのAPIを 1つ見てみます ポインタ型の強制をいつ回避するか 知っておく必要があります メモリのバインドされた型を参照するAPIを 明示的に呼び出す必要があるためです
型の安全性を回避すると 型付きポインタが使われているコードのどこかに 容易に未定義の挙動が作られる危険があります
ルールが1つだけあります 型付きポインタへのアクセスは メモリのバインドされた型に一致させます 単純ですが簡単ではありません コードの様々な部分が メモリ型に一致している必要があり コンパイラは導いてくれないからです そのような危険なAPIを使用する理由と なぜ安全に使えるのかご説明します まれにコードが型付きポインタを 保存しない場合があります 仮に生ポインタだけだとしても メモリがバインドされている型が分かれば 理解していることをSwiftに伝え 型付きポインタを返すことができるはずです この例では生メモリを保持する コンテナがありますが 変数pointsToIntもあり メモリが整数値のみを保持できるか分かります assumingMemoryBound toを 生ポインタ上で呼び出し 引数としてInt型を与えることで 整数への型付きポインタが返されます メモリを割り当てた際に 型Intにバインドされたので安全です メモリを保証できる場合の コールassumingMemoryBound toのみが 望む型にバインドされます ランタイム時にはチェックされません コンパイラに想定をするよう 依頼しているだけなので 想定の正確性は皆さんが責任を持ちます assumingMemoryBound toが必要な別の例です 今回はC APIのpthread_createを 呼び出しています まずカスタムThreadContext型で コンテクストポインタを初期化します pthread_createを呼び出したら コンテクストポインタに渡します しかしpthread_createが新しいスレッドで スタートルーチンにコールバックすると 生ポインタのみが与えられます C関数はコールバックに ボイドアスタリスク引数を宣言し それがUnsafeMutableRawPointerとして インポートされます Cでは時に同様の事が発生し 全般的にタイプセーフにする方法はありません この場合は assumingMemoryBound toを呼び出して 型付きコンテクストポインタを 回収するのが安全です それは数行上でメモリを割り当てた際に メモリをバインドしたばかりの型だからです 最後の数例では 元のポインタ型は消去されました 型付きポインタがあっても型の構成の中で 間違ったレベルにあることもあります これはポインタを整数に持っていく関数です Intsのタプルがある場合 タプル要素へのポインタを その関数に渡せるはずです ポインタをタプルのストレージに入れるため withUnsafePointerを呼び出す必要があります しかし私たちの関数型と互換していない タプル型へのポインタが返ってきます メモリは一度に1つの型にのみバインドできます ただタプル型は複合型なので タプル型にメモリをバインドすると 要素型にもバインドされます つまりタプルのストレージの整数へのポインタを 使用することはタイプセーフですが 非タイプセーフなAPIを使用して そのポインタ型を取得する必要があります まず生ポインタを構築して 故意的にタプル型の型を消去します それからassumingMemoryBound toを使用して 整数へのポインタを作成します このようにポインタをメンバー型に下げるには 複合型のレイアウトを知っている必要があります Swiftは要素がすべて同じ型であるタプルは 要素型のストライドに従って 1つの値から次へと標準のパターンで レイアウトされるよう実装されています structプロパティへの適用を見てみましょう これもポインタを整数に持っていく関数です 今度はタプルの代わりに 整数プロパティを持つstructがあります withUnsafePointerは 外側のstructへの型付きポインタを提供します MemoryLayout APIを使用して 値のプロパティのバイトオフセットを計算します structポインタを生ポインタにダウンキャストし そのバイトオフセットを追加することにより 値のプロパティへの生ポインタを取得します プロパティのメモリは常に プロパティの宣言型にバインドされているので assumingMemoryBound toを呼び出して 整数へのポインタを取得することは安全です 一般的にstructプロパティのレイアウトは 保証されていないので structプロパティへのポインタを取得したら そのプロパティの単一の値にのみ使えます structプロパティへのポイントは一般的なので 安全でないAPIを回避する簡単な方法があります in out引数として渡されたプロパティは コンパイラが黙示的に その関数の引数に対して 宣言された安全でないポインタ型に変換します assumingMemoryBound toが コンパイラに伝え メモリのバインドされた型について 未チェックの想定が作成されます bindMemory APIが実際に メモリのバインドされた型の変更を可能にします まだ型にバインドされていないメモリの場所なら 初めてバインドされます メモリが既に型にバインドされていれば その型をリバインドし メモリ内にあった値はいずれも 新しい型を取ります 2つのUInt16値を保持するよう メモリのブロックを割り当てるとします そのメモリブロックへの生ポインタを求めます 生ポインタ上でbindMemoryを呼び出して 配置されている型を単一のInt32の値に変えます 単なるビット単位の変換なので 通常の型変換で発生する 安全性チェックはありません 実際 ランタイム時には何も起きません bindMemoryは型がメモリの場所で変更された というコンパイラへの宣言です メモリにアクセスするのに使用すべき Int32ポインタを返します 古いUInt16ポインタへのアクセスは未定義です プログラムのどのポイントでもメモリの場所は 単一の型にのみバインドされています メモリのバインドされた型の変更は メモリを物理的には修正しませんが メモリ状態のグローバルプロパティを 変更すると考えてください 2つの理由でタイプセーフではありません まず配置されている生バイトを解釈し直します そのため生ポインタを使用する場合と同様に メモリ内の型のレイアウトに関しては 皆さんが責任を持ちます しかしメモリをリバインドするのは 生ポインタの使用よりも危険です 既存の型付きポインタを 無効にすることでもあるからです ポインタのアドレスはそのまま有効ですが メモリが間違った型にバインドされている間の アクセスは未定義です 変数や配列のような宣言型を伴うオブジェクトの ストレージへのポインタがある場合 そのポインタを使ってメモリをリビルドすると オブジェクト自体を無効にすることがあります bindMemory APIは Swiftでは下位レベル言語のプリミティブです 通常のコード向けではありません しかし現実ではメモリ型をリバインドしたい 場面が出てきます これは あるデータの型において 一致しない複数の外部APIがあり データをコピーして行ったり来たりすることは 回避したい場合です ポインタの型の安全性が慎重に 検討されなかったC APIで起こりやすいです ここにUInt8ポインタを取る関数と 型Int8のメモリへのポインタがあります Swiftはポインタ型を強制するので そのポインタを関数に渡すことはできません 適切な型で新しいメモリのブロックを割り当て そのデータをコピーすることは可能です これはポインタ型に関しては安全ですが遅いです コールの持続時間に対して メモリを再解釈すればよいだけなので withMemoryRebound to APIを 使うことができます UnsafePointerの場合と同様に withMemoryRebound toを使えば クロージャのスコープに有効だと保証された ポインタが与えられます Int8ポインタはクロージャ内の どこにも使用されていないため クロージャ内で メモリをリバインドするのは安全です クロージャが返されたら withMemoryRebound toが メモリを元のInt8型にリバインドします これで周辺コードではメモリは 型付きポインタアクセスから独立します クロージャ内のコードを判断すれば 使用は安全だと証明できます withMemoryRebound to APIは周辺コードに関し メモリのバインドを安全にしますが 厳密な制限がいくつかあります そのためまだbindMemoryを 直接呼び出す必要があるかもしれません その場合には 安全性の判断と同じテクニックを使います 管理されたスコープ内のポインタを 取得するためだけにbindMemoryを使うのです スコープの終了時は メモリをリバインドして元の型に戻してから 他のコードが前に取得したポインタを使って 同じメモリにアクセスできるようにします Memory-binding APIについて まとめましょう assumingMemoryBound toは生ポインタから 型付きポインタを回収する意図的な方法です メモリが既にその型にバインドされていることを 知っている必要があるので危険です bindMemoryは下位レベルのプリミティブで メモリのバインドされている型の状態を変えます 型付きポインタがコード内の 別の場所でアクセスされる場合に 未定義な挙動が発生する可能性があるので とりわけ危険です withMemoryRebound toは比較的安全な方法で 必要な時に一時的にメモリをバインドします 根底のメモリをコピーせずに 型の一致しないC APIを呼び出すのに役立ちます bindMemory APIはよく間違って 単にメモリから異なる型を読むのに使われます このコードはbindMemoryを呼び出し 読みたい型のポインタを取得します その型付きポインタは 値を読むのに必要なだけです しかし型付きポインタを作成する中で 私たちはメモリ状態を変更し おそらく他のポインタを無効にしました 型を再解釈したいだけの場合 ポインタ型の落とし穴を回避するには UnsafeRawPointerのロードAPIを使います 皆さんはメモリレイアウトに対して 責任を持つだけでよいのです この方法はいつでも使えます メモリへの型付きポインタがある場合 生ポインタにキャストすることができます 変数や配列または データオブジェクトがある場合 withUnsafeBytesメソッドで 生バッファへ直接アクセス可能になります メモリのリージョンを特定の要素型を持つ要素の シーケンスとして見るとします しかし根底のストレージは 生ポインタとして利用可能で コードの別の部分では 異なる型として見られるかもしれません その生ポインタを中心にラッパーを簡易に作成し 自身の要素型を保持します BufferViewと呼びましょう 自動的にメモリを管理する必要がないよう 名前にUnsafe接頭辞を追加して デバグビルドへの境界チェックを制限します 生バッファ上で BufferViewを作成するには 要素ストライドに基づいて バッファに収まる要素数を計算します もちろん事前条件も追加しバッファの バイト数とアライメントが適切か検証します ここでインデックス付き要素を読むために バイトオフセットを計算して 生バッファに要素型をロードするよう依頼します ポインタ型に関しては 生メモリからのローディングは安全なので 他のコードが同じメモリをどう見るか 心配する必要はありません BufferViewを使えば要素型を維持しながら バイトのシーケンスを安全に再解釈できるので 型付きポインタを使用する必要はありません 最後にポインタ型を扱う際の 戦略を確認しましょう 最善の戦略はできる限り ポインタの使用を回避することです
まれに同じメモリの場所を異なる型として 再解釈する必要がある場合は 慎重に選択すべきです 型付きポインタはメモリのバインドされた型と 常に一致している必要があるので 型の再解釈には使わないのが最善です Cコードではポインタ型をキャストすることで これに対処しますが Swiftではそのために 生ポインタに基づいたAPIが提供されています 純粋なSwiftコードでも有効なAPIです 例えばバイトストリームからの値を デコードする時に使えます またはSetのような連続するメモリで異なる型を 保持するコンテナを実装するのにも有効です Swiftでのポインタ型の安全性について 見かけほど難しくないと 感じてもらえたら幸いです ご視聴ありがとうございました
-
-
5:44 - Images: undefined behavior can lead to data loss
struct Image { // elided... } // Undefined behavior can lead to data loss… struct Collage { var imageData: UnsafeMutablePointer<Image>? var imageCount: Int = 0 } // C-style API expects a pointer-to-Int 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
-
10:06 - Direct memory allocation
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() }
-
14:24 - Using a raw pointer to read from Foundation Data
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))
-
14:37 - Raw allocation
func rawAllocate<T>(t: T, numValues: Int) -> UnsafeMutablePointer<T> { let rawPtr = UnsafeMutableRawPointer.allocate( byteCount: MemoryLayout<T>.stride * numValues, alignment: MemoryLayout<T>.alignment) let tPtr = rawPtr.initializeMemory(as: T.self, repeating: t, count: numValues) // Must use the typed pointer ‘tPtr’ to deinitialize. return tPtr }
-
15:43 - Contiguous allocation
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) }
-
18:03 - Using assumingMemoryBound(to:) to recover a typed pointer
func takesIntPointer(_: UnsafePointer<Int>) { /* elided */ } struct RawContainer { var rawPtr: UnsafeRawPointer var pointsToInt: Bool } func testContainer(numValues: Int) { let intPtr = UnsafeMutablePointer<Int>.allocate(capacity: numValues) let rc = RawContainer(rawPtr: intPtr, pointsToInt: true) // ... if rc.pointsToInt { takesIntPointer(rc.rawPtr.assumingMemoryBound(to: Int.self)) } }
-
18:40 - Calling pthread_create
// Use assumingMemoryBound to recover a pointer type from a (void *) C callback. /* func pthread_create(_ thread: UnsafeMutablePointer<pthread_t?>!, _ attr: UnsafePointer<pthread_attr_t>?, _ start_routine: (UnsafeMutableRawPointer) -> UnsafeMutableRawPointer?, _ arg: UnsafeMutableRawPointer?) -> Int32 */ import Darwin struct ThreadContext { /* elided */ } func testPthreadCreate() { let contextPtr = UnsafeMutablePointer<ThreadContext>.allocate(capacity: 1) contextPtr.initialize(to: ThreadContext()) var pthread: pthread_t? let result = pthread_create( &pthread, nil, { (ptr: UnsafeMutableRawPointer) in let contextPtr = ptr.assumingMemoryBound(to: ThreadContext.self) // ... The rest of the thread start routine return nil }, contextPtr) }
-
19:26 - Pointing to tuple elements
func takesIntPointer(_: UnsafePointer<Int>) { /* elided */ } func testPointingToTuple() { let tuple = (0, 1, 2) withUnsafePointer(to: tuple) { (tuplePtr: UnsafePointer<(Int, Int, Int)>) in takesIntPointer(UnsafeRawPointer(tuplePtr).assumingMemoryBound(to: Int.self)) } }
-
20:26 - Pointing to struct properties
func takesIntPointer(_: UnsafePointer<Int>) { /* elided */ } struct MyStruct { var status: Bool var value: Int } func testPointingToStructProperty() { let myStruct = MyStruct(status: true, value: 0) withUnsafePointer(to: myStruct) { (ptr: UnsafePointer<MyStruct>) in let rawValuePtr = (UnsafeRawPointer(ptr) + MemoryLayout<MyStruct>.offset(of: \MyStruct.value)!) takesIntPointer(rawValuePtr.assumingMemoryBound(to: Int.self)) } }
-
21:17 - bindMemory(to:capacity:) invalidates pointers
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() }
-
23:13 - withMemoryRebound(to:capacity:) API
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 }
-
25:49 - BufferView: Layering types on top of raw memory
struct UnsafeBufferView<Element>: RandomAccessCollection { let rawBytes: UnsafeRawBufferPointer let count: Int init(reinterpret rawBytes: UnsafeRawBufferPointer, as: Element.Type) { self.rawBytes = rawBytes self.count = rawBytes.count / MemoryLayout<Element>.stride precondition(self.count * MemoryLayout<Element>.stride == rawBytes.count) precondition(Int(bitPattern: rawBytes.baseAddress).isMultiple(of: MemoryLayout<Element>.alignment)) } var startIndex: Int { 0 } var endIndex: Int { count } subscript(index: Int) -> Element { rawBytes.load(fromByteOffset: index * MemoryLayout<Element>.stride, as: Element.self) } } func testBufferView() { let array = [0,1,2,3] array.withUnsafeBytes { let view = UnsafeBufferView(reinterpret: $0, as: UInt.self) for val in view { print(val) } } }
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。