Swift Unsafe Part - 「危險的 Swift 」指北

前言

此篇文章背景源自一次偶現高頻次崩潰問題排查。底層長鏈接通訊採用 Rust 編寫,涉及與業務層的橋接:Rust <-> C <-> Swift,雖然說 Rust 與 Swift 都以安全著稱,但不論是 Rust FFI 到 C 仍是 Swift 與 C 的交互,代碼中都不得不觸及unsafe關鍵字,也是此次崩潰問題的緣由所在,這就不難理解爲何 Swift 和 Rust 的設計者絕不保留地採用Unsafe-命名與unsafe關鍵字了。編程

![](https://user-gold-cdn.xitu.io/2019/9/14/16d2cef2e1ee2b06?w=732&h=164&f=jpeg&s=21782)
// typedef struct {
// const uint8_t *array;
// size_t len;
// } ByteArray;

// ......

// 問題代碼
let bodyToSend = bodyData.withUnsafeBytes { (bytes: UnsafePointer<UInt8>) in
    bytes
}
// 修復代碼
let bodyToSend = [UInt8](bodyData)

// 調用 C 方法與 Rust 交互
let len = bodyData.count
let bodyByteArray = ByteArray(array: bodyToSend, len: len)
halo_send_message(1, namespace, path, metadataToSend, bodyByteArray)

// ......
複製代碼

TL;DR - 結論

  1. withUnsafe-方法中獲取的指針必定不要讓其"逃出"「不安全區」,僅在所屬不安全閉包中使用,不然該指針將再也不受控制,致使不可預測的問題,如崩潰。
  2. 必定不要隱式獲取變量的不安全指針,這會隱藏上述結論 1. 中的問題,更難以察覺。
let bodyToSend = UnsafePointer(&bodyData) // 必定不要這麼獲取變量指針
複製代碼

Swift 的安全性

想必稍微對 Swift 語言有所瞭解都會知道這是一門安全的編程語言,所以,在談及其不安全的部分以前,先說說它的安全性:swift

  • 內存安全 ❤️
  • 不可訪問未初始化的內存 💘
  • 防止野指針訪問,數組沒法越界 💖
  • 避免未定義行爲 💕

關於安全性,Swift 語言的設計者們對此的定義不是不崩潰,而是:數組

永遠不會在無心中訪問錯誤的內存安全

爲此,Swift 作兩件事,一是讓編譯器時刻提醒開發者注意安全;二則是開發者強行開車致使產生未定義的行爲的話,當即原地爆炸💥,避免更嚴重的問題發生。閉包

不安全的 Swift - UnsafePointer

既然 Swift 追求安全,爲何要設計不安全的部分呢?通俗地講,就是容許有經驗的老司機開黑車:防抱死功能一關,請開始你的表演。編程語言

  • 爲超高性能實現提供方案,安全與高性能經常須要權衡與妥協
  • 與其它非安全的語言,如 C,進行交互,包括直接訪問硬件

Swift 內存分配與佈局

認識UnsafePoint 前,咱們先來了解下 Swift 如何對內存進行分配與佈局的。ide

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<String>.size       // returns 16
MemoryLayout<String>.alignment  // returns 8
MemoryLayout<String>.stride     // returns 16

let zero = 0.0
MemoryLayout.size(ofValue: zero)    // return 8, zero as Double implictly

struct EmptyStruct {}

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

struct SampleStruct {
  var number: UInt32
  var flag: Bool // {
// didSet {
// print("wow")
// }
// }
}

MemoryLayout<SampleStruct>.size              // returns 5
MemoryLayout<SampleStruct>.alignment         // returns 4
MemoryLayout<SampleStruct>.stride            // returns 8
MemoryLayout.offset(of: \SampleStruct.flag)  // return 4 without didSet; return nil with didSet


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)
MemoryLayout.offset(of: \SampleClass.flag)  // return nil
複製代碼

爲了解 Swift 中的佈局狀況,咱們用到了自帶的MemoryLayout工具類。工具

  • MemoryLayout<T>.size:一個 T 類型數據實例所佔的連續內存大小,單位:bytes
  • MemoryLayout<T>.alignment:數據類型 T 數據類型的對齊原則大小,單位:bytes
  • MemoryLayout<T>.stride:一個 T 類型數組中,任意一個元素從開始地址到下一個元素的開始地址所佔用的連續內存大小,單位:bytes

