2015년 6월 30일 화요일

Swift 와 C 포인터(Pointer)

Swift 는 문법으로도 다양한 기능을 제공하는 고급 언어이다. 하지만 고급 언어이기 때문(?)에 최적화된 C 라이브러리를 종종 사용해야 할지도 모르고 그럴 때는 C의 포인터를 함께 사용해야 할 가능성도 있다. 그래서 Swift의 포인터 처리에 대해 간단히 정리하려 한다.

참고로 이 글은 Swift 3 가 등장하기 이전에 쓰여졌다. Swift 3 에서의 포인터는 [Swift 속의 C Pointer 이야기 - 시작] 글을 참고하자.

포인터의 기본

기본이라 적었지만 C 포인터에 대한 설명을 하려는게 아니다. 포인터에 대해 알고 있다고 가정한 예제이다.
int value = 10;
int *ptr_value = &value;

printf("value = %d\n", value);
// value = 10 이 찍힌다.

*ptr_value = 20;

printf("value = %d\n", value);
// value = 20 이 찍힌다.
위 C 코드 예제는 value 라는 int형 변수의 메모리 주소(즉 포인터)를 얻어서 직접 이 내용을 뜯어 고치는 기본적인 포인터 활용 예이다. 이를 Swift 식으로 표현하면 아래와 같다.
let value: Int = 10
var pointerOfValue = UnsafeMutablePointer<Int>.initialize(&value)

print("value = \(value)")
// value = 10 이 찍힌다.

// pointerOfValue 포인터가 가리키는 메모리에 20을 기록
pointerOfValue(20)

print("value = \(value)")
// value = 20 이 찍힌다.
앞서 본 C 예제와 동일한 역활을 하는 Swift 코드이다. 동일하게 value 라는 변수의 포인터를 담고 있는 녀석을 하나 만들어서 이 녀석을 이용해 원래의 value 값을 고치고 있다.

UnsafeMutablePointer 타입의 initialize 메소드는 (T) -> Void 클로져를 리턴한다. 즉 위의 경우라면 (Int) -> Void 의 클로저가 리턴된다. 따라서 pointerOfValue(10) 같은 식으로 호출 할 수 있다. 일반적인 UnsafeMutablePointer 타입을 받아오는게 아니라는 점에서 주의가 필요하다.

위와 같은 식으로 쓰는게 C 포인터 예제의 Swift 판(?) 이겠지만, Swift 에서는 위와 같은 식으로는 쓸 일이 별로 없을 것 같다는 생각이다. 굳이 일부러 포인터 데이터 변동용 클로져를 만들어도 쓸 만한 곳은 제약이 좀 있다.

UnsafeMutablePointer

UnsafeMutablePointer 라는 타입은 아마도 Swift에서 포인터를 다룰 때 가장 자주 쓰게 될 타입이라 생각된다. 포인터 변수를 만들고 이 포인터에 메모리를 할당하고 값을 엑세스 하고 메모리를 해제하는 C 수준의 작업을 처리하는게 가능하기 때문이다.

아래 예제는 UnsafeMutablePointer 타입을 활용해 Int 타입 1개 분의 메모리를 할당하는 예제이다.
var ptr = UnsafeMutablePointer<Int>.alloc(1)
ARC를 쓰기 때문에 보기 힘든 alloc 이라는 메소드가 보인다. 그 이름과 비슷하게 실제로 메모리를 할당하기 위한 용도이다. 여기서는 1 만큼 할당을 받는데 1 Byte가 아니라 sizeof(T) * 1 만큼 할당하는 예이다. 즉 여기서는 Int 타입 하나를 저장하기 위한 메모리가 할당된다.

이 포인터는 memory 라는 프로퍼티를 이용해 엑세스가 가능하다.
ptr.memory = 10
let anotherValue: Int = ptr.memory
그런데 뭐 특별한 점은 없는 것 같다. 그저 memory 프로퍼티로 데이터를 읽거나 쓰고 있을 뿐이다.

포인터의 사용이 다 끝나면 메모리를 해제해야 한다.
ptr.dealloc(1)
이게 과연 필수일까는 의문이다. 왜냐하면 UnsafeMutablePointer 타입이 ARC에 의해 해제가 되면 자동으로 할당했던 메모리도 힙에서 해제가 될 테니까.

C 함수와의 연동 예제

실제로 포인터가 포인터로써 활용될 만한(?) 별로 유용하지는 않은(?) 예제를 보자.
int make_double(int input, int *output) {
    *output = input * 2;
    return input * 2;
}
make_double 이라는 C 함수를 하나 만들었다. input 파라미터로 정수를 넘기면 output 이라는 포인터를 통해 input 의 두 배의 값을 넘겨준다. 물론 2배된 값을 리턴도 한다. 이 함수를 Swift 에서 이용해 보자.
var mem = UnsafeMutablePointer<Int32>.alloc(1)

let res = make_double(10, mem)
let output = mem.memory
// output의 값은 20
그다지 유용한 예제는 아니지만, UnsafeMutablePointer를 이용해 int 타입 포인터를 대체하는 방법이 이렇다는 예제는 될 것 같다. 굳이 output 이라는 변수는 신경 안써도 되겠지만 그러려니 하자.

