제목: Unsafe Swift: Using Pointers and Interacting with C


스위프트는 디폴트로 메모리 세이프하다. 메모리 세이프 하다는 의미는 메모리에 직접 접근하는 것을 막아주고, 당신이 사용하기 전에 모든것이 초기화 되어있음을 보장한다는 뜻이다. 핵심은 "디폴트"이다. 언세이프 스위프트는 여러분이 필요할때 포인터를 이용해서 메모리에 직접 다룰 수 있게 해준다.

이 튜토리얼은 스위프트의 이른바 "언세이프"라 불리는 소용돌이의 여행에 데려다 줄것이다. "언세이프"라는 용어는 종종 혼란을 만든다. 이것이 당신이 도작하지도 않을, 위험하고 나쁜 코드를 작성하고 있다는 의미가 아니라, 오히려 컴파일러가 도와줄 수 있는 부분의 한계를 뛰어넘고 추가적인 주의를 필요로하는 코드를 작성한다는 의미이다.

C같은 언세이프 언어와 함께 작업할때, 추가적인 런타임 성능이 필요할때, 혹은 그냥 그 내부를 살펴보고 싶을때 이러한 기능이 필요함을 발견할 수 있을 것이다. 이 주제가 한걸음 더 나아간 주제이긴 하지만 여러분이 합리적인 스위프트 언어 지식을 가지고 있다면, 따라올 수 있을 것이다. 또한 C언어 경험이 도움이 될것이지만 필수조건은 아니다.

시작하기
이 튜토리얼은 3가지 플레이그라운드로 구성된다. 첫번째 플레이그라운드에서는 메모리 레이아웃(Memory Layout)을 살펴보고 언세이프 포인터를 사용해보는 코드를 몇개 만들어 볼 것이다. 두번째 플레이그라운드에서는 스위프트 인터페이스로 데이터 압축을 스트리밍하는 저수준 C API를 가볍게 다룰것이다. 마지막 플레이그라운드에서는 arc4random에대한 독립적인 대안의 플랫폼을 만들 것인데, 언세이프 스위프트를 사용했지만 사용자들이 그 세부내용은 모르도록 만들것이다.

UnsafeSwift라는 새 플레이그라운드를 생성하면서 시작해보자. 이 튜토리얼의 모든 코드들은 플랫폼에 의존하지 않기 때문에 아무 플랫폼이나 선택해도 된다. Foundation 프레임워크를 불러왔는지 확인하자.

메모리 레이아웃

샘플 메모리샘플 메모리


언세이프 스위프트는 메모리 시스템에 직접적으로 동작한다. 메모리는 일련의 상자라고 생각할 수 있고(실제로 10억개의 상자이다), 각 상자 안에는 숫자가 들어있다. 각 상자는 그것과 연관된 유일한 메모리 주소를 가진다. 저장소의 가장 작은 주소로 가능한 단뒤는 한 바이트이며, 한 바이트는 8비트로 이루어져있다. 8비트는 0에서 255의 값을 저장할 수 있다. 프로세서는 메모리 워드(word)에는 효율적으로 접근할 수 있는데, 워드는 보통 1바이트 이상이다. 64비트 시스템에서는 한 워드가 8바이트 혹은 64비트의 길이이다.

스위프트는 여러분의 프로그램에서 어떤것의 크기나 계열(alignment)에대해 이야기해주는 메모리 레이아웃(Memory Layout) 기능을 가지고 있다.

여러분의 플레이그라운드에 아래의 코드를 추가하자.
MemoryLayout<Int>.size          // returns 8 (on 64-bit)
MemoryLayout<Int>.alignment     // returns 8 (on 64-bit)
MemoryLayout<Int>.stride        // returns 8 (on 64-bit)

MemoryLayout<Int16>.size        // returns 2
MemoryLayout<Int16>.alignment   // returns 2
MemoryLayout<Int16>.stride      // returns 2

MemoryLayout<Bool>.size         // returns 1
MemoryLayout<Bool>.alignment    // returns 1
MemoryLayout<Bool>.stride       // returns 1

