[譯]Unsafe Swift – 指標與C互動

kim_jin發表於2019-03-03

預設情況下,Swift是記憶體安全的,這就意味著 Swift 會避免記憶體的直接訪問,並確定所有變數會在初始化後才進行使用。這裡的關鍵詞語是“預設的”。也就是說,當我們需要時,不安全的 Swift 還是可以通過指標來直接訪問到記憶體地址的

這篇教程會告訴你為啥 Swift 又是“不安全的”。“不安全”這個詞可能會使人誤解,它不是說我們寫的程式碼可能會發生出乎意料的問題,而是說我們需要額外注意這些程式碼,因為編譯器在編譯過程中並不會給予我們幫助,我們無法直觀地捕獲到錯誤。

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

開始

這篇教程由3個 playgrounds 組成。在第一個 playground 中,將會建立幾個簡短的片段來了解記憶體佈局並且使用不安全的指標。在第二個 playground 中,會封裝C語言的API來執行流資料的壓縮。最後的 playground ,建立arc4random的替代函式

首先建立一個新的 playground,名稱為UnsafeSwift

記憶體佈局

[譯]Unsafe Swift – 指標與C互動

不安全的 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.這就意味著其記憶體地址必須是偶數地址

因此,假設地址100和101給Int16分配地址的話,肯定就是選擇100了, 因為101違背了對齊原則。當我們將一堆Int16打包在一起的話,stride表示的是,當前型別的記憶體地址開頭與下一個記憶體地址開頭之間的距離

接下來,看看使用者自定義的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 Swift – 指標與C互動

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

相關文章