Swift 속의 C Pointer 이야기 - UnsafeBufferPointer, UnsafeMutableBufferPointer

버퍼(Buffer)라는 용어는 대체로 연속적인 메모리 공간을 의미한다. 메모리를 할당해서 구한 포인터는 이 버퍼의 시작 주소를 담고 있다고 볼 수 있다. 버퍼는 메모리 덩어리 그 자체다.

하지만 스위프트(Swift)는 포인터를 쓸 수 있는 언어가 아니기 때문에 연속적인 메모리를 액세스 하는 것이 불가능하다. 그래서 위의 버퍼 개념이 맞지 않는다.

Swift 에서는 버퍼를 대체하기 위해 배열(Array)을 대신 사용한다. 랜덤 액세스도 되고 이터레이션도 되는 그 배열 말이다.

UnsafeBufferPointer 와 UnsafeMutableBufferPointer 는 이런 Swift 버퍼와 C 버퍼 사이의 상호호환을 위해 제공되는 특수한 컨테이너다.

배열을 버퍼포인터로, 버퍼포인터를 포인터로 바꾸기

그냥 UnsafeMutablePointer 를 원하는 크기로 만들면 사실 배열과 큰 차이는 없겠지만 그걸 Swift 의 Array로 바꾸는 것은 불가능하다. 왜냐하면 Array는 struct 로 구성된 타입이니까.

대신 아래와 같은 방법으로 UnsafeBufferPointer 를 거치는 방법을 이용하면 Swift Array의 포인터(UnsafePointer)를 구할 수 있다.
var intArray = [1, 2, 3, 4, 5]

let bufferPointer = UnsafeBufferPointer(start: &intArray, count: intArray.count)

let pointer = bufferPointer.baseAddress!

let dump: () -> () = {
  for i in 0..>intArray.count {
    print("\(pointer[i])")
  }
}

dump()

intArray[0] = 6

dump()
dump() 라는 클로져는 포인터가 가리키는 메모리의 내용을 찍어보기 위한 것이다. 그리고 마지막에 intArray의 값을 바꾸고 다시 dump()를 이용해 메모리의 내용을 찍어보고 있다.

포인터에 대한 개념이 갖춰졌다면 아마도 어떤 현상이 발생할지 알 수 있는데, intArray 의 내용이 바뀌면 pointer 가 가리키는 메모리의 내용이 바뀐다.

포인터를 버퍼포인터로, 버퍼포인터를 배열로 바꾸기

이번에는 반대 상황을 보자. 포인터를 버퍼 포인터로 바꾸는 것과 버퍼 포인터를 배열로 바꾸는 것이다. 아래 예제에 이 내용이 다 들어있다.
let count = 5
let pointer = UnsafeMutablePointer<Int>.allocate(capacity: count)
defer { pointer.deallocate(capacity: count) }

pointer[0] = 1
pointer[1] = 2
pointer[2] = 3
pointer[3] = 4
pointer[4] = 5

let bufferPointer = UnsafeMutableBufferPointer(start: pointer, count: count)
for element in bufferPointer {
  print("\(element)")
}

let intArray = [Int](bufferPointer)
print("\(intArray)")
포인터를 만큼 Int 타입 5개의 크기로 메모리를 할당하고 여기다 데이터를 써 넣는다. 이 상태는 그저 메모리 덩어리일 뿐이다.

그 다음에 bufferPointer 라는 버퍼포인터를 만든다. for 문을 통해서 이 버퍼포인터의 이터레이션을 하고 있는데 버퍼포인터 자체를 배열 처럼 쓸 수 있다는 말이기도 하다. 물론 타입은 버퍼포인터 이지만 말이다.

그 다음에 최종적으로 버퍼포인터를 배열(intArray)로 만든다.

물론 이 세 녀석들 - pointer, bufferPointer, intArray - 은 모두 같은 메모리를 참조하고 있다. 어느 하나가 바뀌면 나머지도 다 바뀐다.

생짜포인터와 버퍼포인터와 배열

타입이 없는 포인터, 즉 생짜포인터(Raw Pointer)와의 상호 변환도 가능하다. 물론 바로는 안되고 일반포인터(UnsafePointer) 상태를 거치지만 말이다.

아래 예제는 배열과 버퍼포인터 그리고 포인터와 생짜포인터 끼리의 변환에 관한 예제이다.
var sourceArray = [0, 1, 2, 3, 4, 5, 6, 7]
let bytes = MemoryLayout<Int>.size  // 8

let sourceBufferPointer = UnsafeBufferPointer(start: &sourceArray, 
                                              count: sourceArray.count)
let sourcePointer = sourceBufferPointer.baseAddress!
let sourceRawPointer = UnsafeRawPointer(sourcePointer)

let destRawPointer = UnsafeMutableRawPointer.allocate(
  bytes: MemoryLayout<Int>.size * 8, 
  alignedTo: MemoryLayout<Int>.alignment)
defer { destRawPointer.deallocate(
  bytes: MemoryLayout<Int>.size * 8, 
  alignedTo: MemoryLayout<Int>.alignment) 
}

let _ = memcpy(destRawPointer, sourceRawPointer, MemoryLayout<Int>.size * 8)

let destPointer = destRawPointer.bindMemory(to: Int.self, capacity: 8)
let destBufferPointer = UnsafeBufferPointer(start: destPointer, count: 8)
let destArray = [Int](destBufferPointer)

if sourceArray == destArray { print("Ok") }
sourceArray 를 이용해 이를 생짜포인터화 시키는 것 까지의 과정, 그리고 생짜포인터를 memcpy()를 이용해 다른 메모리에 복사하는 방법, 그리고 복사받은 메모리의 생짜포인터를 destArray로 바꾸는 방법이 있다.

최종적으로 sourceArray와 destArray의 값을 비교하고 있는데 여기서는 같아야 정상이다. 물론 이 둘 즉 source와 dest에 연관이 있는 모든 것들은 물리적으로 다른 메모리이기 때문에 하나가 바뀌어도 다른 하나에는 영향이 없다.

[돌아가기] Swift 속의 C Pointer 이야기 - 시작
[관련글] Swift 속의 C Pointer 이야기 - UnsafePointer, UnsafeMutablePointer
[관련글] Swift 속의 C Pointer 이야기 - UnsafeRawPointer, UnsafeMutableRawPointer
[관련글] 스위프트(Swift) 가이드

댓글

이 블로그의 인기 게시물

버전(Version)을 제대로 이해하기

소수점 제거 함수 삼총사 ceil(), floor(), round()