MemoryLayout<Float>.size        // returns 4
MemoryLayout<Float>.alignment   // returns 4
MemoryLayout<Float>.stride      // returns 4

MemoryLayout<Double>.size       // returns 8
MemoryLayout<Double>.alignment  // returns 8
MemoryLayout<Double>.stride     // returns 8
MemoryLayout<Type>은 컴파일시간에 특정 Type의 size, alignment, stride를 셜정하는 제네릭 타입이다. 반환된 값은 바이트 단위이다. 예를들어 Int6size에서는 2바이트이고 alignment도 같다. 이것은 2로 나누어 떨어지는 주소에서 시작해야한다는 의미이다.

예를들어 Int16100이라는 주소에 할당할 수 있지만 1이 주소에 할당하는 것은 필요 alignment를 위반하는 것이기 때문에 불가능하다. 만약 Int16들을 함께 모은다면 stride 간격에 모일 것이다. 이 기본 타입을 위해 sizestride와 같다.

다음으로 사용자가 정의한 구조체의 레이아웃을 살펴보고 아래를 플레이그라운드에 추가하자.

struct EmptyStruct {}

MemoryLayout<EmptyStruct>.size      // returns 0
MemoryLayout<EmptyStruct>.alignment // returns 1
MemoryLayout<EmptyStruct>.stride    // returns 1

struct SampleStruct {
  let number: UInt32
  let flag: Bool
}

MemoryLayout<SampleStruct>.size       // returns 5
MemoryLayout<SampleStruct>.alignment  // returns 4
MemoryLayout<SampleStruct>.stride     // returns 8
빈 구조체의 size0이다. 이것은 alignment1이기때문에 어떤 주소에도 가능하다.(모든 자연수는 1로 나누어진다) stride는 특이하게도 1이다. 그 이유는 여러분이 만든 각 EmptyStructsize0이더라도 유용한 메모리 주소를 가져야하기 때문이다.

SampleStruct의 경우는 size5이지만 stride8이다. 이것은 alignment 필요조건의 4바이트 바운더리에의해 그렇게된다. 이것을 고려해볼때 스위프트가 할 수 있는 최고의 일은 8바이트 간격으로 묶는 것이다.

다음을 플레이그라운드에 추가하자.

class EmptyClass {}

MemoryLayout<EmptyClass>.size      // returns 8 (on 64-bit)
MemoryLayout<EmptyClass>.stride    // returns 8 (on 64-bit)
MemoryLayout<EmptyClass>.alignment // returns 8 (on 64-bit)

class SampleClass {
  let number: Int64 = 0
  let flag: Bool = false
}

MemoryLayout<SampleClass>.size      // returns 8 (on 64-bit)
MemoryLayout<SampleClass>.stride    // returns 8 (on 64-bit)
MemoryLayout<SampleClass>.alignment // returns 8 (on 64-bit)
클래스들은 참조 타입이므로 Memory Layout은 참조 크기가 8바이트라고 알려준다.

메모리 레이아웃에대해 더 알고싶다면 Mike Ash의 멋진 글을 보자.

포인터
한 포인터는 한 메모리 주소를 가지고있다. 메모리에 직접 접근하는 타입은 "Unsafe"라는 접두를 가지는데, 따라서 이 포인터 타입은 UnsafePointer라 부른다. 이런식으로 추가적인 타이핑이 성가셔 보일수도 있다. 그러나 이것은 정의되지 않은 행동을 만들 수도 있게 해놓았을때, 컴파일러 없이 메모리 접근의 체크를 하고 있다고 (단지 크레쉬를 예방할 뿐만 아니라) 당신이나 코드를 읽는 사람이 알게 해준다.

스위프트 설계자는 C의 char*와 동일한 UnsafePointer 타입을 만들 수 있게 해놓았다. char*은 구조화되지 않은 방법으로 메모리에 접근할 수 있다. 스위프트는 그렇게 하지 않고, 대게 몇몇개의 포인터 타입을 가지는데, 각각은 다른 기능과 목적을 가진다. 포인터 타입을 가장 알맞게 사용하면, 더욱 의도에 맞게 소통하고, 더 낮은 에러, 그리고 정의되지 않은 동작을 피할 수 있게 도와준다.

