C, Swift, 스터디

Swift 타입으로 변형되는 C 타입들

Swift와 C의 데이터타입 호환

Swift는 기본적인 C 포인터 타입에 대해서는 내부적으로 자동으로 변환을 하게 된다. 예를 들어 const char * 타입의 인자를 받는 함수에는 Swift 문자열을 그대로 넣으면 된다. (이는 해당 C 라이브러리를 import 하는 과정에서 Swift가 C 함수의 인자 타입을 자동으로 변환해준다.) 그 외의 임의 포인터를 사용하는 함수라든지, C 포인터 데이터 타입을 Swift 코드에서 사용해야 하는 경우에는 각 포인터 타입을 Swift 타입으로 사용할 수 있다. 이미 이러한 예는 이전에 다룬 Swift에서 Sqlite 사용하기에서 다루고 있다.

C타입으로 변환되는 내용은 sqlite3등 기존에 사용되던 C라이브러리들을 Swift에서 사용할 수 있게 해준다.

자동 변환의 한계

C API를 사용할 때, 기본적인 C 타입에 대해서는 Swift가 자동으로 변환을 한다고 했다. 즉 C 함수는 Swift 함수로 변환되며, 함수 호출 시에 Swift 컴파일러가 적절하게 변환하여 C 함수를 호출하는 것이다. 예를 들면 int 타입의 인자는 함수에서 Int32 타입을 넣으면 자동으로 변환되어 전달된다.

하지만 이런 변환은 함수 호출 시 파라미터에 대해서만 일어난다. 만약 raw한 C 타입들은 다음과 같은 변환 테이블을 거치게 된다.

  • bool – CBool
  • char, unsigned char – CChar
  • unsigned char – CUnsignedChar
  • short – CShort
  • unsigned short – CUnsigendShort
  • int – CInt
  • unsigned int – CUnsignedInt
  • long – CLong
  • unsigned log – CUnsignedLong
  • long long – CLongLong
  • unsigned long long – CUnsignedLongLong
  • wchar_t = CWideChar
  • char16_t = CChar16
  • char32_t = CChar32
  • float = CFloat
  • double = CDouble

예를 들어 bool isFilled(int a)라는 C 함수는 Swift로 임포트될 때 CBool isFilled(a: Int)로 변경된다. 이 때 중요한 것은 반환값 자체는 다시 Swift의 기본타입이 아닌 Swift의 C 대응타입으로 돌아온다는 것이다. (이는 C의 타입의 값으로 만들어진 Swift 타입이다)

enum

C의 enum은 타입이라기보다는 태그로서, 실제로는 int 형 상수에 이름을 붙인 것이다. Objective-C의 많은 이름이 붙은 상수들은 사실 열거형이다.

typedef NS_ENUM(NSInteger, UITableViewCellStyle) {
    UITableViewCellStyleDefault,
    UITableViewCellStyleValue1,
    UITableViewCellStyleValue2,
    UITableViewCellStyleSubTitle
};

이렇게 정의된 enum 태그는 반입시에 swift의 enum 타입으로 변환된다.

enum UITableViewCellStyle: Int {
    case Default, Value1, Value2, SubTitle
}

“변환”이 되었기 때문에 Swift 내에서는 이 원본이 C에서 정의되었다 하더라도 엄연히 Swift 타입이므로

let cellStyle: UITableViewCellStyle = .Default

라 쓴다.

포인터

Swift에서 포인터를 직접 액세스하는 것은 권장할만한 것이 못되지만, 방법은 제공하고 있다. 일반적으로 C타입의 포인터들은 UnsafePointer<T> 타입으로 Swift에서 명명된다. (T는 Swift의 타입이다.)

  • const Type * = UnsafePointer<Type>
  • Type * = UnsafeMutablePointer<Type>

클래스 타입에 대해서는

  • Type * const * = UnsfaePointer<Type>
  • Type * __strong * = UnsafeMutablePointer<Type>
  • Type ** = AutoreleasingUnsafeMutablePointer<Type>

으로 대응된다.

상수 포인터들

nil은 NULL 포인터에 대응하며, UnsafePonter<T>, UnsafeMutablePointer<T>, AutoreleasingUnsafeMutablePointer<T> 들은 필요시에 자동으로 UnsafePointer<T> 타입으로 변경된다. 배열의 경우([T])에는 C와 비슷하게 변수의 이름이 배열 포인터로 넘어간다. 하지만 이는 함수 호출 시마다 새롭게 C배열이 만들어지는 것으로 함수 세션이 끝나면 항상 같은 왼터가 되리라는 보장이 없다. (따라서 static 변수 같은 곳에 포인터를 저장해서는 안된다.)

만약 다음과 같은 C 함수가 있다면,

void takesAPointer(float * x);

이는 Swift에서는 다음과 같이 표현된다.

func takesAPointer(x: UnsafePointer<Float>)

float 타입은 사실 함수 호출 시에 자동으로 변환되므로 아래와 같으 유연한 방법으로 호출이 가능하다.

var x: Float = 0.0
var p: UnsafePointer<Float> = nil
takesAPointer(nil)
takesAPointer(p)
takesAPointer(&x)
takesAPointer([1.0, 2.0, 3.0])

void * 타입의 포인터를 다시 예로 들면

func takeAVoidPointer(x: UnsafePointer<Void>)

라는 함수가 될 거고

var x: Float = 0.0, y: Int = 0
var p: UnsafePointer<Float> = nil, q:UnsafePointer<Int> = nil
takesAVoidPointer(nil)
takesAVoidPointer(p)
takesAVoidPointer(q)
takesAVoidPointer(&x)
takesAVoidPointer(&y)
takesAVoidPointer([1.0, 2.0, 3.0] as [Float])
let intArray = [1,2,3]
takesAVoidPointer(intArray)

등으로 넘긴다.

변수 포인터

변수포인터는 포인터 변수 자체의 포인터이다. nil은 동일하게 NULL을 가리키며, UnsafeMutablePointer<T> 타입이 된다. 배열을 넘기는 경우, 배열이 변경되면 별다른 inout 어트리뷰트가 없어도 배열 이름 앞에 &을 붙이면 된다.

void takesAMutablePointer(float* x)

이 함수는 Swift에서는

func takesAMutablePointer(x: UnsafeMutablePointer<T>)

이 함수를 호출하려면

var x: Float = 0.0
var p: UnsafeMutablePointer<Float> = nil
var a: [Float] = [1.0, 2.0, 3.0]
takesAMutablePointer(nil)
takesAMutablePointer(p)
takesAMutablePointer(&x)
takesAMutablePointer(&a) //

구조체 포인터

구조체의 포인터는 COpaquePointer 타입이다. 대부분의 C 라이브러리에서 객체는 이 타입으로 표현된다.

함수 포인터

함수 포인터는 Swift에서 CFunctionPointer<Type>으로 반입된다. 예를 들어 int (*)(void)타입의 함수포인터는 CFunctionPointer<()->Int32> 타입으로 들여오게 된다.