Highlight
Swift 把 unsafe pointer 操作分成 typed pointer、raw pointer 和 memory binding 三层,开发者需要在类型绑定、生命周期和边界三件事上承担逐级增加的责任。
核心内容
多数 Swift 代码不该碰指针。数组、切片、迭代器和标准库集合已经覆盖了很多过去会用指针解决的问题。Session 一开始就把这件事说清楚:最安全的策略是不用指针;一旦用了带 Unsafe 前缀的 API,编译器和运行时就少管一部分事,开发者需要补上对应的正确性证明。
麻烦通常来自互操作。你可能要把 Swift 数据传给 C API,或从 Data 这样的字节流里解码结构化值。C 代码常用指针转换解决问题,但 Swift 的 typed pointer 规则更严格:内存被绑定到某个类型后,UnsafePointer<T> 和 UnsafeMutablePointer<T> 只能按这个类型读写。指针地址看起来对,不代表类型也对。
Apple 在这场 session 里给出了一条向下走的阶梯。先用 typed pointer,让泛型参数维持类型安全;需要按字节解释数据时,用 UnsafeRawPointer 或 UnsafeRawBufferPointer;只有在原始指针丢失了类型,或必须临时改写绑定类型时,才进入 assumingMemoryBound、bindMemory、withMemoryRebound 这一层。每下降一层,代码越灵活,证明成本越高。
调试也不能只靠崩溃。transcript 中的图片计数例子说明,错误的指针类型可能让编译器做出错误假设,最后表现为数据静默丢失。Address Sanitizer 和 debug assertion 能缩短排查时间,但它们不会覆盖所有 undefined behavior。更可靠的办法,是把 unsafe 范围压到最小,并在代码里写出边界、对齐和绑定类型的检查。
详细内容
typed pointer 的类型必须匹配内存绑定
(05:44)Session 用一个从 C 风格接口迁移过来的例子说明,类型不一致的指针会把错误交给编译器优化阶段。collage.imageCount 是 Int,C 风格函数却要 UnsafeMutablePointer<UInt32>,代码通过 raw pointer 绕过了 Swift 的类型检查。
struct Image {
// elided...
}
struct Collage {
var imageData: UnsafeMutablePointer<Image>?
var imageCount: Int = 0
}
func addImages(_ countPtr: UnsafeMutablePointer<UInt32>) -> UnsafeMutablePointer<Image> {
// ...
let imageData = UnsafeMutablePointer<Image>.allocate(capacity: 1)
imageData[0] = Image()
countPtr.pointee += 1
return imageData
}
func saveImages(_ imageData: UnsafeMutablePointer<Image>, _ count: Int) {
// Arbitrary function body...
print(count)
}
var collage = Collage()
collage.imageData = withUnsafeMutablePointer(to: &collage.imageCount) {
addImages(UnsafeMutableRawPointer($0).assumingMemoryBound(to: UInt32.self))
}
saveImages(collage.imageData!, collage.imageCount) // May see imageCount == 0
关键点:
withUnsafeMutablePointer(to:)给出的是指向Int存储的 typed pointer。UnsafeMutableRawPointer($0)擦除了类型信息,但没有改变内存真实绑定类型。assumingMemoryBound(to: UInt32.self)要求调用者保证内存已经绑定到UInt32;这里并不满足。- 编译器可以认为
Int属性没有被修改,后续读取可能仍得到初始值。
typed pointer 分配会绑定类型
(10:06)如果确实要直接分配内存,UnsafeMutablePointer<T>.allocate 会把新内存绑定到 T。这比 raw allocation 少一层证明,因为类型从分配开始就固定了。
func directAllocation<T>(t: T, count: Int) {
let tPtr = UnsafeMutablePointer<T>.allocate(capacity: count)
tPtr.initialize(repeating: t, count: count)
tPtr.assign(repeating: t, count: count)
tPtr.deinitialize(count: count)
tPtr.deallocate()
}
关键点:
allocate(capacity:)返回UnsafeMutablePointer<T>,内存已经绑定到泛型参数T。initialize负责把未初始化的内存放入有效值。assign在同一绑定类型下替换已有值。deinitialize结束值的生命周期,deallocate释放存储;这两个步骤都不能漏。
raw pointer 适合按字节读取外部数据
(14:24)当目标是从字节流中读取值,raw pointer 更合适。它把内存看成一串字节,读取时再指定目标类型。开发者负责 byte offset、类型布局、对齐和端序。
import Foundation
func readUInt32(data: Data) -> UInt32 {
data.withUnsafeBytes { (buffer: UnsafeRawBufferPointer) in
buffer.load(fromByteOffset: 4, as: UInt32.self)
}
}
let data = Data(Array<UInt8>([0, 0, 0, 0, 1, 0, 0, 0]))
print(readUInt32(data: data))
关键点:
Data.withUnsafeBytes把底层存储暴露为UnsafeRawBufferPointer,指针只在闭包内有效。fromByteOffset: 4表示从第五个字节开始读取。as: UInt32.self只影响这次 load 的解释方式,不会重新绑定整块内存。- 这类代码应在读取前验证长度和对齐,空数据或过短数据不能直接 load。
raw allocation 用于自己管理连续布局
(15:43)raw allocation 的价值在于一块连续内存里放不同类型,例如头部加可变数量的元素。此时你要自己计算 stride、alignment 和每个区域的起始地址。
func contiguousAllocate<Header>(header: Header, numValues: Int) -> (UnsafeMutablePointer<Header>, UnsafeMutablePointer<Int32>) {
let offset = MemoryLayout<Header>.stride
let byteCount = offset + MemoryLayout<Int32>.stride * numValues
assert(MemoryLayout<Header>.alignment >= MemoryLayout<Int32>.alignment)
let bufferPtr = UnsafeMutableRawPointer.allocate(
byteCount: byteCount, alignment: MemoryLayout<Header>.alignment)
let headerPtr = bufferPtr.initializeMemory(as: Header.self, repeating: header, count: 1)
let elementPtr = (bufferPtr + offset).initializeMemory(as: Int32.self, repeating: 0, count: numValues)
return (headerPtr, elementPtr)
}
关键点:
byteCount必须覆盖 header 和所有Int32元素。alignment不能随便取,示例用 assertion 确保 header 对齐要求足以覆盖元素区域。initializeMemory(as:)会把对应区域绑定到指定类型,并返回 typed pointer。- 这种技巧适合实现内部容器,不适合作为业务代码的默认选择。
memory binding API 是最后一层
(21:17)bindMemory(to:capacity:) 会改变内存的绑定类型。它不会神奇地转换值,只是向编译器声明这块内存现在属于另一个类型。旧 typed pointer 随即失效。
func testBindMemory() {
let uint16Ptr = UnsafeMutablePointer<UInt16>.allocate(capacity: 2)
uint16Ptr.initialize(repeating: 0, count: 2)
let int32Ptr = UnsafeMutableRawPointer(uint16Ptr).bindMemory(to: Int32.self, capacity: 1)
// Accessing uint16Ptr is now undefined
int32Ptr.deallocate()
}
关键点:
uint16Ptr原本指向两段UInt16存储。bindMemory(to: Int32.self, capacity: 1)把同一地址范围重新绑定为一个Int32。- 重新绑定后继续访问
uint16Ptr属于 undefined behavior。 - 只想读取不同类型时,优先用 raw pointer 的
load,不要为了读取去改写绑定状态。
(23:13)如果只是为了调用一个类型要求不同的 C API,withMemoryRebound 通常更安全。它把重新绑定限制在闭包内,结束后恢复原类型。
func takesUInt8Pointer(_: UnsafePointer<UInt8>) { /* elided */ }
func testWithMemoryRebound(int8Ptr: UnsafePointer<Int8>, count: Int) {
int8Ptr.withMemoryRebound(to: UInt8.self, capacity: count) {
(uint8Ptr: UnsafePointer<UInt8>) in
// int8Ptr cannot be used within this closure
takesUInt8Pointer(uint8Ptr)
}
// uint8Ptr cannot be used outside this closure
}
关键点:
withMemoryRebound把类型重新绑定的影响限制在闭包作用域。- 闭包内部不能再用原来的
int8Ptr访问同一内存。 uint8Ptr不能逃逸到闭包外。- 适用场景是短时间调用外部 API,尤其是 C API 对同一批字节使用不同指针类型时。
核心启发
-
二进制格式解码器:做一个读取自定义文件头或网络包的解析层;raw buffer 允许按 offset 读取
UInt32、标志位和长度字段;从Data.withUnsafeBytes开始,先写长度、对齐和端序检查,再调用load(fromByteOffset:as:)。 -
C callback 上下文封装:把
pthread_create这类只回传void *的接口包成 Swift API;session 证明assumingMemoryBound只适合恢复自己刚刚绑定过的类型;从一个ThreadContext指针开始,在分配、初始化、回调恢复和释放四处集中管理生命周期。 -
内部连续存储容器:为性能敏感的数据结构实现 header 加元素区的单块内存布局;raw allocation 能减少分散分配;从
MemoryLayout<T>.stride、alignment和initializeMemory(as:)开始,把 unsafe 细节藏在一个小类型内部,对外暴露普通 collection 接口。 -
遗留指针代码审计工具:给迁移自 C 的 Swift 代码找出危险转换点;这场 session 的核心风险是
bindMemory和错误的assumingMemoryBound会让旧 typed pointer 失效;从搜索bindMemory、assumingMemoryBound、UnsafeMutableRawPointer开始,把只读场景改成 rawload,把短期重绑场景改成withMemoryRebound。
关联 Session
- Unsafe Swift — 本场的前置内容,先解释 Swift unsafe 操作、C API 指针和 undefined behavior 的基本边界。
- Embrace Swift type inference — 从类型推断角度理解编译器怎样使用类型事实,帮助判断为什么指针类型不能随意伪装。
- Advancements in the Objective-C runtime — 继续看 Objective-C 和 Swift 类背后的低层布局、tagged pointers 与运行时变化。
- Refine Objective-C frameworks for Swift — 如果 unsafe pointer 来自 Objective-C 互操作,先通过头文件注解和命名把 Swift API 表面整理清楚。
评论
GitHub Issues · utterances