언세이프 스위프트 포인터는 그 기능이 예상가능한 네이밍 형식으로, 그 포인터의 특징이 무엇인지 알 수 있게 도와준다. 가변적(mutable)인지 불가변적인지, raw한지 typed인지, 버퍼 스타일인지 말이다. 아래와같이 전체적으로 8가지 조합이 있다.



다음 섹션들에서 우리는 이 포인터 타입들에대해 배워볼 것이다.

Raw 포인터 사용하기
아래 코드를 여러분의 플레이그라운드에 추가하자.
// 1
let count = 2
let stride = MemoryLayout<Int>.stride
let alignment = MemoryLayout<Int>.alignment
let byteCount = stride * count

// 2
do {
  print("Raw pointers")
  // 3
  let pointer = UnsafeMutableRawPointer.allocate(bytes: byteCount, alignedTo: alignment)
  // 4
  defer {
    pointer.deallocate(bytes: byteCount, alignedTo: alignment)
  }
  // 5
  pointer.storeBytes(of: 42, as: Int.self)
  pointer.advanced(by: stride).storeBytes(of: 6, as: Int.self)
  pointer.load(as: Int.self)
  pointer.advanced(by: stride).load(as: Int.self)
  // 6
  let bufferPointer = UnsafeRawBufferPointer(start: pointer, count: byteCount)
  for (index, byte) in bufferPointer.enumerated() {
    print("byte \(index): \(byte)")
  }
}
이 예제는 두 정수를 저장하고 불러오기 위해 언세이프 스위프트 포인터를 사용한다. 여기서 무슨 일이 일어나는지에대한 설명이다.
  1. 이러한 상수들은 사용된 값들을 종종 들고 있다.
    • count는 저장하기 위해 정수를 가지고 있다.
    • strideInt 타입의 stride를 가지고 있다.
    • alignmentInt 타입의 alignment를 가지고 있다.
    • byteCount는 필요한 모든 바이트 수를 가지고 있다.
  2. 스코프 레벨을 추가하기위해 do 블럭을 추가하였기 때문에, 나중에 예제에서 변수 이름을 재사용할 수 있다.
  3. UnsafeMutableRawPointer.allocate 메소드는 필요한 바이트를 할당하는데 사용한다. 이 메소드는 UnsafeMutableRawPointer를 반환한다. 저 타입의 이름에서 알 수 있듯, 포인터는 (가변의) Raw 바이트를 불러오고 저장하는데 사용될 수 있다.
  4. defer 블럭은 포인터가 적절하게 해제되었는지 확인하기위해 추가된 부분이다. 여기서 ARC는 도움이 되지 않는다. 여러분은 스스로 메모리 관리를 해야한다! 여기에서 defer에대해 더 읽어보길 바란다.
  5. storeBytesload 메소드는 바이트를 저장하고 불러오는데에 쓰인다. 두번째 정수의 메모리 주소는 그 포인터의 stride 바이트를 증가시켜서 계산된디.포인터는 stride 하기때문에, (pointer+stride).storeBytes(of: 6, as: Int:Self)로 포인터 계산 또한 할 수 있다.

  1. UnsafeRawBufferPointer는 메모리가 마치 바이트의 모음인것처럼 접근할 수 있게 해준다. 이 의미는 바이트들을 돌면서 차례로 접근할 수 있게 해주고, 서브스크립트를 이용해 접근하며 filter, map, reduce와같은 멋진 메소드까지 사용할 수 있게 한다. 이 버퍼 포인터는 raw 포인터를 초기화한다.

