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 中建议优先让数组、字符串、withUnsafeBufferPointer、withUnsafeMutablePointer 这类闭包 API 生成临时指针,把 unsafe 代码压到一次函数调用或一个闭包里。
Session 的后半段给出一个更实用的判断:能用隐式转换调用 C API 时,不要自己分配和释放内存;需要显示生命周期时,用 closure-based API 让指针有效期写在代码结构里;只有要直接构造 Array 或 String 的底层存储时,才使用新的 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
关键点:
value是nil,不满足强制解包的前置条件。value!仍然属于 safe operation,因为它对无效输入有明确行为。- 运行时会停止执行并报告错误位置,开发者可以根据崩溃信息修复代码。
(01:58)unsafelyUnwrapped 省掉了优化构建中的检查。它只有在调用者已经能证明值非空时才适合使用。
let value: String? = "Hello"
print(value.unsafelyUnwrapped) // Hello
关键点:
- 这段代码能工作,因为
value确实包含字符串。 unsafelyUnwrapped把非空证明交给调用者。- transcript 明确说它属于性能场景,只应放在测量后确认检查成本有影响的代码路径。
(02:25)如果前置条件不成立,unsafe 操作可能读取不存在的值。这里的重点不在崩溃形式,而在行为已经没有定义。
let value: String? = nil
print(value.unsafelyUnwrapped) // ?!
关键点:
nil没有可读取的String值。- 优化构建下这次读取不保证触发同一种错误。
- 同一段代码在不同运行、不同优化条件下可能表现不同。
手动分配的指针不管理生命周期
(07:37)UnsafeMutablePointer 能直接分配内存、初始化值、读取 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:32)sysctl 有六个参数,包含输入 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
关键点:
- 第一次
sysctl用nil输出 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,把它们改成withUnsafePointer或withUnsafeMutablePointer闭包。 -
做什么:用新字符串初始化器读取 C 字符串输出。为什么值得做:
String.init(unsafeUninitializedCapacity:initializingUTF8With:)可以让 C 函数直接写入字符串存储,避免临时 buffer。怎么开始:先调用 C API 查询长度,再在初始化器闭包中写入 buffer,返回去掉 NUL 结尾后的 UTF-8 字节数。
关联 Session
- Safely manage pointers in Swift — 继续深入 typed pointer、raw pointer 和 memory binding 的边界。
- What’s new in Swift — 汇总 Swift 5.3 的运行时性能、语言功能和标准库更新。
- Refine Objective-C frameworks for Swift — 处理 Objective-C 互操作前,先用头文件注解改善 Swift API 表面。
- Advancements in the Objective-C runtime — 从低层布局和 tagged pointers 理解 runtime 变化为何会影响 unsafe 假设。
评论
GitHub Issues · utterances