Raw 포인터 사용에 대해

https://developer.apple.com/documentation/swift/unsaferawpointer

UnsafeRawPointer 타입은 자동 메모리 관리, 타입 안정성 및 메모리 정렬 보장이 되지 않는 원시 포인터 액세스를 제공합니다. 이 타입을 사용하려면 누수를 피하고, 할당된 메모리의 라이프 사이클을 직접 관리해야 하며, 그 외의 정의되지 않는 동작들을 회피해야 합니다. 수동으로 직접 관리하는 메모리 영역은 특정한 타입에 바운드되거나, 타입이 지정되지 않을 수도 있습니다. 메모리 영역에서 해당 영역이 특정 타입에 묶여있는지 여부와 무관하게 순수 바이트를 액세스하려할 때 UnsafeRawPointer 타입을 사용할 수 있습니다.

막 할당된 Raw 메모리는 타입화되지도 초기화되지도 않은 상태입니다. 이 메모리는 타입화된 연산을 사용하기 전에 반드시 초기화되어야 합니다. (초기화되려면 초기값을 가져야하고, 이는 타입화를 수반해야한다는 의미가 됩니다.) 초기화되지 않은 상태에서 특정 타입에 바인등하려면 bindMemory(to: count:)를 사용합니다. 이 메소드는 타입화된 포인터를 반환하며, 이후에는 해당 포인터를 사용해야 합니다.

Raw 포인터 사용에 대해 더보기

버퍼 포인터 이해하기

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 프로토콜에 의한 여러가지 연산을 지원받습니다.

버퍼 포인터 이해하기 더보기

포인터 관련 메모

타입화된 포인터

Swift는 C API와의 상호작용 혹은 고성능 자료구조의 구현등을 위해 포인터를 통한 메모리 액세스를 제한적으로 지원하고 있습니다. 기본적으로 Swift 타입 혹은 Swift 에서 인식할 수 있는 C 타입에 대한 포인터는 ‘타입화된(typed)’ 포인터라고 하며, UnsafePointer를 사용합니다. UnsafePointer는 제네릭 struct 타입으로 특정 Swift 타입에 대한 포인터로 기능합니다. UnsafePointer는 메모리 주소를 통한 액세스를 허용해주기는 하지만 값의 불변성을 보장하기 때문에 해당 포인터가 가리키는 값(pointee)를 변경하는 것을 허용하지 않습니다. 변수에 대한 포인터는 UnsafeMutablePointer를 별도로 사용합니다.

포인터 관련 메모 더보기

Swift에서 포인터를 함수에 전달하는 법

기본적으로 Swift는 C(그리고 Objective-C)와 호환이 가능하다. 이를 위해 C의 각 데이터 타입에 대응할 수 있는 Swift 타입들을 정의해 두고 있다. 따라서 C로 작성된 라이브러리를 사용할 수 있고, 이 때 해당 라이브러리의 함수들은 적절하게 Swift 버전에 맞는 시그니처로 변환된다. 예를 들어 문자열의 길이를 구하는 C함수인 strlen 함수를 Swift에서 호출하고 싶은 상황을 가정해보자. strlen 함수의 원형과 반입된 버전의 함수는 각각 아래와 같은 형태로 만들어질 것이다.

// C의 strlen 원형
unsigned int strlen(const char * s)

// C로부터 반입된 함수
func strlen(_ __s: UnsafePointer<Int8>!) -> UInt

자 그러면 Swift 문자열의 길이를 저 함수를 사용해서 구하고 싶은 상황이라고 하자. 문자열을 어떻게 UnsafePointer<UInt> 로 변환할 수 있을까? 그리고 그외에 Unsafe…로 시작하는 포인터 타입을들 받는 API는 어떤식으로 호출해야 할까?

C 함수를 호출하는 방법

Swift에서 C함수를 호출하는 방법을 다루는 내용에 대해서는 공식문서중 Using Imported C Functions in Swift에서 소개하고 있다.

먼저 C헤더에 선언된 함수들은 모두 Swift 전역 함수로 반입된다. 우선 다음과 같은 C 함수들이 있다고 하자.

int product(int multiplier, int multiplicand);
int quotient(int dividend, int divisor, int *reminder);

struct Point2D createPoint2D(float x, float y);
float distance(struct Point2D from, struct Point2D to);

이들 함수는 Swift에서 다음의 형태로 반입된다.

func product(_ multiplier: Int32, _ multiplicand: Int32) -> Int32
func quotient(_ dividend: Int32, _ divisor: Int32, _ remainder: UnsafeMutablePointer<Int32>) -> Int

func createPoint2D(_ x: Flaot, _ y: Float) -> Point2D
func distance(_ from: Point2D, _ to: Point2d) -> Float

Swift에는 아래 표와 같은 C 타입 대응 타입들을 만들어두고 있는데, 사실 이들은 호환가능한 Swift 타입에 대한 타입 별칭이다. 그리고 헤더를 반입하는 과정에서 Mapped 타입에 대응하는 Swift 타입 이름을 노출하기 때문에 기본적으로 API 상에 노출되고 있는 Swift 타입을 그대로 쓰면 된다.

C/C++ TypeMappedSwift Type
_Bool / boolCBoolBool
charCCharInt8
char16_tCChar16Int8
char32_tCChar32Uincode.Scalar
doubleCDoubleDouble
floatCFloatFloat
intCIntInt32
longCLongInt16
long longCLongLongInt64
shortCShortInt16
signed charCSignedCharUInt8
unsigned charCUnsignedCharUInt8
unsigned intCUnsignedIntUInt32
unsigned longCUngisnedLongUInt
unsigned long longCUnsignedLongLongUInt64
unsigned shortCUnsignedShortUInt8
wchar_tCWideCharUnicode.Scalar