Typed 포인터 사용하기
typed 포인터를 사용하면 이전의 예제를 간단하게 만들 수 있다. 아래 코드를 플레이그라운드에 추가하자.
do {
  print("Typed pointers")
  let pointer = UnsafeMutablePointer<Int>.allocate(capacity: count)
  pointer.initialize(to: 0, count: count)
  defer {
    pointer.deinitialize(count: count)
    pointer.deallocate(capacity: count)
  }
  pointer.pointee = 42
  pointer.advanced(by: 1).pointee = 6
  pointer.pointee
  pointer.advanced(by: 1).pointee
  let bufferPointer = UnsafeBufferPointer(start: pointer, count: count)
  for (index, value) in bufferPointer.enumerated() {
    print("value \(index): \(value)")
  }
}
아래의 다른 점들을 인지하자.
  • UnsafeMutablePointer.allocate 메소드를 사용하여 메모리를 할당한다. 이 제네릭 파라미터는 포인터로 Int 타입의 값을 불러오고 저장하는데 사용될 것이라는 것을 스위프트에게 알려준다.
  • typed 메모리는 사용하기전과 소멸하기전에 반드시 초기화되어야한다. 이것은 initialize 메소드와 deinitialize 메소드로 할 수 있다. Update: atrick라는 유저가 달아놓은 커멘트처럼 소멸은 non-trivial 타입들만 필요로 한다. 이 말은, 여러분이 non-trivial의 무언가를 바꿀 경우에 소멸을 가지고 있는 것이 여러분 코드의 훗날을 위해 좋은 방법이다. 또한 컴파일러가 이것을 최적화할것이기 때문에 항상 비용이 발생하지 않는다.
  • typed 포인터는 pointee라는 프로퍼티를 가지고 있는데, 이것은 값을 불러오고 저장할때 타입 세이프한 방법을 제공하는 프로퍼티이다.
  • typed 포인터를 증가시키면, 여러분의 숫자의 값을 원하는대로 증가해가며 나타낼 수 있다. 그 포인터는 그 타입의 포인터가 가리키는 값을 기반으로 옳바른 stride를 계산할 수 있다. 다시말해 포인터 계산 또한 가능하다. (pointer+1).pointee=6 이런것 또한 가능하다.
  • typed 버퍼 포인터와 같은 점은, 바이트 대신에 값을 차례로 반복해갈 수 있다.

Raw 포인터를 Typed 포인터로 변환하기
typed 포인터는 항상 직접 초기화 할 필요가 없다. 또한 raw 포인터로부터 만들어질 수 있다.

여러분의 플레이그라운드에 아래 코드를 추가하자.
do {
  print("Converting raw pointers to typed pointers")
  let rawPointer = UnsafeMutableRawPointer.allocate(bytes: byteCount, alignedTo: alignment)
  defer {
    rawPointer.deallocate(bytes: byteCount, alignedTo: alignment)
  }
  let typedPointer = rawPointer.bindMemory(to: Int.self, capacity: count)
  typedPointer.initialize(to: 0, count: count)
  defer {
    typedPointer.deinitialize(count: count)
  }

  typedPointer.pointee = 42
  typedPointer.advanced(by: 1).pointee = 6
  typedPointer.pointee
  typedPointer.advanced(by: 1).pointee
  let bufferPointer = UnsafeBufferPointer(start: typedPointer, count: count)
  for (index, value) in bufferPointer.enumerated() {
    print("value \(index): \(value)")
  }
}
이 예제는 이전 것과 비슷한데, 다른점은 처음에 raw 포인터를 생성한다는 것이다. typed 포인터는 필요한 Int 타입으로 메모리를 바인딩하면서 생성된다. 메모리를 바인딩함으로서 타입 세이프한 방법으로 접근할 수 있게 된다. 메모리 바인딩은 typed 포인터를 생성할때 이미 완료된다.

예제에서 남은 부분은 이전것과 동일하다. 한번 typed 포인터의 세계에 발을 드리는 순간 그 예시로 'pointee'를 사용할 수 있다.

한 인스턴스의 바이트를 뽑아내기
종종 현재 있는 인스턴스의 타입에서 바이트를 조사하고 싶을 것이다. 이것은 withUnsafeBytes(of:) 메소드를 호출하여 조사할 수 있다.

