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 - 結論
-
withUnsafe-
方法中獲取的指標一定不要讓其"逃出"「不安全區」,僅在所屬不安全閉包中使用,否則該指標將不再受控制,導致不可預測的問題,如崩潰。 - 一定不要隱式獲取變數的不安全指標,這會隱藏上述結論 1. 中的問題,更難以察覺。
let bodyToSend = UnsafePointer(&bodyData) // 一定不要這麼獲取變數指標
複製程式碼
Swift 的安全性
想必稍微對 Swift 語言有所瞭解都會知道這是一門安全的程式語言,因此,在談及其不安全的部分之前,先說說它的安全性:
- 記憶體安全 ❤️
- 不可訪問未初始化的記憶體 ?
- 防止野指標訪問,陣列無法越界 ?
- 避免未定義行為 ?
關於安全性,Swift 語言的設計者們對此的定義不是不崩潰,而是:
永遠不會在無意中訪問錯誤的記憶體
為此,Swift 做兩件事,一是讓編譯器時刻提醒開發者注意安全;二則是開發者強行開車導致產生未定義的行為的話,立即原地爆炸?,避免更嚴重的問題發生。
不安全的 Swift - UnsafePointer
既然 Swift 追求安全,為什麼要設計不安全的部分呢?通俗地講,就是允許有經驗的老司機開黑車:防抱死功能一關,請開始你的表演。
- 為超高效能實現提供方案,安全與高效能常常需要權衡與妥協
- 與其它非安全的語言,如 C,進行互動,包括直接訪問硬體
Swift 記憶體分配與佈局
認識UnsafePoint
前,我們先來了解下 Swift 如何對記憶體進行分配與佈局的。
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,value) in bufferPointer.enumerated() {
print("value \(offset): \(value)")
}
複製程式碼
可變性與不可變性
Swift 中通過 let
和 var
關鍵字區分變數的可變性,指標中也採用了類似方案,讓開發者針對性地控制指標的可變性,即其指向的記憶體塊的可寫性。
Buffer 指標
Buffer 指標,其本質就是一個指標+一個大小count
,即一串連續的記憶體塊。
指標使用過程中的一些重要原則 ⚠️
Swift 是一門型別安全的語言,但當代碼中出現Unsafe
字樣時,務必遵循以下一些指標操作原則,以避免未定義行為發生,否則遇到問題時將非常難以定位。
- 指標使用前,務必分配記憶體並初始化
- 務必釋放已分配的記憶體
- 務必恢復已初始化的記憶體
- 千萬別在
withUnsafe-
方法中返回獲取的指標 -
bindMemory
一次僅可繫結一種型別 - 指標操作不要「越界 」
- 不要多次釋放或逆初始化同一塊記憶體