태그 보관물: C API

IUO 를 인자로 받는 함수 (Swift)

IUO타입을 인자로 선언한 함수들이 있는데, 이들은 주로 Objective-C/C API에서 발견된다. Objective-C에서 임의의 객체를 의미하는 id 타입은 항상 nil 일 수 있기 때문에 파라미터 타입을 AnyObject 가 아닌 기대하는 타입의 옵셔널로 받는 것이다. 하지만 이러한 API의 내부에서는 대부분 해당 값에 대해서 null 검사를 엄밀히 하지 않거나 하지 않아도 되는 경우가 많다.1

또한 C-API에서도 흔히 볼 수 있다. 이들은 임의의 포인터 혹은 불투명 타입[^1]을 인자로 받는데, C언어의 특성상 nil을 다른 값과 구분할 수 없는데, Swift 입장에서는 nil이 곧 NULL이기 때문에 옵셔널을 보낼 수 있게 되기 때문이다. 따라서 어떠한 API의 인자가 !으로 선언된 암묵적 언래핑 옵셔널을 사용한다고 할 때, 이를 사용하는 측면에서는 다음의 당연한 두 가지 접근법 중 하나를 취하게 된다.

  1. 옵셔널에 대해서 nil 체크를 하고 언래핑하여, 최종적으로는 옵셔널이 아닌 타입의 값만 넘긴다.
  2. 옵셔널 값을 그냥 넘겨준다.

2의 경우에는 자신이 무엇을 하고 있는지 정확히 파악되는 경우에만 시도해야 한다. Objective-C/C API 내부에서는 받은 인자의 값이 NULL이더라도 어떠한 동작을 하게끔 시도할지는 몰라도, 그것은 감춰져서 알 수 없으며 혹시라도 nil 값이 그대로 넘어간 경우에는 런타임 에러로 직결되기 때문이다.

많은 C/Objective-C API에서 이러한 타입을 볼 수는 있다. 하지만 그것은 언어의 기능이 불안정성을 깔끔하게 덜어내지 못하고 많은 부분의 불안요소를 컴파일 타임을 넘어 런타임까지 짊어지고 갈 수 밖에 없는 한계를 가지기 때문에 그러한 디자인을 갖게 된 것이다. Swift는 언어 수준에서 불안정성을 최소화하고 컴파일 타임에서 발생할 수 있는 최대한의 범위의 에러를 미리 찾을 수 있는 기능을 제공하고, 지원한다. 그것을 이용하고 지키는 습관은 단순히 타이핑을 몇 자 덜하는 것보다 훨씬 더 생산적이며, 나아가 C가 아닌 Swift의 디자인과 패러다임을 통한 사고의 틀을 만드는 것에 보다 빨리 익숙해지게 한다.

Swift에서 T! 타입을 인자로 받는 함수를 정의하기

이런 타입 시그니처는 함수 작성시에 당연히 쓸 수 있는 표현이기 때문에 그러한 타입의 인자를 받는 함수를 정의할 수는 있다. 하지만 앞에서도 말했듯이 그럴 필요가 있을 케이스가 있을 가능성이 0%이다. 받으려는 값이 항상 있어야하는 경우에는 T 타입을 쓰면 되고, 없거나 실패할 수 있는 가능성이 있다면 옵셔널을 쓰면 된다. 그 외의 경우는 없다.


  1. Objective-C에서는 nil에 어떤 메시지를 보내면 그냥 무시되기 때문에. 

(Swift3) swift3 관점에서의 sqlite3 API 분석 – C-API 사용법 심화

Swift에서 sqlite3 사용하기에 대한 글을 몇 편 작성했었는데, 이 글에서는 C API를 사용하는데 있어서 관련되는 Swift 타입들과, 실제 C/C++ API가 어떤식으로 변환되어 Swift 영역으로 들어오는지에 대해 좀 더 자세하고 깊이 들여다보도록 하겠다.

sqlite3_open

먼저 데이터베이스 파일을 열고 커넥션을 만드는 부분부터 시작하자. 모든 작업의 시작점이 될 sqlite3_open() 함수의 원형1은 다음과 같다. 파일경로를 받아서 연결을 생성한 후 연결 핸들러를 같이 전달받은 ppDB에 넣어준다. 이 때 ppDB의 타입은 sqlite3인데, 이는 C 구조체로되어 있다.2

int sqlite3_open(
    const char *filename,
     sqlite3 **ppDB
);

//...
typedef struct sqlite3 sqlite3;

ppDB의 타입 자체는 Swift가 알 수 없는 타입으로 들어온다. (아무래도 헤더파일이 아닌 구현부쪽에 정의된 타입으로 보인다) 따라서 *ppDB 라는 포인터는 UnsafePointer<T> 타입이 아닌OpaquePointer로 반입된다. 따라서 sqlite3 ** 타입은 UnsafeMutablePointer<OpaquePointer!>가 될 것이고 따라서 sqlite3_open()의 Swift 타입은 아래와 같이 변형될 것이다.

