Swift 속의 C Pointer 이야기 - 기타 도구들

이 글은 포인터에 관련이 있거나 간접적인 접근 방법에 대한 글이다.

MemoryLayout

이미 생 포인터(Raw Pointer) 글에서 소개는 했지만 한번 더 소개한다. MemoryLayout 구조체는 특정 타입에 대한 다양한 메모리 관점에서의 정보를 제공한다. 사용방법은 대략 두 가지로 나뉘지만 간단하게 아래 처럼 사용한다.
MemoryLayout<Int>.size       // 8
MemoryLayout<Int>.alignment  // 8
MemoryLayout<Int>.stride     // 8
size 프로퍼티는 해당 데이터의 실제 크기(바이트 단위)이다. Swift 3 에는 비슷한 역활을 하던 sizeof() 가 사라졌는데 대신 이 정보를 사용할 수 있다.

alignment 는 Raw Pointer 에서 다뤘는데, 데이터가 표현되는 최소 단위이다. 즉 Int 타입의 포인터는 실제 메모리에 내용을 기록하거나 읽을때 8바이트 단위로 액세스 하게 된다.

마지막의 stride 는 메모리에 pack 되는 단위이다. 이 말은 위의 alignment 와 비슷하기도 한데, alignment 는 최소단위인 반면 이 stride 는 해당 타입의 최대단위로 볼 수 있다. 그래서 단일 변수 타입의 경우 alignment 와 stride 의 값은 같다. 하지만 복잡한 데이터의 경우 이 둘은 차이가 발생하게 된다.

좀 더 복잡한 예를 보자. 아래는 구조체의 레이아웃을 조사하는 경우이다.
struct A { }
MemoryLayout<A>.size       // 0
MemoryLayout<A>.alignment  // 1
MemoryLayout<A>.stride     // 1

struct B {
  let value: Int
  let name: String
}
MemoryLayout<B>.size       // 32
MemoryLayout<B>.alignment  // 8
MemoryLayout<B>.stride     // 32
Swift 의 구조체는 메모리상에 멤버데이터의 레퍼런스를 alignment 단위로 일렬로 배열하는 형태로 구현되는 듯 하다. 그래서 멤버에 따라 실제 크기가 달라진다.

어쨌거나 B의 경우 alignment 는 8 이고 stride 가 32임을 알 수 있다. 즉, 이 구조체의 데이터를 메모리에 집어 넣을 경우(pack) 32바이트의 크기가 필요하다는 말이다. (A의 경우는 딱히 설명이 필요없을 것 같다. 무조건 1바이트니 말이다)

참고로 클래스의 경우는 좀 다르다.
class CA { }
MemoryLayout<CA>.size       // 8
MemoryLayout<CA>.alignment  // 8
MemoryLayout<CA>.stride     // 8

class CB {
  let value: Int
  let name: String
  init(value: Int, name: String) {
    self.value = value
    self.name = name
  }
}
MemoryLayout<CB>.size       // 8
MemoryLayout<CB>.alignment  // 8
MemoryLayout<CB>.stride     // 8
클래스의 경우 무조건 8 바이트다. 이유는 단순하다. 이 8 바이트는 레퍼런스를 저장하기 위한 사이즈다. 클래스는 무조건 레퍼런스 기반으로 동작한다는 사실을 알고 있다면 크게 문제될 것이 없을 것이다.

MemoryLayout 의 경우는 이런 식으로 포인터 연산 시 필요한 수치 정보를 제공해 주는 역활을 한다.

String.withCString() 그리고 String(cString:)

문자열의 경우도 C에서 포인터로 액세스 하는 대표적인 데이터 타입이다. Swift 의 문자열(String)은 구조체로써 다양한 데이터를 담고 있다. 이 둘은 물리적으로 완전히 다른 데이터다. 그래서 서로간에 호환되지 않는다.
C String 이란 char 타입(Swift로 치자면 Int8 타입)의 단순 연속 나열에 불과하다. 문자열 마지막 바이트에는 반드시 '\0' (개념이 틀리긴 하지만 0 혹은 NULL 로 표기하기도 한다) 이 들어있어야 하는 점이 특징이다.
대신 Swift String 과 C String 사이의 상호 변환을 하기 위해 특수한 메소드가 제공된다. 바로 포인터를 바로 얻을 수 있는 메소드인 withCString 과 포인터에서 문자열을 생성할 수 있는 String(cString:) 생성자다.
let sourceString = "this is test"
let destRawPointer = UnsafeMutableRawPointer.allocate(bytes: 100, alignedTo: 1)
defer { destRawPointer.deallocate(bytes: 100, alignedTo: 1) }

