암묵적 언랩 옵셔널 타입이 함수의 인자로 오는 경우 (Swift)

간혹 T! 타입이 함수의 인자로 들어가는 경우를 종종 볼 때가 있다. 주로 C나 Objective-C 라이브러리를 사용할 때, 이런 경우를 볼 수 있었다. 특정 연산의 결과를 T! 타입으로 미리 선언하는 것은 그럴 수 있다 하겠는데, 함수의 인자라면 호출하는 곳의 관점으로는 옵셔널이거나 아니면 옵셔널이 아니거나 둘 중 하나일 것인데 왜 이런 시그니쳐가 보이는 것일까?

결론부터 이야기하면 Objective-C 의 언어적 한계 때문이다. 임의의 클래스의 객체를 받는 인터페이스가 있다고 하자. Objective-C에서 객체를 가리킬 수 있는 모든 변수 타입에는 nil이 들어갈 수 있고, nil에 어떤 메시지를 보내는 것은 아무런 일도 일어나지 않지만 Objective-C 에서는 문법적으로 합당한 행위이다. 따라서 모든 Objective-C 객체 타입은 Swift의 입장에서 보면 옵셔널인데, 정작 Objective-C 문법은 별도로 옵셔널을 지원하지 않는데. 이러한 차이에서 Swift 컴파일러는 Objective-C API를 반입할 때, T? 타입일 수 있을 값을 T! 로 바꿔서 표시해준다.

물론 사용하는 Swift 코드의 입장에서는 이러한 API를 호출할 때에는 T? 타입의 옵셔널이든 T 타입의 객체값이든 어떤 것을 넘겨도 동작에는 상관없다.

참고로 Objective-C에서 nullable, nonnull 어노테이션이 추가되면서 많은 API에서 이러한 표현은 제거되었고, 실제 옵셔널이 필요한 부분에서는 T? 타입을, 그렇지 않은 부분에서는 T 타입을 받도록 업데이트되어 있다.

SQLite3 C API를 Swift에서 사용하는 방법

Swift에서 SQLite3를 사용하는 방법은 크게 두 가지이다. 하나는 Objective-C에서 SQLite3를 액세스하는 래퍼 클래스를 작성하고, 이 것을 Swift 프로젝트에 포함시켜서 컴파일하는 것이다. Swift는 Objective-C와 자연스럽게 상호호환되기 때문에 Objective-C에 친숙하다면 이 방법도 나쁘지 않다.

관련글 : Objective-C 래퍼를 통해 Swift에서 SQLite3를 사용하는 법

다른 한가지 방법으로는 sqlite3.h 헤더를 Swift에서 반입하여 Swift에서 SQLite3 C API를 바로 사용하는 것이다. 결국 업어치나 메치나 똑같은 것이기는 한데, Swift에서 C API를 직접 사용하는 것은 Swift와 C의 연계 방식에 대한 이해가 필요하다. 이 글에서는 Swift에서 Sqlite3를 사용하기 위해 필요한 배경 지식들에 대해서 살펴볼 예정이다. SQLite3 API에 대한 자세한 설명이 필요할 수 있는데, 이 내용은 Objective-C에서 SQLite3를 사용하는 법을 다룬 위 링크에서 대략 소개하고 있으며, 이 글에서는 최소한 SQLite3 API에 대해 알고 있다고 전제하겠다.

SQLite3 C API를 사용하여 쿼리를 실행하는 결과에 대해서는 다음의 과정을 거친다.

  1. 데이터베이스 연결
  2. 쿼리 컴파일 및 쿼리 바인딩
  3. 쿼리 실행 및 각 Row의 데이터 획득
  4. 연결닫기
SQLite3 C API를 Swift에서 사용하는 방법 더보기

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
  }
}