func sqlite3_open(_ filename: String, _ ppdb: UnsafeMutablePointer<OpaquePointer!>!) -> Int32

따라서 실제 사용시에는 다음과 같은 모양이 된다. 아래 코드는 사용자의 문서 폴더에 data.db 파일을 생성하고 이를 연결한다. 이때 Swift3에서 변형된 API에 맞게, FileManager, NSURL 관련된 코드도 좀 더 다듬어졌다.

let path = FileManager.default.urls(for:.documentDirectory, in:.userDomainMask)
            .last!.appendingPathComponent("data.db").path
var db: OpaquePointer? = nil // inout 으로 넘겨지기 때문에 var로 선언해야 한다.
if sqlite3_open(path, &db) == SQLITE_OK { /// 만약 함수 내에서 이렇게 쓴다면, guard 문을 쓰는 편이 낫겠지?
  print("db connected.")
} else {
  print("failed")
}

여담인데, 만약 sqilte3 구조체가 Swift로 들여올 수 있는 구조체라면, OpaquePointer 대신에 UnsafePointer 계열을 쓸 수 있다. 다음은 그렇다는 가정하에 어떻게 sqlite3_open()을 열 수 있는가 하는 예를 보여준다. 3

var db = UnsafeMutablePointer<sqlite3>.allocate(capacity: 1)
if sqlite3_open(path, &db) == SQLITE_OK {
  ...
}

dbUnsafeMutablePointer<sqlite3>으로 선언했고, 이는 C에서 sqlite* 포인터 타입에 해당된다. 이 때 넘겨지는 파라미터는 이중포인터 타입이므로 여기에 다시 &을 붙여서 이중포인터로 보낸다.

쿼리 컴파일 및 실행

문자열로 주어진 쿼리는 sqlite3 내부에서 컴파일되며, 컴파일 성공 시에 쿼리를 실행하게 된다. 쿼리를 컴파일한 결과는 sqlite3_stmt라는 구조체에 저장된다. 4

그리고 이 원형은 아래와 같다.

int sqlite3_prepare(
    sqlite3 *db, // 데이터베이스 연결 핸들러
    const char *zSql, // 쿼리
    int nBytes, // 쿼리의 최대 길이, 모르겠으면 그냥 -1
    sqlite3_stmt **ppStmt, // stmt 객체 포인터
    const char **pzTail // 쿼리의 남은 길이. 사용되지 않으므로 NULL 을 쓴다.
);

비슷하게 다음과 같이 번역된다.

func sqlite3_prepare(_ db: OpaquePointer!, _ zSql: String, 
                  _ nByte: Int32, _ ppStmt: UnsafeMutablePointer<OpaquePointer>,
                    _ pzTail: UnsafePointer<String>) -> Int32

따라서 쿼리를 컴파일 하는 과정은 다음과 같이 사용한다.

/// usage of sqlite3_prepare

var stmt: OpaquePointer? = nil
do {
  defer{sqlite3_finalize(stmt)}
  let query = "CREATE TABLE IF NOT EXISTS test ( num INT );"
  if sqlite3_prepare(db, query, -1, &stmt, nil) == SQLITE_OK {
    /// ... 쿼리 실행...
  } else {
    print("There is an error in your query.")
  }
}

단일 쿼리를 실행하는 것은 sqlite3_step으로 이루어진다. 이는 SQLITE_ROWSQLITE_DONE을 리턴하는데, _ROW의 경우에는 SELECT 문에 의해서 가져온 결과가 있다는 의미이며, _DONE은 모든 쿼리 실행이 완료되었다는 의미이다.

/// usage of sqlite3_prepare

var stmt: OpaquePointer? = nil
do {
  defer{sqlite3_finalize(stmt)}
  let query = "CREATE TABLE IF NOT EXISTS test ( num INT );"
  if sqlite3_prepare(db, query, -1, &stmt, nil) == SQLITE_OK {
    if sqlite3_step(stmt) == SQLITE_DONE {
      print("table created")
    } else {
      print("failed to create table.")
    }
  } else {
    print("There is an error in your query.")
  }
}

insert 액션

insert, update, delete 등은 한 회당 한 번씩 실행한다. 보통 이 때는 쿼리 문자열 내에 값을 넣는 것보다 보안상으로는 쿼리를 컴파일한 후에 값을 바인딩하는 것이 안전하다고 한다. 이 때 사용하는 함수들이 sqlite3_bind_* 함수들이다. 이는 각각의 칼럼5에 넣을 값을 바인딩해준다. 쿼리 바인딩을 위해서는 컴파일 시에 sqlite3_prepare 말고 sqlite3_prepare_v2를 쓴다. 쿼리에서 바인딩될 값은 ? 문자로 표시한다.

