% swift --version
Apple Swift version 5.3.2 (swiftlang-1200.0.45 clang-1200.0.32.28)
Target: x86_64-apple-darwin20.2.0
書いたこと
- UnsafePointer系クラスの関係
- UnsafePointer
- UnsafeMutablePointr
- UnsafeBufferPointer
- UnsafeMutableBufferPointer
- UnsafeRawPointer
- UnsafeRawMutablePointer
- UnsafRawBufferPointer
- UnsafeRawMutableBufferPointer
- UnsafePointerへの暗黙的変換
- Rawポインタから非Rawポインタへの接続
- Unsafeの意味
UnsafePointer相関図
UnsafePointerには数多くのクラスが用意されていますが、immutableなUnsafePointerワールドとmutableなUnsafeMutablePointerの2つの世界に分けることができます。
UnsafePointerワールド
UnsafeMutablePointerワールド
immutableとmutableの違い
値自体には、特段の違いはありません。
また相互に初期化メソッドを通して変換も可能です。
ただmutable系はstatic関数として、allocate
メソッドが用意されています。
func makePointer<T>(withVal val: T) -> UnsafePointer<T> {
let pointer = UnsafeMutablePointer<T>.allocate(capacity: 1) // Tインスタンスを一つ作成する
pointer.initialize(to: val) // 必ず初期化する
return UnsafePointer(pointer) // UnsafePointer<T>に変換
}
typealias Couple<T> = (T, T)
let pointer = makePointer(withVal: Couple(1, 2))
print(pointer.pointee)
自身でallocateでUnsafePointerを生成すると、deallocateを呼び出さなければメモリーリークになります。
UnsafePointerへの暗黙的変換 (Implicitly bridging)
関数の実引数に&
演算子をつけて渡すことで、暗黙的にUnsafePointer,UnsafeRawPointerに変換されます。
Arrayの場合は、&
も不要です。
また関数内のみで有効なUnsafePointerになりますので、dellocateを呼び出す必要がありません。
func print<T>(atAddress pointer: UnsafePointer<T>) {
print(pointer.pointee)
}
// 暗黙的変換でわたせるのはミュータブルな変数のみ
var num = 5
print(atAddress: &num) // numは`UnsafePointer<Int>`にキャストされる
var hearts = ["💕", "💞", "💘", "💝"]
print(atAddress: &hearts) // heartsは`UnsafePointer<String>` にキャストされる
printStrings(atAddress: hearts) // 配列は&演算子が不要
/* 結果
5
💕
💕
*/
&演算子によるるキャストは関数の実引数に渡すときのみ有効です。
// NG
var num = 5
let pointer: UnsafePointer<Int> = &num
/* コンパイルエラー
error: use of extraneous '&'
let pointer2: UnsafePointer<Int> = &num
^
*/
Unsafe Buffer Pointer系のクラスは暗黙的変換ができません。
// NG
func printStrings(atAddress: UnsafeBufferPointer<String>) {}
var feelings = ["😄", "😡", "🥺", "😗"]
printStrings(atAddress: &feelings)
またUnsafePointer系のイニシャライザにおいて、&
演算子による暗黙的変換は動作未定とされていますので、NGです。
let pointer = UnsafePointer(&hearts)
/* ダングリングポインタになりますという警告が表示される
warning: initialization of 'UnsafePointer<String>' results in a dangling pointer
let pointer = UnsafePointer(&hearts)
*/
NOTE:
- ダングリングポインタ: 確保したメモリが宙に浮くこと、詳細はUnsafeの意味に記載
UnsafeRaw(Mutable)PointerからUnsafe(Mutable)Pointer<Pointee>への接続
Unsafe Raw Pointer、Unsafe Raw MutablePointerは、特定の型を指し示さないVoidポインタ型を表しています。
そのためこれらを特定の型を指し示すUnsafePointer<Pointee>、UnsafeMutablePointer<Pointee>に接続するには、bindする必要があります。
bindは、以下のメソッドを通して行います。
bindMemory(to:capacity:)
UnsafeRawPointerからUnsafePointer<Pointee>へのバインド
func printInt(_ rawPointer: UnsafeRawPointer) {
// UnsafePointer<Int>に変換
let pointer = rawPointer.bindMemory(to: Int.self, capacity: 1)
print(pointer.pointee)
}
var num = [1, 2, 3, 4, 5]
printInt(&num)
Unsafeの意味
Unsafeという名前通り、このクラスを使う時、以下のことに注意が必要です。
(特にCライブラリからグローバルな値を受け取る時に注意です。いつその中身が開放されているとかわからないですから。。。)
- メモリーリーク
- ダングリングポインタ (指し示す値が開放されている)
- 範囲外へのアクセス
メモリーリーク
Mutable系のクラスはallocate
メソッドが用意され、直接UnsafePointerを作ることができます。
ただしallocateした中身は自身が責任を持ってdeallocate
する必要があります。
struct Person {
let name: String
let age: Int
}
func main() {
let tanaka = UnsafeMutablePointer<Person>.allocate(capacity: 1)
tanaka.initialize(to: Person(name: "Tanaka", age: 24))
print(tanaka.pointee)
// deallocateを呼び出さなければメモリリーク
tanaka.deallocate()
}
main()
ダングリングポインタ
UnsafePointer系クラスは、すでにdeallocate
が呼び出されている可能性があります。
例1 deallocateを呼び出されたUnsafePointerを保持してしまっている
struct Person {
let name: String
let age: Int
}
var tanakaPointer: UnsafePointer<Person>?
func main() {
let tanaka = UnsafeMutablePointer<Person>.allocate(capacity: 1)
tanaka.initialize(to: Person(name: "Tanaka", age: 24))
print(tanaka.pointee)
// deallocを呼び出さなければメモリリーク
tanaka.deallocate()
// 中身が開放されているので、ダングリングポインタとなる
tanakaPointer = UnsafePointer(tanaka)
}
main()
if let pointer = tanakaPointer {
// 動作未定義
print(pointer.pointee)
}
例2 Cライブラリ側でダングリングポインタが発生
func callDoSomething() {
let num = 5
doSomething(&num)
// この関数が終わるとnumは廃棄される
}
func doSomething(_val: UnsafePointer<Int>) {
// cライブラリ上の関数に渡す
call_c_func(val)
}
上記、cライブラリ上でvalを持ち回してしまった場合、そのポインタ変数はダングリングポインタとなってしまっています。
範囲外へのアクセス
UnsafePointer、UnsafeRawPointerは、+
演算子によって次のポインタにアクセスが可能ですが、
範囲を超えるとクラッシュします。
func itelatePointer<T>(_ pointer: UnsafePointer<T>, count: Int) {
for i in 0 ..< count {
// +演算子で次のポインタにアクセスが出来る
print((pointer + i).pointee)
}
}
var feelings = ["😄", "😡", "🥺", "😗"]
itelatePointer(feelings, count: 4) // OK
// itelatePointer(feelings, count: 5) // NG クラッシュする
まとめ
Cライブラリの関数において、引数にポインタ型が定義されている場合、Swift側では、UnsafePointerとしてやりとりする必要があります。
また一部Fondationクラスには、UnsafePointerを受け取るメソッドが用意されています。
UnsafePointer型の場合は、中身が開放されている可能性があるため、受け取った場合は、さっさとpointeeで値を取り出し、
渡す場合は、値の中身が開放されないように、責任を持ってallocate
、dealloc
で管理しましょう。
つまり危険です。