WWDC Quick Look 💓 By SwiftGGTeam
Unsafe Swift

Unsafe Swift

观看原视频

Highlight

Swift 把违反前置条件后可能进入 undefined behavior 的接口标成 Unsafe,开发者可以用 buffer pointer、withUnsafe* 闭包和 Swift 5.3 的诊断把指针生命周期限制在更小范围内。

核心内容

Swift 的安全性不等于永不崩溃。普通的强制解包遇到 nil 会触发明确的 fatal runtime error,崩溃报告能告诉你哪里破坏了约束。unsafelyUnwrapped 也要求值不能为 nil,但优化构建下它会信任调用者,直接读取值。前置条件一旦不成立,结果可能是崩溃、垃圾值,或更难追踪的状态污染。

指针把风险放大了。Swift 的运行时通常会管理对象、栈变量和动态内存的位置,开发者不需要手动追踪地址。使用 unsafe pointer 后,指针只是一个地址;它不知道内存是否还活着,也不知道那块内存现在是不是同一种 Swift 值。释放后的 dangling pointer 仍然长得像有效指针,写入它可能破坏应用的其他状态。

C 和 Objective-C 互操作是必须接触指针的主要场景。C API 常把数组、字符串和单个值表示成 pointer 加 count,Swift 需要把自己的值临时暴露成这些形式。Apple 在这场 session 中建议优先让数组、字符串、withUnsafeBufferPointerwithUnsafeMutablePointer 这类闭包 API 生成临时指针,把 unsafe 代码压到一次函数调用或一个闭包里。

Session 的后半段给出一个更实用的判断:能用隐式转换调用 C API 时,不要自己分配和释放内存;需要显示生命周期时,用 closure-based API 让指针有效期写在代码结构里;只有要直接构造 ArrayString 的底层存储时,才使用新的 unsafeUninitializedCapacity 初始化器,并在返回前完成所有边界检查。

详细内容

safe 操作会给出确定结果

00:52)普通强制解包也可能是严重错误,但 Swift 会把错误变成定义良好的 fatal runtime error。这个差异是理解 Unsafe 前缀的起点。

let value: Int? = nil

print(value!) // Fatal error: Unexpectedly found nil while unwrapping an Optional value

关键点:

  • valuenil,不满足强制解包的前置条件。
  • value! 仍然属于 safe operation,因为它对无效输入有明确行为。
  • 运行时会停止执行并报告错误位置,开发者可以根据崩溃信息修复代码。

01:58unsafelyUnwrapped 省掉了优化构建中的检查。它只有在调用者已经能证明值非空时才适合使用。

let value: String? = "Hello"

print(value.unsafelyUnwrapped) // Hello

关键点:

  • 这段代码能工作,因为 value 确实包含字符串。
  • unsafelyUnwrapped 把非空证明交给调用者。
  • transcript 明确说它属于性能场景,只应放在测量后确认检查成本有影响的代码路径。

02:25)如果前置条件不成立,unsafe 操作可能读取不存在的值。这里的重点不在崩溃形式,而在行为已经没有定义。

let value: String? = nil

print(value.unsafelyUnwrapped) // ?!

关键点:

  • nil 没有可读取的 String 值。
  • 优化构建下这次读取不保证触发同一种错误。
  • 同一段代码在不同运行、不同优化条件下可能表现不同。

手动分配的指针不管理生命周期

07:37UnsafeMutablePointer 能直接分配内存、初始化值、读取 pointee,但它不会在释放后阻止继续访问。

let ptr = UnsafeMutablePointer<Int>.allocate(capacity: 1)
ptr.initialize(to: 42)
print(ptr.pointee) // 42
ptr.deallocate()
ptr.pointee = 23 // UNDEFINED BEHAVIOR

关键点:

  • allocate(capacity:) 给出一段能存放 Int 的动态内存。
  • initialize(to:) 开始这块内存中值的生命周期。
  • deallocate() 释放存储后,ptr 变量本身仍然保存原地址。
  • 继续写 ptr.pointee 会访问已经失效的内存,可能污染其他对象或后来复用该地址的值。

buffer pointer 把起始地址和长度放在一起

12:33)只传一个起始地址时,长度会在代码里重复出现。Swift 提供 buffer pointer 类型,把地址和元素数量作为一个对象处理。

UnsafeBufferPointer<Element>
UnsafeMutableBufferPointer<Element>

UnsafeRawBufferPointer
UnsafeMutableRawBufferPointer

关键点:

  • typed buffer pointer 适合已经知道元素类型的连续内存。
  • raw buffer pointer 适合按字节处理外部数据。
  • debug 构建下,buffer pointer 的下标会做边界检查。
  • 它们仍然不能验证底层内存是否处于正确类型和生命周期,只是减少了漏传长度的错误。

用闭包生成临时指针

13:48)把 Swift 数组传给 C 函数时,优先让数组在闭包中暴露临时 buffer pointer。这样内存仍由数组管理,指针只在闭包内有效。

let values: [CInt] = [0, 2, 4, 6]

values.withUnsafeBufferPointer { buffer in
  print_integers(buffer.baseAddress!, buffer.count)
}

关键点:

  • values 保持普通 Swift 数组语义。
  • withUnsafeBufferPointer 在闭包中提供底层连续存储。
  • buffer.baseAddress! 是给 C 函数用的起始指针。
  • buffer.count 和指针来自同一个 buffer,减少了数量写错的机会。