let _ = sourceString.withCString { (pointer) in
  memcpy(destRawPointer, UnsafeRawPointer(pointer), sourceString.characters.count)
}

let destPointer = destRawPointer.bindMemory(to: Int8.self, capacity: 100)
let destString = String(cString: destPointer)

if sourceString == destString {
  print("Ok")
}
이 코드에서 withCString 으로 둘러쌓인 코드는 꼬리클로져로 호출되는데, pointer 라는 녀석이 UnsafePointer<Int8> 타입으로 들어온다. 이 예제에서는 이 포인터로 memcpy() 를 호출해 데이터를 복제해서 destString 까지 흘러가게(?) 만든다.

C String 을 Swift 에서 써야 할 상황이 얼마나 될지는 잘 모르겠다. 그런데 아직도 C로 구현되는 Hash Function 이 상당히 많다. 이런 류에서는 아마도 유용하게 쓸 수 있을 것 같다.

주의) withCString 클로져 내부에서 받은 포인터 혹은 포인터의 데이터들을 바로 리턴시키는 등의 일은 문제를 일으킬 수 있다. 값이 필요하다면 복사해서 전달하고 포인터가 자체가 필요하다면 직접 포인터 인스턴스를 생성해서 처리하자.

withUnsafeBytes

생짜포인터에 대한 이야기를 하면서 왜 타입이 없는 포인터를 다루는지의 이유에 대해 이야기 한 적이 있다. 바로 데이터를 바이트 단위로 액세스 하기 위함이다. 바이트 단위로 액세스를 하게 되면 바이트 단위로 구성할 수 있는 모든 타입 단위로 액세스가 가능하다.

그런데 바이트 단위 데이터를 가지는 생짜포인터를 얻기 위해 Swift 내에서는 굳이 포인터를 생성하지 않고도 얻는 방법이 있다.
var dataString = "this is test"
let _ = withUnsafeBytes(of: &dataString) { 
  (rawPointer) in
  // rawPointer is UnsafeRawBufferPointer

  for character in rawPointer {
    print(character)
  }
}
withUnsafeBytes 의 두 번째 인자로 클로져(여기서는 꼬리 클로져라서 파라미터 바깥에 붙어있지만)에 입력으로 들어오는 것은 UnsafeRawBufferPointer 라는 특수한 타입이다. 이 타입은 특별히 거론하지는 않았지만, 이름처럼 UnsafeBufferPointer 와 비슷하면서도 UInt8 타입 단위로 동작하는 UnsafeRawPointer 와 거의 동일하다. 아마도 UnsafeBufferPointer 에 대해 알고 있다면 별 다른 설명 없이도 쓰는데 무리는 없을 것이다.

어쨌든, withUnsafeBytes 를 이용해 포인터를 얻을 수도 있다는 설명이 된다.

다만 위의 경우 String.withCString과 분리하려다 보니 잘못된 예제가 된 것 같다. 일단 동작은 하긴 하고 에러도 없지만 제대로 문자열 버퍼를 가져오려면 wiftUnsafeBytes 가 아니라 String.withCString() 메소드를 이용해 가져오는게 맞다. 이 예제는 이런게 있다라는 참고 수준으로만 보자.

주의) withUnsafeBytes 역시 클로져 내부에서 전달받은 포인터 혹은 포인터의 데이터를 외부로 직접 가져오는건 지양하자. 필요하다면 값을 복사하거나 외부에서 직접 포인터 인스턴스를 생성하자.

[돌아가기] Swift 속의 C Pointer 이야기 - 시작
[관련글] Swift 속의 C Pointer 이야기 - UnsafePointer, UnsafeMutablePointer
[관련글] Swift 속의 C Pointer 이야기 - UnsafeRawPointer, UnsafeMutableRawPointer
[관련글] Swift 속의 C Pointer 이야기 - UnsafeBufferPointer, UnsafeMutableBufferPointer
[관련글] 스위프트(Swift) 가이드

댓글

양승현님의 메시지…
잘 배우고 갑니다. 감사합니다 :)

이 블로그의 인기 게시물

버전(Version)을 제대로 이해하기

소수점 제거 함수 삼총사 ceil(), floor(), round()