9
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

UnsafePointer相関図

Last updated at Posted at 2021-01-09
% 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ワールド

UnsafePointerワールド.png

UnsafeMutablePointerワールド

UnsafeMutablePointerワールド.png

immutableとmutableの違い

値自体には、特段の違いはありません。
また相互に初期化メソッドを通して変換も可能です。

ただmutable系はstatic関数として、allocateメソッドが用意されています。

UnsafePointerの生成.swift
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を呼び出す必要がありません。

UnsafePointerへの変換は暗黙的変換によるキャストを使うと楽.swift
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
💕
💕
*/

&演算子によるるキャストは関数の実引数に渡すときのみ有効です。

&演算子による暗黙的変換は実引数を渡すときのみ可能.swift
// NG
var num = 5
let pointer: UnsafePointer<Int> = &num

/* コンパイルエラー
error: use of extraneous '&'
let pointer2: UnsafePointer<Int> = &num
                                   ^
*/

Unsafe Buffer Pointer系のクラスは暗黙的変換ができません。

UnsafeBufferPointer系は暗黙的変換によるキャストができない.swift
// NG
func printStrings(atAddress: UnsafeBufferPointer<String>) {}

var feelings = ["😄", "😡", "🥺", "😗"]
printStrings(atAddress: &feelings)

またUnsafePointer系のイニシャライザにおいて、&演算子による暗黙的変換は動作未定とされていますので、NGです。

UnsafePointerイニシャライザでの暗黙的変換は動作未定義.swift
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>へのバインド

UnsafePointerにbindする.swift
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する必要があります。

メモリーリーク.swift
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を保持してしまっている

ダングリングポインタ.swift
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ライブラリ側でダングリングポインタが発生

Cライブラリ側でダングリングポインタが発生する可能性.swift
func callDoSomething() {
	let num = 5
	doSomething(&num)
	// この関数が終わるとnumは廃棄される
}

func doSomething(_val: UnsafePointer<Int>) {
	// cライブラリ上の関数に渡す
	call_c_func(val)
}

上記、cライブラリ上でvalを持ち回してしまった場合、そのポインタ変数はダングリングポインタとなってしまっています。

範囲外へのアクセス

UnsafePointer、UnsafeRawPointerは、+演算子によって次のポインタにアクセスが可能ですが、
範囲を超えるとクラッシュします。

範囲外へのアクセスはクラッシュする.swift
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で値を取り出し、
渡す場合は、値の中身が開放されないように、責任を持ってallocatedeallocで管理しましょう。

つまり危険です。

9
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?