아래 코드를 플레이그라운드에 추가하자.
do {
  print("Getting the bytes of an instance")
  var sampleStruct = SampleStruct(number: 25, flag: true)

  withUnsafeBytes(of: &sampleStruct) { bytes in
    for byte in bytes {
      print(byte)
    }
  }
}
이 코드는 SampleStruct 인스턴스의 raw 바이트를 출력해낸다. withUnsafeBytes(of:) 메소드는 UnsafeRawBufferPointer에 접근할 수 있게 해주는데, 클로저 안에서 사용할 수 있다.

withUnsafeBytes 또한 ArrayData의 인스턴스 메모리로서 사용할 수 있다.

체크섬 계산하기
withUnsafeBytes(of:)를 사용하면 결과를 반환할 수 있다. 이것의 사용에대한 예제는 구조체에서 32비트 체크섬의 바이트를 계산하는 것이다.

아래 코드를 플레이그라운드에 추가하자.
do {
  print("Checksum the bytes of a struct")
  var sampleStruct = SampleStruct(number: 25, flag: true)
  let checksum = withUnsafeBytes(of: &sampleStruct) { (bytes) -> UInt32 in
    return ~bytes.reduce(UInt32(0)) { $0 + numericCast($1) }
  }
  print("checksum", checksum) // prints checksum 4294967269

}
reduce를 호출해서 모든 바이트를 합쳐서 ~연산자로 비트를 뒤집는다. 특별히 강력한 에러 감지는 아니지만 그 컨셉을 보여준다.

언세이프 클럽에서의 규칙
정의되지 않은 행동을 피하기위해 언세이프한 코드를 작성할때 주의를 기울여야한다. 아래 나쁜 코드의 몇 예시가 있다.

withUnsafeBytes로부터 포인터를 반환하지마라!
// Rule 1
do {
  print("1. Don't return the pointer from withUnsafeBytes!")

  var sampleStruct = SampleStruct(number: 25, flag: true)

  let bytes = withUnsafeBytes(of: &sampleStruct) { bytes in
    return bytes // strange bugs here we come ☠️☠️☠️
  }

  print("Horse is out of the barn!", bytes)  /// undefined !!!
}
절때 포인터를 withUnsafeBytes(of:) 클로저 밖으로 내보내지 마라. 그 코드가 오늘은 동작할지라도...

오직 한번에 한 타입을 바인드하라!
// Rule 2
do {
  print("2. Only bind to one type at a time!")

  let count = 3
  let stride = MemoryLayout<Int16>.stride
  let alignment = MemoryLayout<Int16>.alignment
  let byteCount =  count * stride
  let pointer = UnsafeMutableRawPointer.allocate(bytes: byteCount, alignedTo: alignment)

  let typedPointer1 = pointer.bindMemory(to: UInt16.self, capacity: count)

  // Breakin' the Law... Breakin' the Law  (Undefined behavior)
  let typedPointer2 = pointer.bindMemory(to: Bool.self, capacity: count * 2)

  // If you must, do it this way:
  typedPointer1.withMemoryRebound(to: Bool.self, capacity: count * 2) {
    (boolPointer: UnsafeMutablePointer<Bool>) in
    print(boolPointer.pointee)  // See Rule 1, don't return the pointer
  }
}

badpunbadpun


절때 한번에 두가지 연관된 타입을 메모리에 바인딩하지마라. 이것을 Type Punning(역자: 컴퓨터 과학에서, 타입 펀닝(type punning)이란 언어의 범주에서 달성하기가 힘들거나 불가능한 기능을 만들기 위해 언어의 형식 시스템을 우회하는 프로그래밍 기법을 말함)이라 부르며 스위프트는 pun을 좋아하지 않는다. 대신에, withMemoryRebound(to: capacity:)같은 메소드로 임시적인 메모리를 리바인드할 수 있다. 또한 이 룰은 trivial 타입(Int와같은)부터 non-trivial 타입(클래스와같은)까지 리바인드하는 것은 불법이라고 말하고 있다. 그러지 말자.

