Swift 와 C 포인터(Pointer)
Swift 는 문법으로도 다양한 기능을 제공하는 고급 언어이다. 하지만 고급 언어이기 때문(?)에 최적화된 C 라이브러리를 종종 사용해야 할지도 모르고 그럴 때는 C의 포인터를 함께 사용해야 할 가능성도 있다. 그래서 Swift의 포인터 처리에 대해 간단히 정리하려 한다.
참고로 이 글은 Swift 3 가 등장하기 이전에 쓰여졌다. Swift 3 에서의 포인터는 [Swift 속의 C Pointer 이야기 - 시작] 글을 참고하자.
UnsafeMutablePointer 타입의 initialize 메소드는
위와 같은 식으로 쓰는게 C 포인터 예제의 Swift 판(?) 이겠지만, Swift 에서는 위와 같은 식으로는 쓸 일이 별로 없을 것 같다는 생각이다. 굳이 일부러 포인터 데이터 변동용 클로져를 만들어도 쓸 만한 곳은 제약이 좀 있다.
아래 예제는 UnsafeMutablePointer 타입을 활용해 Int 타입 1개 분의 메모리를 할당하는 예제이다.
이 포인터는 memory 라는 프로퍼티를 이용해 엑세스가 가능하다.
포인터의 사용이 다 끝나면 메모리를 해제해야 한다.
Swift에서는 아래와 같은 식으로 이용이 가능하다.
당연하게도 Swift로의 이주는 'C 함수와의 결별' 을 의미하는 거라고 생각한다. 하지만 그렇다고 C나 Objective-C 코드를 버릴 단계는 아직은 아니다. 미래가 어떻게 바뀔지는 모르겠지만 지금의 컴퓨터 발전은 정체기이다보니 아직은 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 // 3list 는 Array<Int> 타입이다. 이를 UnsafeMutableBufferPointer를 이용해 버퍼포인터 형식으로 치환한 다음 baseAddress 프로퍼티를 통해 실제 포인터(UnsafeMutablePointer 타입)를 구할 수 있다. 즉 필요하다면 이 baseAddress 를 C 함수에 넘겨주면 된다.
마무리
여기까지 본 내용들은 Unsafe 하고 Mutable 한 포인터만 봤지만 변경가능성(Mutable)을 제외하면 안전하지 않다(Unsafe)는 점은 일단 고려하지 않아도 된다. 어차피 포인터 연산 자체가 완벽하게 안전하진 않다. 물론 COpaquePointer 같이 독특한 포인터 타입이 있지만 이 녀석들은 사용이 상당히 제한적으로 아마도 Mutable한 포인터의 사용이 가장 많을테니 UnsafeMutablePointer 위주로 살펴보게 되었다.당연하게도 Swift로의 이주는 'C 함수와의 결별' 을 의미하는 거라고 생각한다. 하지만 그렇다고 C나 Objective-C 코드를 버릴 단계는 아직은 아니다. 미래가 어떻게 바뀔지는 모르겠지만 지금의 컴퓨터 발전은 정체기이다보니 아직은 C 코드의 유용함은 이어지리라 생각한다. 그래서 포인터의 사용법은 어떻게든 알아둬야 한다고 생각한다.
그나저나 이 글은 참 이해시키기가 어려운 주제 같다. 포인터는 C를 배우는 이들에게 가장 큰 걸림돌로 알려져 있는데 =_=; 이 정도로 해도 되는걸까?
[관련글] 스위프트(Swift) 가이드
댓글
그런데 스위프트에서도 포인터를 사용해야하는 경우가 자주 생기나요?
항상 올려주시는 글 잘 보고 있습니다.
불행히도 아직은 상황에 따라 CoreFoundation이나 Carbon쪽 함수들을 꼭 써야 하는 경우가 있기 때문에 아예 안쓸수는 없습니다. 하지만 이런건 그다지 많지는 않겠지요.
개인적으론 C나 C++ 등의 언어로 루틴 최적화 하는게 아니라면 포인터를 몰라도 문제없다는 생각입니다.