포인터를 받는 함수

위 예에서 quotient(_: _: _:)와 같이 포인터를 받는 함수를 보면 덜컥 겁이 날 수 있을지도 모르겠다. Unsafe~ 어쩌구하는 부분에 관한 타입이나 함수가 되게 많이 있었던 것 같은데, 사실 C 함수를 사용하기 위해서 이 모든 내용을 다 알 필요도 없다. Swift 컴파일러는 “암묵적 포인터 캐스팅”이라는 것을 지원하기 때문이다. 포인터를 인자로 받는 C 함수를 Swift에서 호출하면 호환될 것 같은 Swift 타입의 변수 포인터나 배열을 넘겨주기만 하면 중간의 포인터 변환등은 Swift가 알아서해준다.

T타입에 대한 포인터를 요구하는 함수가 있을때, 암묵적 포인터 캐스팅이 적용되는 케이스와 사용방법은 다음과 같다.

  • UnsafePointer<T> 타입을 요구하는 경우, 포인터의 가변성등에 상관없이 다음 타입들을 모두 사용할 수 있다. 이들은 모두 UnsafePointer<T> 타입으로 캐스팅되어 전달된다. – UnsafePointer<T>, UnsafeMutablePointer<T>, AutoReleasingUnsafeMutablePointer<T>
  • Int8, UInt8의 포인터를 요구하는 경우, String 타입의 값을 그대로 넘길 수 있다. 문자열은 UTF8 값에 대한 널종료 버퍼로 자동 변환되고 이 버퍼에 대한 포인터가 함수로 전달된다. 이는 마치 C 문자열을 char 배열로 보는 관습과 비슷하다.
  • 문자열과 유사하게 T타입 포인터를 요구하는 인자에 Array<T> 타입의 배열을 그대로 전달해줄 수 있다.
  • T 타입 포인터에 대해서는 &을 붙인 inout 형태의 변수 표현을 사용할 수 있다.

즉 Swift에서 strlen 함수를 사용하기 위해서 굳이 UnsafePointer 타입의 값을 만들거나, withUnsafePointer() 함수를 사용할 필요는 없고, 그냥 String 값을 그대로 넘겨주면 된다는 것이다.

함수 내부에서 포인터로 넘겨받은 객체를 변경하는 경우라면, 인자의 타입이 T 타입 가변포인터로 UnsafeMutablePointer<T>를 요구하는 경우가 있다. 이 때에도 T 타입 상수 포인터를 넘겨주는 것과 거의 완전히 동일하게 사용하면 된다. 이때 넘겨주는 객체는 반드시 변수로 선언되어야 한다.

  • UnsafeMutablePointer<T> 포인터를 얻을 수 있다면 포인터를 전달한다.
  • &을 사용한 inout 표현을 넘겨준다.
  • 배열의 경우에는 앞에 &을 붙인 inout 표현을 넘겨준다.

앞서 소개한 quotient(_: _: _:) 함수의 경우에도 세번째 인자로 Int32의 가변포인터를 받는데, 역시 변수를 미리 선언하고 inout 표현으로 넘겨주면 된다.

var r: Int32 = 0
let q = quotient(37, 6, &r)
// r :-> 1

자동릴리즈되는 포인터의 경우에는 AutoReleasingPointer<T> 타입이나 T 타입의 inout 표현을 넘겨줄 수 있다. 다른 경우와 달리 문자열이나 배열 표현을 바로 사용할 수는 없다.

함수 포인터

C 함수들은 콜백 호출을 위해서 함수 포인터를 인자로 받는 경우가 있다. 이 때에는 최상위 함수(자유함수)나 클로저 리터럴을 그대로 사용할 수 있으며, @convention(c) 속성을 붙여 정의한 클로저 객체 및 nil을 사용할 수 있다.

구조체

사실 이 부분부터는 포인터랑은 상관없고, Swift가 C 코드를 반입할 때 구조체등은 어떻게 가져오는지를 보여준다.

C 헤더에 정의된 구조체 역시 Swift 컴파일러는 Swift Struct 타입으로 변환하려고 시도한다. 예를 들어 다음의 구조체를 보자.

struct Color {
  float r, g, b;
};
typedef struct Color Color;

위 코드에선 Color라는 타입을 정의하고 있는데, 이는 실상 C 구조체이며 3개의 float 타입 멤버를 가지고 있다. 이 구조체는 실질적으로 Swift struct에 그대로 대응할 수 있어서 변환이 가능하다. 특히 Swift 구조체를 정의하면 memberwise 이니셜라이저가 자동으로 생성되는 부분까지 처리된다.

참고로 모든 멤버는 변수로 선언된다.

public struct Color {
  var r: Float
  var g: Float
  var b: Float

  init() {
    self.r = 0
    self.g = 0
    self.b = 0
  }

  init(r: Float, g: Float, b: Float) {
    self.r = r
    self.g = g
    self.b = b
  }
}

[C/C++] 한 편으로 요약하는 포인터

한편으로 요약하는 포인터

C의 포인터는 사실 단순히 ‘메모리상의 주소를 저장하는 변수’의 개념인데, 실제로는 상당히 어렵다고 느끼는 경우가 많다. 왜냐면 메모리 주소를 사용하는 방식은 단순히 ‘간접적’으로 변수 값을 참조하는 것이라 “과연 이걸 어디다, 왜 쓴단 말인가”라고 생각해버리기 쉽기 때문에 실제로 어떻게 써야 하는지에 대한 감을 잡기 힘들기 때문이다. 이 글에서는 몇 개의 예제를 살펴보고 이를 통해 포인터의 기초적인 내용을 공부해 보도록 하겠다.

[C/C++] 한 편으로 요약하는 포인터 더보기