버퍼 포인터 이해하기

C에서 특정한 T타입의 배열은 메모리 상에서 연속적인 공간입니다. 이 때문에 정적 배열이든 동적 배열이든 배열을 액세스하는 것은 필연적으로 포인터와 관련됩니다. 반면 Swift의 배열에서 원소들은 반드시 이런 식으로 배치되지는 않습니다. C의 배열이 단지 원소값이 나란히 배치된 메모리 영역임에 비해 Swift의 배열은 struct로 구성되는 보다 복잡한 내부 구조를 가지고 있습니다.

이 때 T 타입이 차지하는 바이트 수가 고정되어 있으므로 배열의 시작번지와 인덱스 값을 알고 있다면 해당 인덱스에 위치한 값을 액세스할 수 있습니다. C에서 배열 이름은 암묵적으로 배열의 시작번지를 의미하므로, arr[i]로 표현되는 i 번째 원소의 값은 실제 컴파일러는 *(arr + i) 로 변환하여 접근합니다.

Swift에서도 UnsafePointer를 사용하여 포인터를 다룰 수 있는데, 이 때 범위(capacity, Pointee 타입의 메모리 사이즈 x 원소의 개수)내에는 동일타입을 구성하는 값들이 연속하여 배치되어 있습니다. 따라서 (ptr + i).pointee 와 같은 식으로 i 번째 원소에 대해 액세스가 가능합니다. 이것은 C의 접근방법과 매우 유사합니다. 하지만 이것은 단순한 메모리 연속체에서 특정 지점을 액세스하는 법일 뿐, Swift의 배열을 다루는 것과는 차이가 있습니다. Swift의 배열은 원소가 연속해있으면서 Sequence, Collection 프로토콜에 의한 여러가지 연산을 지원받습니다.

포인터와 배열의 간극을 메우는 용도로 UnsafeBufferPointer라는 래퍼 타입이 제공됩니다. 이는 특정한 포인터가 가리키는 메모리 연속체를 버퍼로 보고, 이 버퍼 내의 특정한 크기만큼의 메모리를 배열의 각 원소처럼 다룰 수 있게 해줍니다. 버퍼 포인터는 불변/가변 속성에 따라서 UnsafeBufferPointerUnsafeMutableBufferPointer로 구분됩니다.

버퍼 포인터의 Element 타입은 그 근간이 되는 UnsafePointer 타입의 Pointee와 같습니다. 이 말은 버퍼 포인터는 내부적으로 포인터에 의해 액세스되는 메모리 영역을 사용하면서 마치 Element 타입에 대한 연속열처럼 다룰 수 있는 API를 제공한다는 뜻입니다. 즉 어떤 T 타입의 포인터가 C에서 T타입 배열을 가리킨다고 할 때, Swift에서 이 포인터로 버퍼를 만들면 Swift 배열처럼 사용할 수 있게 해줍니다.

원소의 액세스

UnsafePointer<Pointee> 타입의 포인터를 기점으로 사용하여 UnsafeBufferPointer를 생성하면, 이 포인터는 마치 Array<Pointee> 처럼 사용이 가능합니다. 배열과 동일한 subscript 문법을 사용하여 i 번째 포인터가 가리키는 값을 바로 액세스할 수 있으며, 그 외에도 배열이 지원하는 for ... in 순회 라든지 map, filter, reduce 등을 사용할 수 있습니다.

// 특정 인덱스의 원소 액세스, 가변 버퍼인 경우에는 값을 변경할 수 있습니다.
buffer[2] = 99  

// for ... in 을 통한 원소 순회
for uint8 in buffer {
  print(uint8)
}

할당

버퍼 포인터를 사용해서 특정한 메모리 공간을 할당하여 바로 버퍼로 만들 수는 없습니다. 새로운 메모리를 할당하여 버퍼로 만들고 싶다면 UnsafeMutablePointer<Element>UnsafeMutableRawPointer를 사용하여 원하는 크기만큼의 메모리 공간을 할당하고, 이 포인터를 시작번지로 하여 필요한 원소의 개수만큼을 버퍼의 영역으로 지정하여야 합니다. 다음 코드는 UInt8 타입의 가변 포인터를 할당하고, 초기화한 다음 버퍼로 액세스할 수 있게 만듭니다.

let numbers: Array<UInt8> = [2, 3, 5, 7, 11, 13, 17]
let ptr = UnsafeMutablePointer<UInt8>.allocate(capacity: 8)
ptr.initialize(from: numbers, count: 8)

let buffer = UnsafeMutableBufferPointer<UInt8>(start: ptr, count: 8)

버퍼 포인터에서 시작번저를 얻으려면 baseAddress? 프로퍼티를 사용합니다. 만약 이 시작포인터가 이 예제에서처럼 수동으로 할당된 포인터라면, 버퍼 포인터의 deallocate() 메소드를 호출하여 해당 공간을 해제할 수 있습니다.

마무리

사실 Sequence 프로토콜에는 어떤 연속열이 연속된 메모리 저장공간을 사용한다면, 이를 버퍼 포인터처럼 사용하게 해주는 withContiguousStorageIfAvailable(_:) 메소드를 제공합니다. 이를 사용하여 Swift 배열을 버퍼 포인터로 변환할 수 있습니다. (참고)