하나 더 남았다
// Rule 3... wait
do {
  print("3. Don't walk off the end... whoops!")

  let count = 3
  let stride = MemoryLayout<Int16>.stride
  let alignment = MemoryLayout<Int16>.alignment
  let byteCount =  count * stride

  let pointer = UnsafeMutableRawPointer.allocate(bytes: byteCount, alignedTo: alignment)
  let bufferPointer = UnsafeRawBufferPointer(start: pointer, count: byteCount + 1) // OMG +1????

  for byte in bufferPointer {
    print(byte)  // pawing through memory like an animal
  }
}
현재 OBOE(off-by-one error: 원하지 않았던 추가적인 사이즈에 관한 에러) 문제는 특히 심각하게 언세이프한 코드이다. 조심하고, 검토하며, 테스트하라!

언세이프 스위프트 예제1: 압축
여러분의 모든 지식을 가져와서 C.API를 감싸볼 시간이다. 코코아는 일반적으로 데이터를 압축하는 알고리즘을 구현한 C 모듈을 포함한다. 여기서 LZ4는 속도가 중요시될때, LZ4A는 속도보다는 높은 압축률이 중요시될때, ZLIB는 공간과 속도가 균형있게 중요시되고 새로운것(오픈소스)이고, LZFSE도 공간과 속도의 균형 면에서 잘 동작한다.

Compression이라 부르는 플레이그라운드를 생성하자. Data를 사용하는 순수 스위프트 API를 정의함으로서 시작해보자.

그러고나서 아래 코드를 여러분의 플레이그라운드에 넣자.
import Foundation
import Compression

enum CompressionAlgorithm {
  case lz4   // speed is critical
  case lz4a  // space is critical
  case zlib  // reasonable speed and space
  case lzfse // better speed and space
}

enum CompressionOperation {
  case compression, decompression
}

// return compressed or uncompressed data depending on the operation
func perform(_ operation: CompressionOperation,
             on input: Data,
             using algorithm: CompressionAlgorithm,
             workingBufferSize: Int = 2000) -> Data?  {
  return nil
}
이 함수는 압축과 해제를 하는 perform 함수인데 지금은 nil을 리턴하게 해두었다. 여기에 짧은 언세이프 코드를 추가하게 될것이다.

다음 프레이그라운드 마지막에 아래의 코드를 추가하자.
// Compressed keeps the compressed data and the algorithm
// together as one unit, so you never forget how the data was
// compressed.

struct Compressed {
  let data: Data
  let algorithm: CompressionAlgorithm
  init(data: Data, algorithm: CompressionAlgorithm) {
    self.data = data
    self.algorithm = algorithm
  }
  // Compress the input with the specified algorithm. Returns nil if it fails.
  static func compress(input: Data,
                       with algorithm: CompressionAlgorithm) -> Compressed? {
    guard let data = perform(.compression, on: input, using: algorithm) else {
      return nil
    }
    return Compressed(data: data, algorithm: algorithm)
  }
  // Uncompressed data. Returns nil if the data cannot be decompressed.
func decompressed() -> Data? {
    return perform(.decompression, on: data, using: algorithm)
  }

}
entryData 타입의 익스텐션이다. 여러분은 옵셔널 Compressed 구조체를 반환하는 compressed(with:)라는 메소드를 추가했었다. 이 메소드는 간단하게 Compressed에서 compress(input:with:)라는 스테틱 메소드를 호출한다.

마지막에있는 예제사용은 현재 동작하지는 않지만, 지금 고쳐보자!