대표적으로 sqlite3_bind_int, sqlite3_bind_text를 보도록 하자.

int sqlite3_bind_int(
  sqlite3_stmt*, // 컴파일된 구문객체
  int column, // 쿼리에서 나타나는 ?의 순서. 1부터 시작한다. 
  int vaule // 바인딩할 값
);

int sqlite3_bind_text(
  sqlite3_stmt*, // 컴파일된 구문 객체
  int, // 순서
  const char*, // 문자열 시작점
  int, // 문자열의 길이
  void(*)(void*) // 문자열이나 BLOB 객체를 처리하는 함수포인터
);

칼럼 순서가 1부터 시작한다는 부분에 주의해야 한다. 참고로 C의 경우 동적으로 할당한 메모리에 문자열을 복사하고 바인딩한 후에 문자열을 폐기할 수 있는 함수 포인터를 같이 넣어줄 수 있다. (이 때 폐기 함수는 바인딩에 실패해도 동작한다.) 바인딩 완료후 직접해도 되는 부분이라 굳이 쓸 필요는 없으며, 어차피 Swift는 문자열 데이터를 알아서 관리해주니까 그냥 nil 을 주면 된다.

이들의 Swift 컨버전은 다음과 같을 것이다. 6

func sqlite3_bind_int(_ stmt: OpaquePointer!, _ column : Int32, _ value: Int32) -> Int
func sqlite3_bind_text(_ stmt: OpaquePointer!, _ column: Int32,
                    _ value: String, _ length: Int32,
                    _ disposer: (UnsafePointer<Void>) -> Void)
)

이번에는 그러면 값을 삽입하는 과정을 살펴보자.

let query = "INSERT INTO test (num) VALUES (?);"
let n: Int32 = 52
var stmt: OpaquePointer? = nil
if sqlite3_prepare_v2(db, query, -1, &stmt, nil) == SQLITE_OK {
  if sqlite3_bind_int(stmt, 1, n) == SQLITE_OK {
    if sqlite3_step(stmt) == SQLITE_DONE {
      print("completed.")
    }
  } else {
    print("can't bind value")
  }
} else {
  print("can't compile query")
}
sqlite3_finalize(stmt)

값을 불러오는 과정은 다음과 같다. SELECT 쿼리와 같이 리턴 값이 있는 경우 sqlite3_step() 함수는 결과를 SQLITE_ROW로 표시한다. 그러면 결과로 불려온 Row가 있다는 뜻이며 해당 row의 각 칼럼 값을 얻어내기 위해서는 바인딩때와 유사하게 sqlite3_column_* 함수들을 쓴다. 다만 바인딩때와는 달리 구문객체와 칼럼 순서(역시 1부터 시작함)만 있으면 값을 얻어낼 수 있다.

let query = "SELECT num FROM test LIMIT ?"
let maxCount: Int32 = 10
var result = [Int32]()
if sqlite3_prepare_v2(db, query, -1, &stmt, nil) == SQLITE_OK {
  if sqlite3_bind_int(stmt, 1, maxCount) == SQLITE_OK {
    while sqlite3_step(stmt) == SQLITE_ROW {
      let q:Int32 = sqlite3_column_int(stmt, 1)
      result.append(q)
    }
  }
}
for i in result {
  print(i)
}


텍스트인 경우에는 const unsigned char* sqlite3_column_text() 함수를 쓰는데 보다 시피 리턴타입이 문자열 포인터 타입이고, 이는 Swift 에서 UnsafePointer<CChar> 가 된다. C문자열(const char*)로부터 문자열을 만들기 위해서는 String의 Foundation 확장이 필요하며 init(cString:), init?(cString:encoding:)을 쓸 수 있다.

var result = [String?]()
...
while sqlite3_step(stmt) == SQLITE_ROW {
  let strPtr = sqlite3_column_text(stamt, 1)
  let str = String(cString:strPtr, encoding:.utf8)
  result.append(str)
}
...
for s in result.flatMap{$0} {
  print(s)
}

이상으로 sqlite3의 C/C++ API를 Swift 관점에서 바라볼 때 어떤 식으로 반입되며 어떻게 상호작용하는 Swift 코드를 작성할 것인지에 대해 살펴보았다.


  1. https://www.sqlite.org/capi3ref.html#sqlite3_open 
  2. https://www.sqlite.org/capi3ref.html#sqlite3 
  3. 이중 포인터를 UnsafeMutablePointer<UnsafeMutablePointer<T>> 타입으로 만들어야 한단 말인가? 
  4. typedef struct sqlite3_stmt sqlite3_stmt https://www.sqlite.org/capi3ref.html#sqlite3_stmt 
  5. 1부터 시작한다. 
  6. 함수포인터의 경우, 일반적인 Swift 자유 함수나 클로저 혹은 nil을 넣을 수 있다. 참고