기본적으로 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++ Type | Mapped | Swift Type |
_Bool / bool | CBool | Bool |
char | CChar | Int8 |
char16_t | CChar16 | Int8 |
char32_t | CChar32 | Uincode.Scalar |
double | CDouble | Double |
float | CFloat | Float |
int | CInt | Int32 |
long | CLong | Int16 |
long long | CLongLong | Int64 |
short | CShort | Int16 |
signed char | CSignedChar | UInt8 |
unsigned char | CUnsignedChar | UInt8 |
unsigned int | CUnsignedInt | UInt32 |
unsigned long | CUngisnedLong | UInt |
unsigned long long | CUnsignedLongLong | UInt64 |
unsigned short | CUnsignedShort | UInt8 |
wchar_t | CWideChar | Unicode.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
}
}