Swift 속의 C Pointer 이야기 - 기타 도구들
이 글은 포인터에 관련이 있거나 간접적인 접근 방법에 대한 글이다.
alignment 는 Raw Pointer 에서 다뤘는데, 데이터가 표현되는 최소 단위이다. 즉 Int 타입의 포인터는 실제 메모리에 내용을 기록하거나 읽을때 8바이트 단위로 액세스 하게 된다.
마지막의 stride 는 메모리에 pack 되는 단위이다. 이 말은 위의 alignment 와 비슷하기도 한데, alignment 는 최소단위인 반면 이 stride 는 해당 타입의 최대단위로 볼 수 있다. 그래서 단일 변수 타입의 경우 alignment 와 stride 의 값은 같다. 하지만 복잡한 데이터의 경우 이 둘은 차이가 발생하게 된다.
좀 더 복잡한 예를 보자. 아래는 구조체의 레이아웃을 조사하는 경우이다.
어쨌거나 B의 경우 alignment 는 8 이고 stride 가 32임을 알 수 있다. 즉, 이 구조체의 데이터를 메모리에 집어 넣을 경우(pack) 32바이트의 크기가 필요하다는 말이다. (A의 경우는 딱히 설명이 필요없을 것 같다. 무조건 1바이트니 말이다)
참고로 클래스의 경우는 좀 다르다.
MemoryLayout 의 경우는 이런 식으로 포인터 연산 시 필요한 수치 정보를 제공해 주는 역활을 한다.
C String 을 Swift 에서 써야 할 상황이 얼마나 될지는 잘 모르겠다. 그런데 아직도 C로 구현되는 Hash Function 이 상당히 많다. 이런 류에서는 아마도 유용하게 쓸 수 있을 것 같다.
주의) withCString 클로져 내부에서 받은 포인터 혹은 포인터의 데이터들을 바로 리턴시키는 등의 일은 문제를 일으킬 수 있다. 값이 필요하다면 복사해서 전달하고 포인터가 자체가 필요하다면 직접 포인터 인스턴스를 생성해서 처리하자.
그런데 바이트 단위 데이터를 가지는 생짜포인터를 얻기 위해 Swift 내에서는 굳이 포인터를 생성하지 않고도 얻는 방법이 있다.
어쨌든, 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) 가이드
MemoryLayout
이미 생 포인터(Raw Pointer) 글에서 소개는 했지만 한번 더 소개한다. MemoryLayout 구조체는 특정 타입에 대한 다양한 메모리 관점에서의 정보를 제공한다. 사용방법은 대략 두 가지로 나뉘지만 간단하게 아래 처럼 사용한다.MemoryLayout<Int>.size // 8 MemoryLayout<Int>.alignment // 8 MemoryLayout<Int>.stride // 8size 프로퍼티는 해당 데이터의 실제 크기(바이트 단위)이다. 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 // 32Swift 의 구조체는 메모리상에 멤버데이터의 레퍼런스를 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) 가이드
댓글