14:25)这种场景很常见,所以 Swift 还能为 C 函数调用生成等价的临时指针。

let values: [CInt] = [0, 2, 4, 6]

print_integers(values, values.count)

关键点:

  • 数组值可以直接传给期待 unsafe pointer 的 C 参数。
  • 编译器会生成临时 pointer conversion。
  • 指针只在函数调用期间有效,C 函数不能保存它供以后访问。

复杂 C API 可以用隐式转换隔离 unsafe 区域

16:32sysctl 有六个参数,包含输入 buffer、输出 buffer、长度指针和可选的新值 buffer。Swift 可以用数组到指针、inout 到指针的转换,让 unsafe 部分集中在一次调用里。

import Darwin

func cachelineSize() -> Int {
    var query = [CTL_HW, HW_CACHELINE]
    var result: CInt = 0
    var resultSize = MemoryLayout<CInt>.size
    let r = sysctl(&query, CUnsignedInt(query.count), &result, &resultSize, nil, 0)
    precondition(r == 0, "Cannot query cache line size")
    precondition(resultSize == MemoryLayout<CInt>.size)
    return Int(result)
}

print(cachelineSize()) // 64

关键点:

  • query 是 C API 需要的整数标识 buffer。
  • &query 触发数组到 pointer 的临时转换。
  • &result&resultSize 给 C 函数提供可写输出位置。
  • 两个 precondition 把 session 中提到的假设写成运行时检查:调用成功,并且返回字节数等于 CInt 大小。

19:19)在纯 Swift 代码里,closure-based API 更清楚地标出指针有效期。把临时指针塞进 UnsafeMutablePointer 初始化器会产生 dangling pointer,Swift 5.3 会对这种可检测情况发出警告。

var value = 42
withUnsafeMutablePointer(to: &value) { p in
  p.pointee += 1
}
print(value)  // 43

var value2 = 42
let p = UnsafeMutablePointer(&value2) // BROKEN -- dangling pointer!
p.pointee += 1
print(value2)

关键点:

  • 第一段中,p 的有效期被闭包包住。
  • 第二段把临时 pointer 的值保存到 p,初始化器调用结束后底层指针已经失效。
  • 后续访问 p.pointee 是 undefined behavior。
  • transcript 建议在纯 Swift 中偏向闭包形式,因为生命周期更容易读出来。

新初始化器减少临时 buffer

20:02)Swift 标准库新增的 String.init(unsafeUninitializedCapacity:initializingUTF8With:) 允许 C 函数直接写入即将成为字符串存储的 buffer。这样可以省掉手动分配临时内存的步骤。

import Darwin

func kernelVersion() -> String {
    var query = [CTL_KERN, KERN_VERSION]
    var length = 0
    let r = sysctl(&query, 2, nil, &length, nil, 0)
    precondition(r == 0, "Error retrieving kern.version")
    return String(unsafeUninitializedCapacity: length) { buffer in
        var length = buffer.count
        let r = sysctl(&query, 2, buffer.baseAddress, &length, nil, 0)
        precondition(r == 0, "Error retrieving kern.version")
        precondition(length > 0 && length <= buffer.count)
        precondition(buffer[length - 1] == 0)
        return length - 1
    }
}

print(kernelVersion())
// Darwin Kernel Version 19.5.0: Thu Apr 30 18:25:59 PDT 2020; root:xnu-6153.121.1~7/RELEASE_X86_64

关键点:

  • 第一次 sysctlnil 输出 buffer 查询所需长度。
  • String 初始化器提供未初始化的 UTF-8 buffer。
  • 第二次 sysctl 直接把版本字符串复制进这个 buffer。
  • 三个 precondition 检查调用成功、写入长度合法,并确认末尾是 C 字符串的 NUL 字节。
  • 返回 length - 1 会丢弃 NUL 字节,让字符串只包含有效 UTF-8 内容。

核心启发

  • 做什么:写一个安全的 C API 包装层。为什么值得做:session 说明 Swift 可以把数组、字符串和 inout 值临时转成 C 指针。怎么开始:先把每个 C 函数包进一个 Swift 函数,在包装层里调用 withUnsafeBufferPointer 或传入数组值,并用 precondition 检查返回码和输出长度。

  • 做什么:做一个二进制数组读取工具。为什么值得做:buffer pointer 把起始地址和长度放在一起,适合处理文件头、网络包或设备数据。怎么开始:用 Array.withUnsafeBytes 获取 raw buffer,在读取前检查 byte count,再把 offset 和目标类型集中在少数解析函数里。

  • 做什么:审计项目里的 dangling pointer 风险。为什么值得做:Swift 5.3 已经能警告部分临时指针逃逸场景,session 的 broken 示例给了明确搜索目标。怎么开始:搜索 UnsafeMutablePointer(&UnsafePointer(& 和保存到属性的 unsafe pointer,把它们改成 withUnsafePointerwithUnsafeMutablePointer 闭包。

  • 做什么:用新字符串初始化器读取 C 字符串输出。为什么值得做String.init(unsafeUninitializedCapacity:initializingUTF8With:) 可以让 C 函数直接写入字符串存储,避免临时 buffer。怎么开始:先调用 C API 查询长度,再在初始化器闭包中写入 buffer,返回去掉 NUL 结尾后的 UTF-8 字节数。

关联 Session

评论

GitHub Issues · utterances