여러분이 처음 코드로 들어왔던 곳으로 스크롤을 올려보고, perform(_: on: using: workingBufferSize:) 함수를 아래처럼 만들어보자.
func perform(_ operation: CompressionOperation,
             on input: Data,
             using algorithm: CompressionAlgorithm,
             workingBufferSize: Int = 2000) -> Data?  {
  // set the algorithm
  let streamAlgorithm: compression_algorithm
  switch algorithm {
  case .lz4:   streamAlgorithm = COMPRESSION_LZ4
  case .lz4a:  streamAlgorithm = COMPRESSION_LZMA
  case .zlib:  streamAlgorithm = COMPRESSION_ZLIB
  case .lzfse: streamAlgorithm = COMPRESSION_LZFSE
  }
  // set the stream operation and flags
  let streamOperation: compression_stream_operation
  let flags: Int32
  switch operation {
  case .compression:
    streamOperation = COMPRESSION_STREAM_ENCODE
    flags = Int32(COMPRESSION_STREAM_FINALIZE.rawValue)
  case .decompression:
    streamOperation = COMPRESSION_STREAM_DECODE
    flags = 0
  }
  return nil /// To be continued
}
압축 알고리즘과 실행 작업을 위해 여러분의 스위프트 타입에서 압축 라이브러리에 필요한 C 타입으로 변환하게 된다.

다음으로 return nil을 아래로 바꾸자.
// 1: create a stream
var streamPointer = UnsafeMutablePointer<compression_stream>.allocate(capacity: 1)
defer {
  streamPointer.deallocate(capacity: 1)
}

// 2: initialize the stream
var stream = streamPointer.pointee
var status = compression_stream_init(&stream, streamOperation, streamAlgorithm)
guard status != COMPRESSION_STATUS_ERROR else {
  return nil
}
defer {
  compression_stream_destroy(&stream)
}

// 3: set up a destination buffer
let dstSize = workingBufferSize
let dstPointer = UnsafeMutablePointer<UInt8>.allocate(capacity: dstSize)
defer {
  dstPointer.deallocate(capacity: dstSize)
}

return nil /// To be continued
위 코드에서 무슨일이 일어나는지 보자.
  1. compression_stream을 할당하고 소멸을위해 defer 블럭으로 준비한다.
  2. 그러고 pointee 프로퍼티를 사용하여 스트림을 가져와서, compression_stream_init 함수에 보내준다. 이 컴파일러는 여기서 좀 특별한 일을 한다. inout & maker를 이용하여 여러분의 compression_stream을 받아서 자동으로 UnsafeMutablePointer<compression_stream>으로 바꾼다.(streamPointer로 보낼 수도 있고 이 특정 변환을 필요로 하지 않을 수도 있다)
  3. 마지막으로 여러분이 작업할 버퍼인 목적지 버퍼를 생성한다.
return nil을 바꾸어서 perform 함수를 완성했다.
// process the input
return input.withUnsafeBytes { (srcPointer: UnsafePointer<UInt8>) in
  // 1
  var output = Data()
  // 2
  stream.src_ptr = srcPointer
  stream.src_size = input.count
  stream.dst_ptr = dstPointer
  stream.dst_size = dstSize
  // 3
  while status == COMPRESSION_STATUS_OK {
    // process the stream
    status = compression_stream_process(&stream, flags)
    // collect bytes from the stream and reset
    switch status {
    case COMPRESSION_STATUS_OK:
      // 4
      output.append(dstPointer, count: dstSize)
      stream.dst_ptr = dstPointer
      stream.dst_size = dstSize
    case COMPRESSION_STATUS_ERROR:
      return nil
    case COMPRESSION_STATUS_END:
      // 5
      output.append(dstPointer, count: stream.dst_ptr - dstPointer)
    default:
      fatalError()
    }
  }
  return output
}
이것이 실제로 일어나는 것이다. 그리고 무슨 일이 일어나는지 보자.
  1. 아웃픗(압축된 데이터든 압축해제된 데이터든, 그 작업에따라 다르다)을 담은 Data 오브젝트를 생성한다.
  2. 당신이 할당한 포인터와 그 크기와함께 소스버퍼와 목적지버퍼를 설정한다.
  3. 그리고 COMPRESSION_STATUS_OK를 반환할때까지 계속 compression_stream_process를 호출한다.
  4. 목적지버퍼는 마침내 그 함수로부터 반환된 아웃풋으로 복사된다.
  5. 마지막 패킷이 들어오면, COMPRESSION_STATUS_END가 나오고, 오직 목적지버퍼의 부분만 잠재적으로 복사되는것이 필요하다.
