Open3

Swiftでのポインタ、アドレスについて

kabeyakabeya

Swiftのポインタというかアドレスについて調べる

普通のローカル変数のアドレス

func printAddress(_ message: String, _ ptr: UnsafePointer<Int>) {
    var addr = String.init(format:"%016x", ptr)
    print(message, addr)
}
func printAddress(_ message: String, _ ptr: UnsafeBufferPointer<Int>) {
    var addr = String.init(format:"%016x", ptr.baseAddress!)
    print(message, addr)
}

var value1 : Int = 3
printAddress("value1: ", &value1)
var value2 = value1
printAddress("value2: ", &value2)
出力結果
value1:  000000000473c7b0
value2:  000000000473c7b8

value1value2は8バイト違うアドレスですね。

Arrayのアドレス

var array1 = Array<Int>(repeating: 0, count: 1)
printAddress("array1: ", &array1)
var array2 = array1
printAddress("array2: ", &array2)

array1.withUnsafeBufferPointer( { ptr in
    printAddress("buffer: ", ptr)
})
出力結果
array1:  00000000011f0350
array2:  00000000011f0350
buffer:  00000000011f0350

array1array2は同じアドレスです。普通の変数とは違い、別の変数でもアドレスが同じです。copy-on-writeと呼ばれる最適化手法が使われているということなんですね。つまりこの場合、array1array2が更新されない限りarray1array2は同じメモリ領域を共有します。
ちなみに、&で取れるポインタと、withUnsafeBufferPointerで実行されるクロージャに渡ってくるポインタは同じなんですね。

array1[0] = 3
printAddress("array1: ", &array1)
printAddress("array2: ", &array2)
出力結果
array1:  0000000001368aa0
array2:  00000000011f0350

array1の中身を変更すると、array1のアドレスが変わります。array2はそのままなので、元々のarray1のメモリ領域の所有権がarray2に移ったかのような格好です(実際、そんな所有権管理のようなことはしてないと思いますが)。

print("array1.capacity: ", array1.capacity)
出力結果
array1.capacity:  2

キャパシティは2なのでそれを超えるように、array1に要素を足します。

printAddress("array1: ", &array1)
printAddress("array2: ", &array2)
var additional = [Int](repeating: 0, count: 10000)
array1.append(contentsOf: additional)

printAddress("array1: ", &array1)
printAddress("array2: ", &array2)
出力結果
array1:  0000000001368aa0
array2:  00000000011f0350
array1:  0000000039e60020
array2:  00000000011f0350

要素を足す前と足した後で、アドレスが変わります。
この辺りは、C言語の感覚からすると違和感がありますが、C++のstd::vectorとかを考えるとまあそうなのかなという感じですね。

kabeyakabeya
// ↓この書き方はエラーになる
//var ptr: UnsafePointer<Int> = &array

&はアドレスを取得する演算子ではなくて、「inout式」と呼ばれるもの、ということですね。関数の引数にしか使えません。関数側は引数にinoutをつけるか、またはUnsafePointer系引数を使う必要があります。
ローカル変数のアドレスを簡単に取得できないようにしておかないと、array1の再配置のようなことがあったときに容易にバグにつながるから、ということでしょうね。
また、

var ptr = UnsafePointer<Int>(array1)

のような文を書くとInitialization of 'UnsafePointer<Int>' results in a dangling pointerという警告が出ます。この文のあとにarray1が変更された結果、アドレスが変わったとしてもptrは古いアドレスを指し続けるよ、ということですね。

kabeyakabeya

生のポインタのようなものを用意する

var buffer = UnsafeMutableRawBufferPointer.allocate(byteCount: MemoryLayout<Int16>.stride * 1024, alignment: MemoryLayout<Int16>.stride)
defer {
    buffer.deallocate()
}

C言語で言うところのmallocのようなものですね。
このポインタを使ってmemsetとかできます。

// C/Objective-C/C++などのコードを呼び出す
memset(buffer.baseAddress!, 0, MemoryLayout<Int16>.stride * 1024)
// 4バイト目〜5バイト目にInt16型で30をセットする。
buffer.storeBytes(of: 30, toByteOffset: 4, as: Int16.self)

bufferの中身はバイト列として直に見ていっても良いのですが、bindMemoryで、特定の型にキャストするようなことができます。

var arrayPtr = buffer.bindMemory(to: Int16.self)
var receivedData = [Int16](arrayPtr) // ここでポインタだけ渡して、要素数を指定してないことに注目
for i in (0 ..< 8) {
    print(i, " ", receivedData[i])
}

特筆すべきは、[Int16](arrayPtr)の部分ですね。ポインタだけ渡して、要素数を渡してないのに、ちゃんと配列を初期化できています。つまりUnsafeMutableRawBufferPointerは、Unsafeを名乗るわりには自分の境界を把握しています。

ポインタの配列みたいなこともできるわりに、こういう境界を把握できてたりもするので、C/C++のメモリ配置を念頭においてプログラミングするとはまりそうな予感があります。