태그 보관물: OpaquePointer

OpaquePointer

OpaquePointer

OpaquePointer는 Swfit2.x 에서 COpaquePointer가 이름이 바뀐 타입으로 불투명한1 C 포인터를 감싸는 래퍼 타입이다.1 타입을 반입할 수 있는 구조체의 포인터라면 원 구조체 Tc타입을 Swift 로 반입할 수 있고, 이 경우에는 UnsafePointer<Tc> 의 형태로 쓸 수 있다. 하지만 원 타입을 Swift가 이해할 수 없다면, 그 타입의 크기를 결정할 수 없으므로 이에 대한 포인터 역시 Swift 내부에서 결정하는 것은 어렵다.

물론 UnsafePointer<Void> 타입을 이용하는 것도 가능하다 생각할 수 있지만, 이 경우에는 C에서 void * 타입이며, 어찌됐든 pass 시점에 캐스팅해야 하는 한계가 있다. (그리고 원 타입을 알 수 없으면 캐스팅도 불가능하다.)

init: 은 다음과 같은 인자들을 받을 수 있다. (다만 실제로 생성할 일이 있을지는….)

  • UnsafeMutablePointer<T>, UnsafeMutablePointer<T>?
  • UnsafePointer<T>, UnsafePointer<T>?
  • (bitpattern: Int), (bitpattern: UInt)
  • UnsafeRawPointer, UnsafeRawPointer?
  • UnsafeMutableRawPointer, UnsafeMutableRawPointer?

UnsafeRawPonter는 타입이 지정되지 않은 데이터를 액세스하는 raw 포인터 타입이며, 사실상 C의 void * 타입 포인터를 날 것 그대로 엑세스하는 것과 유사하다.

UnsafePointer 패밀리와의 차이

UnsafePointer<T>, UnsafeMutablePointer<T>는 사실상 독립타입이 아닌 특정 Swift 타입에 대한 포인터 래퍼의 개념이므로, 이는 Swift 타입에 대한 포인터여야 한다. 기본적으로 C의 원시타입은 대부분 Swift 의 타입과 맵핑이 가능하지만, C의 구조체는 Swift의 구조체와 다르다. 따라서 UnsafePointer<T> 패밀리에 속하는 타입들은 C-API의 배열이나 포인터를 다루는 API와 연계할 때 사용하며, OpaquePointer는 알 수 없는 구조체 타입에 대한 포인터로 사용된다.

예시

sqlite3의 API를 Swift 내에서 이용하는 상황을 가정해보자.2

데이터베이스에 접근하기 위해서는 연결에 대한 핸들러가 필요한데, sqlite3의 API에서 sqltie_open() 함수는 데이터베이스 파일의 경로문자열을 받아서 해당 파일을 열고, 그 연결 핸들러를 리턴한다. 이 때 핸들러는 void *로 캐스팅된 sqlite3 객체 포인터이며, 따라서 이를 사용하기 위해서는 OpaquePointer를 써야 한다.

class DBConnector {
  lazy var db: OpaquePointer = { [unowned self] in
    let _db: OpaquePointer? = nil
    if sqlite3_open(self.db_path, &_db) == SQLITE_OK {
      return _db!
    }
    return nil
  }()
}

  1. 여기서 불투명한 C 포인터란, Swift로 반입될 수 없는 구조를 가진 C 구조체를 말한다. 
  2. 이 내용은 Swift에서 Sqlite3 사용하기 글에서도 다뤄진다. 

  1. 이름만 바뀐 것 외에도 nil 값을 갖기 위해서는 명시적으로 옵셔널로 선언해야 하는 차이도 있다. 이는 UnsafePointer 계열의 타입도 마찬가지다. 

(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을 넣을 수 있다. 참고