사용 예제에서 10000 요소의 배열이 153바이트로 압축된 것을 볼 수 있다. 나쁘지 않은 결과이다.

언세이프 스위프트 예제2: 난수 생성기
난수는 게임에서부터 기계학습에 이르기까지 많은 어플리케이션에서 중요한 부분으로 다뤄진다. macOS는 훌륭한 (암호학적으로 풀기힘든) 난수를 생성하는 arc4random(A Replacement Call 4 random)을 제공한다. 불행히도 이것은 리눅스에서 사용할 수 없다. 게다가 arc4randomInt32 난수만 제공한다. 그러나 dev/urandom파일은 무제한으로 난수를 제공한다.

이번 섹션에서는 이 파일을 읽은 새로운 정보로 완전히 타입 세이프한 난수를 만들어 볼 것이다.

hexdumphexdump


RandomNumbers라는 새 플레이그라운드를 만듦으로서 시작해보자. 이번시간에는 macOS 플랫폼을 선택했는지 확인하자.

만들고나면 원래 있던 것을 아래의 것으로 바꾸자.
import Foundation

enum RandomSource {
  static let file = fopen("/dev/urandom", "r")!
  static let queue = DispatchQueue(label: "random")
  static func get(count: Int) -> [Int8] {
    let capacity = count + 1 // fgets adds null termination
    var data = UnsafeMutablePointer<Int8>.allocate(capacity: capacity)
    defer {
      data.deallocate(capacity: capacity)
    }
    queue.sync {
      fgets(data, Int32(capacity), file)
    }
    return Array(UnsafeMutableBufferPointer(start: data, count: count))
  }
}
file이라는 변수는 static으로 선언해 놓았으므로 시스템에 오직 하나만 존재하게 될것이다. 그 프로세스가 종료될때 당신은 그것에 접근하는 시스템에 의존할 것이다. 여러 스레드에서 난수를 요구할 수도 있기 때문에 일련의 GCD큐로 접근하는 상황을 막아야한다.

get 함수가 작업이 일어나는 곳이다.  먼저, 할당되지 않은 저장소를 생성하는데, fgets은 항상 0에 종료되므로, 당신이 원하는 것보다 하나 더 많게 준비한다. 다음으로 GCD 큐에서 작업하면서 파일로부터 데이터를 가져온다. 마지막으로는, Sequence처럼 동작하는 UnsafeMutableBuffePointer로 감싸서 표준 배열에 데이터를 복사한다.

지금까지 Int8값의 배열만 (안전하게) 줄것이다. 이제 이것을 확장시켜보자.

플레이그라운드의 마지막 부분에 아래를 추가하자.
extension Integer {
  static var randomized: Self {
    let numbers = RandomSource.get(count: MemoryLayout<Self>.size)
    return numbers.withUnsafeBufferPointer { bufferPointer in
      return bufferPointer.baseAddress!.withMemoryRebound(to: Self.self, capacity: 1) {
        return $0.pointee
      }
    }
  }

}

Int8.randomized
UInt8.randomized
Int16.randomized
UInt16.randomized
Int16.randomized
UInt32.randomized
Int64.randomized
UInt64.randomized
Integer 프로토콜의 모든 하위타입에 static randomized 프로퍼티를 추자했다(Portocol Oriented Programming를 읽어보자!) 먼저 난수를 얻어서 반환된 배열의 바이트와함께 요청된 타입으로 Int8 값을 리바인드한 뒤(C++의 reinterpret_cast 처럼) 복사본을 반환한다. 간단하다! :]

이게 다다! 언세이프 스위프트를 사용한 안전한 방법으로의 난수이다.

여기서 어디로 가야할까?
여기 모든 플레이그라운드가 있다. 그리고 여러분이 더 배우려고 찾아볼 수 있는 추가적인 자료들이 있다.

여러분이 이 튜토리얼을 즐겼기를 바란다. 만약 질문이 생기거나 공유하고 싶은 경험을 겪게되면 이 포럼(링크)에서 그것을 기대하고 있겠다!



이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.


+ Recent posts