Swift 속의 C Pointer 이야기 - UnsafePointer, UnsafeMutablePointer

UnsafeMutablePointer 는 지정된 타입의 포인터를 다루는 가장 일반적인 컨테이너이다. Mutable 이라는 이름에서 유추가 가능하겠지만, 이 포인터 컨테이너는 포인터가 가리키는 메모리를 조작(?)할 수 있는 녀석이다.

참고로 비슷한 이름의 UnsafePointer 는 메모리 할당과 해제 그리고 쓰기 기능이 제한되는 것을 빼면 동일한 인터페이스로 사용이 가능하다. 즉, UnsafeMutablePointer 를 알면 UnsafePointer 도 비슷하게 사용 가능하다. 제목에 있지만 실제 내용이 없는건 이런 이유에서다.

기본 API

간략하게 필수적인 것들만 정리하자면 아래와 같은 API를 거론 할 수 있을 것 같다.
  • 메모리 할당: allocate(capacity:)
  • 메모리 해제: deallocate(capacity:)
  • 포인터 증가: successor()
  • 포인터 감소: predecessor()
  • 포인터 엑세스: pointee or subscript
  • 포인터 랜덤액세스: advanced(by:) or subscript(index)
마치 C 에서 메모리 할당이나 해제를 위한 malloc/free 류 함수, 그리고 초기화를 위한 memset() 같은 류의 함수가 함께 섞여있는 느낌이다.

간단한 예제를 보자
let count = 10

// Allocation(메모리 할당)
let pointer = UnsafeMutablePointer<Int>.allocate(capacity: count)

// Deallocation(메모리 해제)
defer { pointer.deallocate(capacity: count) }

// Initialization(포인터가 가리키는 메모리를 0으로 채우기)
pointer.initialize(to: 0, count: count)

// Random access(n 번째 포인터가 가리키는 메모리 액세스)
for i in 0..<count {
  pointer[i] = i
}

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

dump()

// Access(포인터가 가리키는 메모리 액세스)
pointer.pointee = 11

dump()

// Access next pointer(다음 포인터가 가리키는 메모리 액세스)
pointer.successor().pointee = 12

dump()
이 예제는 포인터를 이용해 마치 10개의 아이템을 가지는 정수형 배열을 액세스 하는 듯한 사용법을 볼 수 있다. 주석으로 간단한 설명을 표기해 놨으니 읽기에는 어렵진 않을 것이다.

참고1) defer 문은 함수나 메소드 블럭이 끝날 때 호출된다. 따라서 위의 deallocate 메소드는 모든 작업이 끝난 후 호출된다.

참고2) dump() 라고 정의한 클로져는 포인터의 내용물을 확인하기 위한 것인데 그냥 적기엔 중복되는 코드가 많기에 클로져로 구현했을 뿐이다.

dump가 실행될 때 내용을 보면 어떤 식으로 동작하는지 알 수 있다.
  • 첫 dump() 에서는 0, 1, 2, 3, … 9 까지가 콘솔에 표시된다.
  • 두번째 dump() 에서는 11, 1, 2, … 9 가 콘솔에 표시된다.
  • 세번째 dump() 에서는 11, 12, 2, … 9 가 콘솔에 표시된다.

C 함수와의 연동

아래와 같은 C 함수를 Swift 에서 사용하고 한다고 치자.
void good_swap(int *a, int *b) {
  int c = *a;
  *a = *b;
  *b = c;
}
포인터를 활용한 아주 대표적인 스왑(두 값을 교환)함수이다. 물론 이거 말고도 비트와이즈 연산으로 3자 변수 없이 교환하는 것도 가능하겠지만 그런 귀찮고 가독성 떨어지는 짓을 하고 싶지는 않다.

이 함수를 Swift 에서 쓰려면 아래와 같은 식으로 쓸 수 있다.
let pointerA = UnsafeMutablePointer<Int32>.allocate(capacity: 1)
pointerA.pointee = 10

let pointerB = UnsafeMutablePointer<Int32>.allocate(capacity: 1)
pointerB.pointee = 20

print("A = \(pointerA.pointee), B = \(pointerB.pointee)")

defer {
  pointerA.deallocate(capacity: 1)
  pointerB.deallocate(capacity: 1)
}

good_swap(pointerA, pointerB)

print("A = \(pointerA.pointee), B = \(pointerB.pointee)")
Int32 가 쓰이고 있는데 C에서 사용하는 int 가 Swift 에서는 Int32 로 번역되기 때문이다.
다만 이 타입 번역은 확정적인 것은 아니라는 점을 명심하자. C에서도 명확한 타입을 쓰는게 좋다. C 에서도 int32_t 라는 확실하게 구분되는 타입을 쓸 수 있다.
첫 print 문과 두번째 print 문에서 pointerA 가 가리키는 메모리의 데이터와 pointerB가 가리키는 메모리의 데이터가 바뀌었다는 점을 알 수 있다.

정말 간단한 예제인데도 좀 복잡해 보인다. 물론 Swift 에서 포인터는 직접 다룰 수는 없기에 이렇게 귀찮게 쓰는 것이다.

희망을 좀 주자면 위 코드는 최신 Swift 3 에서는 아래와 같이 쓸 수도 있다.
​var a = Int32(10)
var b = Int32(20)

print("A = \(a), B = \(b)")

good_swap(&a, &b)

print("A = \(a), B = \(b)")
사실 동일한 코드다. 단순한 포인터는 그냥 Swift 의 inout 방식 즉 앰퍼선드(&)를 붙여서 넘기는 것이 가능하다. 대신 포인터로 넘기기 위해서는 반드시 변수(var)로 선언되어야만 한다. let 으로 생성된 상수는 inout 으로 넘기는 것 자체가 불가능하다.

다만 이렇게 쓸 수 있는 경우는 좀 한정되어 있다는 점을 기억하자.

[돌아가기] Swift 속의 C Pointer 이야기 - 시작
[관련글] Swift 프로젝트에서 Objective-C 코드를 함께 사용하기
[관련글] 스위프트(Swift) 가이드

댓글

이 블로그의 인기 게시물

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

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