UnsafePointer 類型

在 Swift 中指針是幾種以Unsafe-前綴與-Pinter後綴命名的結構體,看得出來,儘管是非安全的指針操做API,Swift 也但願能儘量地作到安全。佈局

  • UnsafePointer<T>: 對應於 const T *
  • UnsafeMutablePointer<T>:對應於 T *
  • UnsafeRawPointer: 對應於 const void *
  • UnsafeMutableRawPointer:對應於 void *

泛型指針與原始指針

原始指針
let stride = MemoryLayout<Int>.stride
let alignment = MemoryLayout<Int>.alignment
let count = 2

let byteCount = stride * count
let pointer = UnsafeMutableRawPointer.allocate(byteCount: byteCount, alignment: alignment) // 原始指針對類型無感知,故指定 byteCount 與 alignment,經過 MemoryLayout 得到
defer {
  pointer.deallocate()
}
// 原始指針操做均須額外指定類型
pointer.storeBytes(of: 0b111111111111, as: Int.self)
pointer.advanced(by: stride).storeBytes(of: 6, as: Int.self)
(pointer+stride).storeBytes(of: 6, as: Int.self)

pointer.load(as: Int.self)
pointer.advanced(by: stride).load(as: Int.self)
(pointer+stride).load(as: Int.self)

let bufferPointer = UnsafeRawBufferPointer(start: pointer, count: byteCount)
for (offset, byte) in bufferPointer.enumerated() {
  print("byte \(offset): \(byte)")
}
複製代碼
泛型指針
let count = 2

let pointer = UnsafeMutablePointer<Int>.allocate(capacity: count)
pointer.initialize(repeating: 0, count: count) // 只需初始值與指定類型實例數量,相似泛型數組的初始化
defer {
  pointer.deinitialize(count: count)
  pointer.deallocate()
}

// 操做無須額外指定類型,經過泛型推斷
pointer.pointee = 0b111111111111
pointer.advanced(by: 1).pointee = 6
(pointer+1).pointee = 6

pointer.pointee
pointer.advanced(by: 1).pointee
(pointer+1).pointee

let bufferPointer = UnsafeBufferPointer(start: pointer, count: count)
for (offset, value) in bufferPointer.enumerated() {
  print("value \(offset): \(value)")
}
複製代碼
原始指針與泛型指針的轉換
let count = 2
let stride = MemoryLayout<Int>.stride
let alignment = MemoryLayout<Int>.alignment
let byteCount = stride * count

// Converting raw pointers to typed pointers

let rawPointer = UnsafeMutableRawPointer.allocate(byteCount: byteCount, alignment: alignment)
defer {
  rawPointer.deallocate()
}

// 將原始指針轉換爲泛型指針,同一地址空間僅可 bindMemory 一次
let typedPointer = rawPointer.bindMemory(to: Int.self, capacity: count)
typedPointer.initialize(repeating: 0, count: count)
defer {
  typedPointer.deinitialize(count: count)
}

typedPointer.pointee = 0b111111111111
typedPointer.advanced(by: 1).pointee = 6
typedPointer.pointee
typedPointer.advanced(by: 1).pointee

let bufferPointer = UnsafeBufferPointer(start: typedPointer, count: count)
for (offset, value) in bufferPointer.enumerated() {
  print("value \(offset): \(value)")
}
複製代碼

可變性與不可變性

Swift 中經過 letvar 關鍵字區分變量的可變性,指針中也採用了相似方案,讓開發者針對性地控制指針的可變性,即其指向的內存塊的可寫性。性能

Buffer 指針

Buffer 指針,其本質就是一個指針+一個大小count,即一串連續的內存塊。

指針使用過程當中的一些重要原則 ⚠️

Swift 是一門類型安全的語言,但當代碼中出現Unsafe字樣時,務必遵循如下一些指針操做原則,以免未定義行爲發生,不然遇到問題時將很是難以定位。

  • 指針使用前,務必分配內存並初始化
  • 務必釋放已分配的內存
  • 務必恢復已初始化的內存
  • 千萬別在withUnsafe-方法中返回獲取的指針
  • bindMemory一次僅可綁定一種類型
  • 指針操做不要「越界 」
  • 不要屢次釋放或逆初始化同一塊內存
相關文章
相關標籤/搜索