WWDC Quick Look 💓 By SwiftGGTeam
Safely manage pointers in Swift

Safely manage pointers in Swift

观看原视频

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,让泛型参数维持类型安全;需要按字节解释数据时,用 UnsafeRawPointerUnsafeRawBufferPointer;只有在原始指针丢失了类型,或必须临时改写绑定类型时,才进入 assumingMemoryBoundbindMemorywithMemoryRebound 这一层。每下降一层,代码越灵活,证明成本越高。

调试也不能只靠崩溃。transcript 中的图片计数例子说明,错误的指针类型可能让编译器做出错误假设,最后表现为数据静默丢失。Address Sanitizer 和 debug assertion 能缩短排查时间,但它们不会覆盖所有 undefined behavior。更可靠的办法,是把 unsafe 范围压到最小,并在代码里写出边界、对齐和绑定类型的检查。

详细内容

typed pointer 的类型必须匹配内存绑定

05:44)Session 用一个从 C 风格接口迁移过来的例子说明,类型不一致的指针会把错误交给编译器优化阶段。collage.imageCountInt,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:17bindMemory(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>.stridealignmentinitializeMemory(as:) 开始,把 unsafe 细节藏在一个小类型内部,对外暴露普通 collection 接口。

  • 遗留指针代码审计工具:给迁移自 C 的 Swift 代码找出危险转换点;这场 session 的核心风险是 bindMemory 和错误的 assumingMemoryBound 会让旧 typed pointer 失效;从搜索 bindMemoryassumingMemoryBoundUnsafeMutableRawPointer 开始,把只读场景改成 raw load,把短期重绑场景改成 withMemoryRebound

关联 Session

评论

GitHub Issues · utterances