[譯]Unsafe Swift - 指針與C交互

默認狀況下,Swift是內存安全的,這就意味着 Swift 會避免內存的直接訪問,並肯定全部變量會在初始化後才進行使用。這裏的關鍵詞語是「默認的」。也就是說,當咱們須要時,不安全的 Swift 仍是能夠經過指針來直接訪問到內存地址的swift

這篇教程會告訴你爲啥 Swift 又是「不安全的」。「不安全」這個詞可能會令人誤解,它不是說咱們寫的代碼可能會發生出乎意料的問題,而是說咱們須要額外注意這些代碼,由於編譯器在編譯過程當中並不會給予咱們幫助,咱們沒法直觀地捕獲到錯誤。緩存

在開發工做中,當你須要與C這些不安全的語言進行交互時,就會用得上這篇文章所介紹的特性了。安全

開始

這篇教程由3個 playgrounds 組成。在第一個 playground 中,將會建立幾個簡短的片斷來了解內存佈局而且使用不安全的指針。在第二個 playground 中,會封裝C語言的API來執行流數據的壓縮。最後的 playground ,建立arc4random的替代函數dom

首先建立一個新的 playground,名稱爲UnsafeSwiftide

內存佈局

不安全的 Swift 會直接在內存系統中進行交互。內存能夠經過一系列格子來進行可視化,每一個格子裏是與內存相關的惟一性的內存地址。存儲的最小單位是 byte,由8個 bits 組成。8 bit 的 byte 能夠保存0-255大小的數字。函數

Swift 有一個MemoryLayout的類能夠告訴咱們每一個類型對象的大小和分佈。添加以下代碼:佈局

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>會在編譯時肯定指定類型的size,alignmentstride。舉個例子來講, Int16size是2個byte,內存對齊也是2.這就意味着其內存地址必須是偶數地址ui

所以,假設地址100和101給Int16分配地址的話,確定就是選擇100了, 由於101違背了對齊原則。當咱們將一堆Int16打包在一塊兒的話,stride表示的是,當前類型的內存地址開頭與下一個內存地址開頭之間的距離spa

接下來,看看用戶自定義的structs的內存佈局:設計

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

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

MemoryLayout<SampleStruct>.size         // 5
MemoryLayout<SampleStruct>.alignment    // 4
MemoryLayout<SampleStruct>.stride       // 8
複製代碼

空的結構體的size爲0.由於空結構體的stride爲1,因此它能夠分配在任意的地址上。

對於SampleStruct來講, 其size爲5,stride爲8.這是由於內存對齊的位數是4個字節。

而後看下類對象的:

class EmptyClass {}

MemoryLayout<EmptyClass>.size      // 8
MemoryLayout<EmptyClass>.alignment // 8
MemoryLayout<EmptyClass>.stride    // 8

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

MemoryLayout<SampleClass>.size      // 8
MemoryLayout<SampleClass>.alignment // 8
MemoryLayout<SampleClass>.stride    // 8
複製代碼

能夠看到,類對象的sizealignmentstride都是8,且不論是否空的對象。

指針

指針對象包含了一個內存地址。直接涉及內存訪問的類型會有一個unsafe的前綴,因此其指針稱爲UnsafePointer. 雖然長長的類型看起來會比較煩,可是可使咱們清楚地知道相關的代碼是沒有通過相關的編譯器檢查,可能會致使未定義的行爲(而不只僅是一個可預見的崩潰)。

Swift 的設計者其實能夠建立了一個UnsafePointer類型,而且該類型與C語言中的char *相等,能夠用來以非結構化方式來訪問內存。可是他們並無。Swift 涵蓋了大部分的指針類型,每種類型都有不一樣的用處和目的。使用合適的指針類型能夠更好地達到咱們的需求,更少地引發錯誤。

不安全的 Swift 指針的命名可讓咱們知道該指針的特徵。可變( Mutable )或者不可變( Immutable ), 原始的( raw ) 或者其餘類型的( typed ),是不是緩存風格( buffer style ). 在 Swift 中,一共有8種類型組合:

Unsafe[Mutable][Raw][Buffer]Pointer[]

指針就是內存地址,直接訪問內存就是 Unsafe

Mutable 表示可寫

Raw 表示它是否指向了二進制數據類型的字節(blob of bytes)

Buffer 表示其是不是一個結合

原始指針的使用

// 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)")
    }
}

// Output
// Raw pointers
// byte 0: 42
// byte 1: 0
// byte 2: 0
// byte 3: 0
// byte 4: 0
// byte 5: 0
// byte 6: 0
// byte 7: 0
// byte 8: 6
// byte 9: 0
// byte 10: 0
// byte 11: 0
// byte 12: 0
// byte 13: 0
// byte 14: 0
// byte 15: 0

複製代碼

上面的例子中,使用不安全的 Swift 指針來保存並加載兩個整數:

  1. 常量:

    • count: 要保存的整數的個數
    • stride: Int的步長
    • alignment: Int的內存對齊位數
    • byteCount: 總字節數
  2. do添加了一個塊級做用域,方便從新使用變量名

  3. UnsafeMutableRawPointer.allocate用於分配所須要的字節數。該方法返回一個UnsafeMutableRawPointer指針。從名稱咱們能夠得知該指針能夠用來加載和保存原始類型的字節

  4. defer用來保證指針可以獲得釋放。

  5. storeBytesload用於存儲和加載字節。第二個整數的內存地址根據指針的步長進行計算得出。

  6. UnsafeRawBufferPointer讓咱們能夠像訪問字節集合同樣來對內存地址進行訪問。就是咱們能夠遍歷字節,經過下標訪問,或者是調用map, filter等方法。緩存區的指針須要使用原始指針來進行初始化

類型指針的使用

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)")
    }
}

// Output
// Typed pointers
// value 0: 42
// value 1: 6

複製代碼

與原始指針的不一樣點在於:

  • UnsafeMutablePointer.allocate方法用於分配內存。
  • 類型對象的內存必須在使用前進行初始化,再也不使用之後須要進行析構處理。
  • 類型指針有一個屬性pointee用於加載和保存值
  • 當向前移動類型指針時,能夠很方便標誌指針所指向的位置。指針會根據其指向的類型,計算出正確地步長。
  • 類型的 buffer 指針也能夠遍歷指針對象

原始指針轉換爲類型指針

類型指針並不必定須要直接進行初始化,也能夠經過原始指針來進行轉換:

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)")
    }
}

// Output
// Converting raw pointers to typed pointers
// value 0: 42
// value 1: 6
複製代碼

這個例子除了首先建立了原始指針而後將內存綁定到類型指針上之外,與上一個類似。綁定內存後,咱們就能經過一種類型安全的方式對內存進行訪問。內存綁定在咱們建立類型指針時會隱式進行

獲取實例的字節數

通常狀況下,咱們能夠經過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)
    }
  }
}

// Output
// Getting the bytes of an instance
// 25
// 0
// 0
// 0
// 1
複製代碼

這個例子輸出了SampleStruct的實例的原始字節,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)
}
複製代碼

Unsafe 的使用規則

  • 不要在withUnsafeBytes中返回指針
  • 一次只綁定一種類型
  • 避免指針指向最後的位置

原文地址:Unsafe Swift: Using Pointers And Interacting With C

相關文章
相關標籤/搜索