메모리 블럭 엑세스

지금까지는 alloc(1)의 단위 예제만 봤지만, 이제는 좀 더 큰 메모리 블럭을 엑세스 해 볼 차례이다. 아래 C 코드 예제를 보자.
int *list_ptr = malloc(sizeof(int) * 5);

*list_ptr = 1;

int *next_ptr = list_ptr + 1;
*next_ptr = 2;

printf("%d\n", *(next_ptr - 1));   // 1
printf("%d\n", *(list_ptr + 1));   // 2

free(list_ptr);
list_ptr 은 int 타입 다섯개의 크기의 메모리 블럭을 가리키는 포인터이다. 따라서 배열 처럼 엑세스가 가능해지지만 여기서는 일부러 포인터 연산 문법을 활용했다. 어쨌건간에, list_ptr 에 1을 더하거나 빼는 행위는 int 사이즈 만큼의 포인터 연산을 의미한다.

Swift에서는 아래와 같은 식으로 이용이 가능하다.
var listPtr = UnsafeMutablePointer<Int>.alloc(5)

listPtr.memory = 1

var nextPtr = listPtr.successor()
nextPtr.memory = 2

nextPtr.predecessor().memory    // 1
listPtr.successor().memory      // 2

listPtr.dealloc(5)
C의 포인터 연산을 알고 있다면 어려운건 없다. alloc(5) 메소드를 통해 Int 5개 규모의 메모리를 할당하고 successor() 나 predecessor() 를 이용해 이 5개 규모의 메모리 포인터를 Int 타입 사이즈 만큼 이동 할 수가 있다. 마치 배열과 비슷하다. 차례대로 successor() 는 다음 포인터를 가리키고 predecessor() 는 이전 포인터를 가리킨다.

배열(Array) 포인터

Swift Array의 포인터를 얻는 방법은 뭔가 있을까? 다른 방법이 있을지도 모르겠고 또 다른 방법이 더 생길지도 모르겠지만 어쨌든 현재도 사용 가능한 예제를 보자.
var list = [1, 2, 3, 4, 5]
var listPtr = UnsafeMutableBufferPointer<Int>(start: &list, count: list.count)
var ptr = listPtr.baseAddress

ptr.memory                          // 1
ptr.successor().memory              // 2
ptr.successor().successor().memory  // 3
list 는 Array<Int> 타입이다. 이를 UnsafeMutableBufferPointer를 이용해 버퍼포인터 형식으로 치환한 다음 baseAddress 프로퍼티를 통해 실제 포인터(UnsafeMutablePointer 타입)를 구할 수 있다. 즉 필요하다면 이 baseAddress 를 C 함수에 넘겨주면 된다.

마무리

여기까지 본 내용들은 Unsafe 하고 Mutable 한 포인터만 봤지만 변경가능성(Mutable)을 제외하면 안전하지 않다(Unsafe)는 점은 일단 고려하지 않아도 된다. 어차피 포인터 연산 자체가 완벽하게 안전하진 않다. 물론 COpaquePointer 같이 독특한 포인터 타입이 있지만 이 녀석들은 사용이 상당히 제한적으로 아마도 Mutable한 포인터의 사용이 가장 많을테니 UnsafeMutablePointer 위주로 살펴보게 되었다.

당연하게도 Swift로의 이주는 'C 함수와의 결별' 을 의미하는 거라고 생각한다. 하지만 그렇다고 C나 Objective-C 코드를 버릴 단계는 아직은 아니다. 미래가 어떻게 바뀔지는 모르겠지만 지금의 컴퓨터 발전은 정체기이다보니 아직은 C 코드의 유용함은 이어지리라 생각한다. 그래서 포인터의 사용법은 어떻게든 알아둬야 한다고 생각한다.

그나저나 이 글은 참 이해시키기가 어려운 주제 같다. 포인터는 C를 배우는 이들에게 가장 큰 걸림돌로 알려져 있는데 =_=; 이 정도로 해도 되는걸까?

[관련글] 스위프트(Swift) 가이드

댓글 3개 :

jusung :

좋은글 잘 읽었습니다. ^^
그런데 스위프트에서도 포인터를 사용해야하는 경우가 자주 생기나요?
항상 올려주시는 글 잘 보고 있습니다.

Seorenn :

아마도 별로 쓸 일은 없을겁니다. 애초에 Swift 라는 언어 문법 자체가 포인터를 직접 쓰기에는 불편한데 일부러 이렇게 만든 거겠지요.

불행히도 아직은 상황에 따라 CoreFoundation이나 Carbon쪽 함수들을 꼭 써야 하는 경우가 있기 때문에 아예 안쓸수는 없습니다. 하지만 이런건 그다지 많지는 않겠지요.

개인적으론 C나 C++ 등의 언어로 루틴 최적화 하는게 아니라면 포인터를 몰라도 문제없다는 생각입니다.

jusung :

네, 답변 감사합니다.!