大多数浏览器和
Developer App 均支持流媒体播放。
-
在 Swift 里安全管理指针
请跟随我们一起深入了解 Swift 中的不安全指针类型。了解每种类型的要求以及正确使用方法。我们将探讨一下指针类型、下拉切换至原始指针以及最终通过绑定内存来完全规避指针类型安全。 本部分为 WWDC20 的“不安全 Swift”的后续内容。为充分利用,你应该熟悉 Swift 和 C 程序设计语言。
资源
相关视频
WWDC20
-
下载
(你好 WWDC 2020) 你好 欢迎来到 WWDC (ANDREW TRICK SWIFT 编译器工程师) 你好 我是 Andy 我会向你讲解 《在 Swift 里安全管理指针》的指南 这次讲座直接建立在同样来自 WWDC 20 的《Unsafe Swift》讲座上 在那次讲座里 我们定义了不安全操作 即在某些输入上有着未定义行为的操作 这一次 我将更深入地讲解 在与以往不同的安全区域外 编程 Swift 的细节 这些细节一般并不是应用编程员 所需要担心的 安全管理指针意味着我们需要 知道它们可能会变得不安全的所有方式 我会将大部分的时间 都用在讲解类型安全上 这是 C 里未定义行为的源 通常大家都对它知之甚少 我会向你们解释 能够给予 Swift 相同低层级功能的 API 以及如何利用它们来避免未定义行为 我们可以将指针安全看作是一系列的层级 每下一层 你就越需要确保你所编写的代码是正确的 所以我建议你尽可能地 在最高安全层级里编写代码 (安全层级) 第一级是安全代码 Swift 的一大目标是为编写代码提供 不需要任何不安全建构的新方法 Swift 有着非常强大的类型系统 这个系统能提供丰富的弹性和性能 Swift 的 API 集合、切片和迭代器 提供了很多 你可能想要从指针那里获取的功能 而在代码安全这方面 完全不使用指针是一个好策略 但是 Swift 的另一重大目标是 通过不安全语言达成高效互用性 要做到这点 Swift 需要提供以不安全 API 形式 所呈现的低层级可表达性 这些将会通过它们的 类型或函数名称前缀 “Unsafe” 来表示 Swift 的 UnsafePointer 让你负责承担 使用指针的某些危险 而无需担心类型安全 如果你需要将 某些原始内存用作一连串的字节 Swift 提供了 UnsafeRawPointer 选项 利用原始内存加载和存储值 使你有责任了解类型的布局 在最深的层级里 Swift 提供了一些 API 以将内存绑定到类型上 只有当你使用这些最低层级的 API 时 你需要承担管理指针类型安全的所有责任 而不是 Swift 让我来解释一下何谓安全层级吧 安全代码不一定就是正确的代码 但是它的运作是可预料的 在大多数的情况下 如果一个编程错误 可能会引发不可预料的行为时 编译器将会捕获它 至于那些无法在编译时被捕获的错误 运行检查也能通过诊断 使程序立即崩溃 它就不会再继续通过不正确的假设了
所以安全代码其实就和强制执行错误有关 如果你不使用任何被标记成不安全的 Swift 类型或者 API 而且你用心地管理线程安全的话 你就会知道你已经 全面执行了可预料的行为 在不安全的 Swift 代码里 可预料的行为并没有被全面执行 所以你就需要肩负额外的职责 测试依然可以带来有用的诊断 但是诊断的层级 取决于你所选择的安全层级 不安全的标准库 API 在调试版本里具有断言功能 这能捕获某种特定的无效输入 添加你自己的预置条件以确认不安全假设 也是很好的做法 你可以通过进行多次运行检查 启用诸如 Address Sanitizer 等的 Sanitizer 来反复测试 Sanitizer 诊断是绝佳的省时利器 它能够精准定位漏洞 但是不会捕获所有的未定义行为 当你没有在测试中发现错误时 它们可能会引发出乎意料的运行行为 那可能会是一个难以调试的崩溃 其发生的原因远非通过 研究问题的根源所在就能解决 或者可能比崩溃还糟糕 你的程序可能会做错事 甚至破坏用户数据 崩溃固然是不好的体验 但是破坏或弄丢数据就更糟糕了 你越是冒险进入更低层级的不安全区域 你越是难以发现这些错误 而且这些症状也会越来越难以理解 这些症状可能完全不会显现 一直等到漏洞出现为止你才发现 (指针安全) 来看看指针 来了解代码能够如何变得不安全吧 Swift 是为了不使用指针 就能进行编程而设计的 而如果能了解为什么它们并不安全 我们就能更明白 为什么避免它们会是一个好策略 可是如果你必须使用低层级 API 直接访问内存的话 了解你要如何 自己管理安全的不同方面也很实用 你可能需要指向存储容量来获取变量 指向数组的元素 或者指向你直接分配的内存 在你指向该对象前 指针需要稳定的内存位置 你指向的稳定存储容量 只有有限的生命周期 原因就在于 它要么超出范围 要么就是因为你直接解除分配了内存 但是你的指针值也有自己的生命周期 当指针的生命周期 超过存储容量的生命周期时 任何对它的访问尝试都会是未定义的 这就是指针变得不安全的首要原因 但是这并非唯一一个 对象可以由一连串的元素所组成 通过添加偏移量至指针 它就可以 移动到不同的内存地址 这是去到不同元素地址的有效方法 可如果添加或减去太多的偏移量的话 这会指向不属于相同对象的内存 访问一个超越其对象边界的 指针是未定义的 在这次的讨论中 我们会专注于另一个 易于被忽视的安全方面 指针有属于它们自己的类型 它们和内存里的值类型并不相同 我们要如何确保这些类型是一致的 如果它们不一致 又会发生什么事? 当我们请求一个指向 Int16 类型存储容量的指针时 我们会得回一个指向 Int16 的指针 目前为止一切顺利 我们可以看到 要在 Swift 里 使指针指向错误类型是挺困难的 那么我们用不同类型覆盖相同内存的话 现在这是 Int32 这个时候 我们会有一个 指向正确 Int32 类型的指针 但是我们的 Int16 指针可能还逗留着 访问旧的 Int16 类型指针 就会是未定义行为 因为现在指针类型和内存内类型不一致了 你可能会疑惑 未定义的行为怎么可能 比程序崩溃还要糟糕 而且指针类型怎么可能导致这点呢? 要了解这点 我们先来看看 一些非常不安全的代码吧
我不希望任何人编写出这样的代码 但是你想象不到一个从 C 接入 并且还能调用部分旧 C 代码的 Swift 代码能做出什么 能够做出惊人之举的代码 确实会看起来很惊人 但是我们还不需要彻底搞懂 这些低层级的类型来研究问题 想象我们有个拼贴结构体 它包含一个指向内存 部分图像数据的独立指针 以及指向图像数量的另一属性 这个类型可能是从 C 导入进来的
我们也有一个 addImages 函数 它可以将图像数据编写进内存里 并增加一个图像数量
当我们调用 addImages 时 我们想要它更新拼贴结构体的图像数量 但是却出现了结构体的 imageCount 类型即 Int 与指向 UInt 32 的函数参数指针 不匹配的情况
而安全的做法会是 创建正确类型的新数量变量 并使用 Swift 的整数转换 可在这里 这复杂的代码行创建了一个 直接指向我们结构体的指针 之后 这个代码需要再次读取图像数量 并将它交给 saveImages 问题就出在运行时 这个数量很可能是零 这意味着 这个程序 已经悄无声息地丢失了所有图像
将 Int 类型配给数量属性 以及 UInt 32 类型配给指针 这个举动示意了编译器一件事 即那些值 存在于不同的内存对象里 编译器收不到 Int 对象的任何更新 所以它只能重复使用零这个初始化值 在实践中 编译器可以允许错误 因此这样一个示例应该不会出什么大事 但是我们无法预料将会发生的事 对于编译器来说 类型信息是假设所基于的一个事实 一旦编译器做出了不好的假设 这可以渗透编译器的传输管道 并以突发的方式显示 因此两种版本的编译器 可以导致不同的程序行为 指针类型的漏洞可以导致 你的程序出现异常行为 甚至会通过比崩溃还糟糕的形式出现 但使它们变得更可怕的是 它们很少会被注意到 表面上 程序可能会看起来很正常 暗地里 一个漏洞却在里面徘徊数年 根本没人注意到 之后 可能当有人对源 做出安全且看似无害的更改时 问题才得以揭发 或者问题也有可能在一次 例行的编译器升级时而被注意到 实际上根本没人改过代码 但是你的程序却开始出现异常 指针类型安全的挑战 早在 Swift 诞生之前就存在 知道如何在 C 里面正确地使用指针类型 需要非常深厚的语言规格知识
你可以在以下部分找到相关讨论 分别是 “strict aliasing” 和 “type punning” 幸运的是 你不需要搞明白这些规则 就能安全地使用 Swift 指针 虽说将指针从 Swift 传递进 C 是挺常见的 但是 Swift 指针至少需要和 C 一样严格 以便安全地进行互用 Swift 的 UnsafePointer 给你提供了 C 指针的大多数低层级功能 作为交换 你需要管理对象的生命周期和边界 “Unsafe Swift” 讲座 解释过如何做到这点 但是你不需要负责类型安全 UnsafePointer 的泛型类型参数 会在编译时被强制执行 这使之成为一个 typesafe API 来看看 Swift 对指针类型安全的规则 以了解它的操作原理吧 UnsafePointer 的类型参数表示着 期望保存在内存里的值类型 我们将这称为类型指针 在 Swift 里 针对类型指针的 规则是严格而简单的 从概念上来说 内存状态包括 和内存位置绑定的类型 该内存位置只能保存该类型的值 作为一个 typesafe API Unsafe- Pointer 只从内存里读取该类型的值 而 UnsafeMutablePointer 只读取或编写该类型的值
觉得指针类型并不重要 只要字节在内存里布局正确就好 拥有这种想法是正常的 在 C 里 把指针投射成不同类型 两个指针持续地 指向同一个内存是很常见的 可是这在 C 里到底合不合法 则取决于几种特别的个例
在 Swift 里 访问一个 类型参数不和其内存位置 绑定类型所匹配的指针 一直以来都是一个未定义行为 为避免这个问题 Swift 不允许在熟悉的 C 风格里投射指针 通过这样的方法 在编译时 Swift 类型系统会强制执行指针类型 没有必要存储额外的运行信息 内存里的额外类型信息 或执行额外的运行检查 来看看内存是如何被绑定至一个类型 并且类型指针从何而来吧 如果你声明一个 Int 类型的变量 并请求一个指向变量存储容量的指针 你会得回一个 和变量声明一致的 pointertoInt 数组存储容量和数组元素类型绑定 所以显然的 请求一个进入数组存储容量的指针 会给你一个指向数组元素类型的指针 你也可以通过调用 UnsafeMutablePointer 上的 静态分配方法来直接分配内存 分配将内存绑定至它的类型参数 并将类型指针返回到新内存里 这和变量以及数组的指针不同 如状态图表所示 即便内存还未保存任何初始化值 内存已经被绑定至一个类型了 你可以使用分配所给予的类型指针 以便只针对正确的类型 进行初始化内存 在初始化状态下 内存可以被重新分配 分配会暗地里取消初始先前的内存内值 并将内存重新初始成相同类型的新值 你可以使用同一类型指针取消初始内存 在那个时候 内存依然会绑定至相同类型 但它却可以安全地解除分配 通过变量和数组存储容量 这些步骤都会由 Swift 自动处理 通过直接分配 你将负责 管理内存的初始化状态 但 Swift 依然会确保类型安全 由于类型指针遵循简单却严格的规则 基本上你不会有两个指向同一内存位置 却有着不同类型的活跃指针 不过来看看复合类型的情况吧 在这个示例中 我们拥有 一个包含 MyStruct 值的内存块 我们可以获取一个指向结构体外层的指针 或者指向其属性的指针 而这两种指针同时都是有效的 我们可以访问任意其中一个 而无需更改内存绑定的类型 这依然遵照了相同的指针安全基本规则 当内存绑定至一个复合类型时 它也会有效地和该类型的成员绑定 因为它们都存在于内存的布局里
Swift 的类型指针能让你直接访问内存 但是仅限于类型安全的范围里 你不能拥有两个指向同一内存却拥有 不同类型的类型指针 因此 如果你的目标是 将内存字节重新诠释成不同类型 你就需要使用一个低层级的 API
UnsafeRawPointer 让你 能够参考一连串的字节 而无需指定它们可能代表的值类型 你将控制内存布局 有了原始指针后 当你从内存加载字节时 你可以将字节诠释成类型值 想象一下使用类型指针 将内存块 初始化成 Int64 将类型指针投射成原始指针这件事 一直都是可行的 在原始指针上的操作 只能看到内存里的一连串字节 内存的绑定类型是无关紧要的 你可以请求该原始指针加载任意类型 它会通过读取所需的字节数 以及将它们汇编成指定类型而做到这点 打个比方 当我们将加载调用成 UInt 32 四个字节会从当前的地址加载 并生成一个 UInt 32 值 你甚至可以加载一个较小的类型 只要考虑到目标平台的字节序即可 你也可以使用原始指针 将值的字节编写进内存里 存储字节和加载字节是不对等的 因为前者会调整内存内值 与使用类型指针的分配不同 存储原始字节并不会 取消初始内存内的先前值 所以这就变成了你的责任 你要确保内存并不含有任何对象引用 在这个示例中 在原始指针上调用 storeBytes 将会从 UInt 32 值里提取四个字节 并将它们编写进一个 内存内 Int64 值的上四个字节中 当字节被编写进内存后 它们就会被重新诠释成内存的绑定类型 因此 你依然可以使用已经指向 内存内值的类型指针来访问它 我们不能将一个原始指针 投射回去成为类型指针 因为这么做会导致 其和内存的绑定类型不一致 这种情况下 我们得到的结果会是 一个指向 Int64 的指针 和另一个指向 UInt 32 的指针 以重叠内存 从类型指针进行投射 并不是获取原始指针的唯一途径 withUnsafeBytes API 会在其闭包期间 公开作为原始缓冲区的变量存储容量 UnsafeRawBufferPointer 是字节的集合 就像 UnsafeBufferPointer 是类型值的集合一样 在这里 缓冲区计数是 变量类型字节里的大小 集合索引是字节偏移量 而读取编入索引的元素 会给你该字节的 UInt8 值 你也可以调整变量的原始存储容量 withUnsafeMutableBytes 会给你可变字节的集合 这样你就可以将 UInt8 值 存储在特定的字节偏移量里 就像数组有一个 withUnsafeBufferPointer 方法一样 它也有一个 withUnsafeBytes 方法 这个方法能将原始存储容量 公开给数组元素 缓冲区大小是被元素步幅 倍增过的数组数量 某些字节可以因元素对齐方式而进行填充 基础数据类型 通常都会被用来传送字节集合 数据也有 withUnsafeBytes 方法 这个方法会在其闭包期间 公开基础的原始指针 在这里 我们使用这个方法 在选定的字节偏移量里 读取了特定的 元素类型 即 UInt32 你可以直接分配原始内存 只需通过调用 UnsafeMutable- RawPointer 上的静态分配方式即可 在这里 你会肩负起 用字节计算内存大小和对齐方式的责任 经过原始分配后 内存状态既不初始化 也不绑定至类型 要想通过原始指针初始化内存 你需要指定该内存将会保存的值类型 初始化会将内存绑定至该类型 并返回类型指针 过渡到初始化内存的过程 只在一个方向上进行 要想取消初始内存 你需要知道内存内值的类型 所以你不能使用原始指针取消初始 你可以使用经初始化返回的类型指针 来进行取消初始 我们已经看过了类型指针的内存状态图表 只要内存是未初始化状态 你就可以使用原始指针 来解除分配内存 对解除分配来说 内存是否已绑定至类型并不重要 使用类型指针进行内存分配 会更为安全和方便 因此这理应是首选 以下的示例会解释 你可能想要分配原始存储容量的原因 比方说 我们想在 同一个相接的内存块上 存储不同长度的无关联类型 经过计算字节和对齐方式的总容量后 我们调用分配的原始版本 这会给我们提供一个 去到字节相接块的原始指针 现在我们可以将内存的 一部分初始化成标头 这会给我们提供一个 指向标头类型的指针
添加了标头的字节偏移量后 我们可以将剩余的字节初始化成整数 这给我们提供了另一个类型指针 它会指向只保存着整数的内存区域 这个存储分配技术对实施诸如 Set 和代码字典等标准库类型来说 是非常有用的 但它通常不是你想要接触的东西 大致上来说 原始指针是一款有力的工具 它在实施高性能数据结构方面非常有效 但是我们并不应该过度地公开它们 当你对字节偏移量进行微调时需格外留心 通常当你拥有外部生成的字节缓冲区 并且想将这些字节解码成 Swift 类型时 你就会想要使用原始指针 通过使用 UnsafeRawBuffer- Pointer 的加载 API 我们会先读取一个描述符 以此确定后续数据的大小 和类型 我们会在字节偏移量增加的情况下 对加载 API 进行更多调用 每一次都要指定 我们想要从流里解码的类型 原始指针保留了类型安全的一个重要层级 你需要在使用它们时负责内存布局 但它们不会影响 何时能合法使用类型指针 因此使用原始指针并不会让它在使用 相同内存和类型指针时变得危险 在最深的层级里 Swift 提供了 公开内存绑定类型的 API 当你使用这些 API 时 你将全面负责指针类型的安全 在开始使用这些层级前 先看看你是否能使用高层级的 API 你会知道你是否在 回避指针类型的强制执行 因为你需要明确地调用一个 表示内存绑定类型的 API
回避类型安全的危险就在于 你会很容易地就在使用类型指针的代码里 引入未定义行为
你依然只需遵守一个规则 即类型指针的访问需和内存绑定类型一致 这是一条简单却不容易遵守的规则 因为代码的各项部分 都需要一致的内存类型 而编译器却无法为你提供指导 来看看你为什么会使用如此危险的 API 并注意看看为什么 以下每一个用途都是安全的 在极少数的情况下 代码很可能不会保留类型指针
如果说我们只有原始指针 可是我们却非常肯定 内存绑定至什么类型呢? 我们应该要能够告诉 Swift 我们很清醒 并取回我们的类型指针 在这个示例中 我们有一个保留原始内存的容器 但同时我们也有一个变量 即 pointsToInt 这会让我们知道内存是否只能保存整数值 在我们的原始指针上调用 assumingMemoryBound to 并给予它作为参数的 Int 类型 能够帮助我们取回整数的类型指针 我们知道这个方法很安全 因为当我们分配内存时 内存是绑定至 Int 类型的 只有当你确定内存 已经被绑定至你想要的类型时 你才可以调用 assumingMemoryBound to 这不会在运行时被检查 这只是请求编译器做出假设的一个方法 所以你需要为该假设的正确性负责 以下是另一个我们需要 assumingMemoryBound to 的例子 这一次 我们要调用 C API pthread_create 首先 我们要利用定制的 ThreadContext 类型来初始化一个情境指针 当我们调用 pthread_create 时 我们向它传递了我们的情境指针 但是当 pthread_create 在新线程里 回调至我们的启动例程时 它只给了我们一个原始指针 针对回调 C 函数声明了一个开始参数 这个函数将作为 UnsafeMutable- RawPointer 而被导入 有时 这样的事会在 C 发生 而我们没有办法 把它变成一般的 typesafe 在这种情况下 我们知道我们可以 调用 assumingMemoryBound to 来恢复一个类型情境指针 这样做是安全的 当我们在上面几行代码分配它时 我们就已经将这个类型与内存绑定 在过去几个示例中 原本的指针类型被清除了 有时侯 我们有一个类型指针 但是它却位于类型组合的错误层级 在这里 我们有一个 可以把指针带去整数的函数 如果我们有 Int 的元组 我们就能够将指向元组元素的 指针传递进该函数里 要带一个指针进入元组的存储容量 我们需要调用 withUnsafePointer 但这样做会给回我们 一个指向元组类型的指针 而这个指针和我们的函数类型并不兼容 内存一次只能绑定至一个类型 但由于元组类型是复合类型 因此将内存绑定至元组类型 意味着内存也将绑定至元素类型 我们知道 使用指向整数的指针 进行元组存储属于 typesafe 但是我们需要使用 typeunsafe API 来取得这种指针类型 首先 我们要构建一个原始指针 有意地清除我们元组指针的类型 然后我们就能像之前一样调用 assumingMemoryBound to 来创建一个指向整数的指针 把指针降低至一个成员类型 对于这样的事来说 编程员需要拥有对复合类型布局的知识 Swift 的实施能确保 拥有相同元素的元组 按照标准样式进行布局 一个值接着另一个 并完全依照元素类型的步幅 来看看结构体属性是如何应用这一点的吧 同样的 我们拥有一个函数 它是指向整数的指针 但是这一次 我们所拥有的是 带有整数属性的结构体 而非元组 withUnsafePointer 给我们一个 指向结构体外层的类型指针 通过使用 MemoryLayout API 我们可以计算该值属性的字节偏移量 通过将结构体指针投射成原始指针 并添加该字节偏移量 我们就能取得指向值属性的原始指针 属性的内存永远都会 和该属性所声明的类型绑定 所以要调用 assumingMemoryBound to 来获取指向整数的指针是安全的 大致上来说 我们无法保障结构体属性的布局 所以当你取得指向结构体属性的指针时 你只能将它指向该属性的单一值 指向结构体属性是很常见的 因此 幸运的是 我们有一个可以避免 不安全 API 的简易方法 当你将属性传递成 in-out 参数时 编译器会暗地里 将它转换成该函数参数所声明的 不安全指针类型 assumingMemoryBound to 会让编译器 对内存的绑定类型 进行一个不经检查的假设 其实 bindMemory API 可以让你 更改内存的绑定类型 如果内存位置并没有和任何一个类型绑定 它就会让内存和该类型进行首次绑定 如果内存已经绑定至一个类型 那它就会重新绑定该类型 这样一来 之前内存里的所有值 都会采用新类型 比方说 我们分配一个内存块 以保存两个 UInt16 值 然后我们再请求指向该内存块的原始指针 通过在该原始指针上调用 bindMemory 我们会在原地 将该类型更改成单一 Int32 值 这只是一个按位转换 所以你不会遇到在进行 普通类型转换时所遇到的安全检查 事实上 没有任何事需要在运行时发生 bindMemory 是对编译器的一个声明 声明说类型已经在该内存位置上改变了 bindMemory 会返回一个 Int32 指针 我们现在应该要用它来访问内存 访问旧 UInt16 指针是未定义的 在程序的任何一个地方里 内存位置都只会绑定至单一类型 更改内存绑定类型 并不会从物理方面上调整内存 但你应该要将这件事 想象成更改内存状态的 全局属性 出于两个原因 这并不是 typesafe 第一 它在原地重新诠释了原始字节 所以就和你使用原始指针一样 你从 Swift 那里承担起了 对内存里类型布局的责任 但是重新绑定内存比使用原始指针更危险 因为前者会使现有的类型指针都失效 它们的指针地址依然是有效的 但是当内存绑定至错误类型时 访问它们就会是未定义的 当你将指针指入存储容量 要寻找声明了类型的对象时 例如变量、数组或其他集合类型 使用该指针来重新绑定内存 会使该对象本身失效 bindMemory API 确实是 Swift 里的一个低层级语言基元 它并不适合常规代码 不过在现实中 你确实会经历到 需要重新绑定内存类型的情况 这通常会发生在你拥有多个外部 API 和不同类型的数据 并且不想来回地复制数据时 这往往会和 C API 一起出现 在那里 指针类型安全 并没有被仔细地考虑到 在这里 我们拥有一个 使用 UInt8 指针的函数 以及指入 Int8 类型内存的指针 Swift 会强制执行指针类型 所以它不允许我们将指针传进该函数里 我们可以分配具有正确类型的新内存块 并复制该数据 针对指针类型来说 这种做法很安全 但相对较慢 由于我们只需要在调用期间重新诠释内存 我们可以使用 Swift 的 withMemoryRebound to API 和 UnsafePointer 一样 withMemoryRebound to 也会给你一个 在其闭包范围内绝对有效的指针 我们知道在闭包范围内 重新绑定内存是安全的 因为我们的 Int8 指针 不会在闭包范围内的任何地方被使用 当闭包返回后 withMemoryRebound to 会将内存重新绑定回原本的 Int8 类型 这使其能够不随类型指针访问周围代码 我们只需在闭包内进行代码推论 就能证明它是可以被安全使用的 对周围代码来说 withMemoryRebound to API 使绑定内存变得安全 但它有着一些非常严格的限制 因为这些限制 你可能依然 需要直接调用 bindMemory 当你这么做时 请使用相同的方法来推论安全 只有当你处于可控范围内 并知道代码不会使用旧指针 来访问相同内存时 你才可以使用 bindMemory 来获取指针 当超出范围后 你要确保 在其他代码使用先前所取得的 指针来访问相同内存前 你已经将内存重新绑定至原本类型 来回顾一下 memorybinding API 吧 assumingMemoryBound to 是将原始指针 恢复成类型指针的方法 这很危险 因为你需要知道 内存是否已经被绑定至该类型 bindMemory 是一个低层级基元 它可以更改内存的绑定类型状态 当类型指针在代码里的 其他位置上被访问时 它会变得更加危险 因为它可能会引起未定义行为 相对来说 withMemoryRebound to 在必要时暂时性地绑定内存显得安全多了 它在调用不认同相同类型的 C API 并且不需要复制基础内存方面也非常实用
bindMemory API 最常见的错误用例是 只从内存里读取一个不同类型 这样的代码会调用 bindMemory 来获取它想要读取某个类型的指针 该类型指针只用来读取一个值 但是在创建该指针的过程中 我们已经更改了内存状态 甚至可能使其他指针失效了 当你只想要重新诠释一个类型时 UnsafeRawPointer 的加载 API 是 避免指针类型隐患的不二之选 你只需要承担内存布局的责任 这个方法什么时候都能成功 因此 如果你有指向内存的类型指针 你可以将它投射至原始指针 而如果你有一个变量、数组或数据对象 withUnsafeBytes 方法 能让你直接访问原始缓冲区 比方说 你想将内存区域的视图 换成具有特定元素类型的元素序列 但是基础存储容量已经被公开为原始指针 并且代码的不同部分 可能会将其视为不同的类型 其实你可以轻易地 在该原始指针周围创建一个包装 来保留你的元素类型 就把它称为 BufferView 吧 我们会在名称前添加 Unsafe 前缀 这样我们就不需要自动管理内存 而且我们还会限制调试版本的边界检查 要在原始缓冲区上创建一个 BufferView 我们会根据元素步幅计算出该缓冲区 可容纳的元素数量 当然我们也会添加预置条件 以确认该缓冲区具有正确的 字节和对齐方式数量 现在 要想读取编入索引的元素 我们只需计算其字节偏移量 并请求原始缓冲区 加载我们的元素类型即可 由于从原始内存进行加载 对指针类型而言是安全的 我们就无需担心 其他代码要怎样浏览这个内存 BufferView 让我们 在保留元素类型的同时 能够安全地重新诠释字节序列 这样就不必用到类型指针了 结束之前 让我们回顾一下 处理指针类型的策略吧 最好的策略就是尽可能地避免使用指针
在极少数情况下 你需要将相同的 内存位置重新诠释成不同的类型 你就必须要谨慎选择 由于类型指针必须和内存绑定类型匹配 所以你最好不要使用它们来重新诠释类型 这可能会很难记住 因为 C 代码通常都会 通过投射指针类型来处理这件事
可是 Swift 却是通过提供 基于原始指针的 API 来处理的
即便是在纯 Swift 代码中 这些都是非常有用的 API 打个比方 你可能需要从字节流里解码值 或者你可能需要实施 诸如 Set 或代码字典等的容器 以便在相接的内存里保存不同类型 本次分享和 Swift 里的指针类型安全 有关 我希望你受益匪浅 并从中了解到 它其实不如外表看起来那样神秘莫测 感谢收看
-
-
5:44 - Images: undefined behavior can lead to data loss
struct Image { // elided... } // Undefined behavior can lead to data loss… struct Collage { var imageData: UnsafeMutablePointer<Image>? var imageCount: Int = 0 } // C-style API expects a pointer-to-Int 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
-
10:06 - Direct memory 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() }
-
14:24 - Using a raw pointer to read from Foundation Data
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))
-
14:37 - Raw allocation
func rawAllocate<T>(t: T, numValues: Int) -> UnsafeMutablePointer<T> { let rawPtr = UnsafeMutableRawPointer.allocate( byteCount: MemoryLayout<T>.stride * numValues, alignment: MemoryLayout<T>.alignment) let tPtr = rawPtr.initializeMemory(as: T.self, repeating: t, count: numValues) // Must use the typed pointer ‘tPtr’ to deinitialize. return tPtr }
-
15:43 - Contiguous allocation
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) }
-
18:03 - Using assumingMemoryBound(to:) to recover a typed pointer
func takesIntPointer(_: UnsafePointer<Int>) { /* elided */ } struct RawContainer { var rawPtr: UnsafeRawPointer var pointsToInt: Bool } func testContainer(numValues: Int) { let intPtr = UnsafeMutablePointer<Int>.allocate(capacity: numValues) let rc = RawContainer(rawPtr: intPtr, pointsToInt: true) // ... if rc.pointsToInt { takesIntPointer(rc.rawPtr.assumingMemoryBound(to: Int.self)) } }
-
18:40 - Calling pthread_create
// Use assumingMemoryBound to recover a pointer type from a (void *) C callback. /* func pthread_create(_ thread: UnsafeMutablePointer<pthread_t?>!, _ attr: UnsafePointer<pthread_attr_t>?, _ start_routine: (UnsafeMutableRawPointer) -> UnsafeMutableRawPointer?, _ arg: UnsafeMutableRawPointer?) -> Int32 */ import Darwin struct ThreadContext { /* elided */ } func testPthreadCreate() { let contextPtr = UnsafeMutablePointer<ThreadContext>.allocate(capacity: 1) contextPtr.initialize(to: ThreadContext()) var pthread: pthread_t? let result = pthread_create( &pthread, nil, { (ptr: UnsafeMutableRawPointer) in let contextPtr = ptr.assumingMemoryBound(to: ThreadContext.self) // ... The rest of the thread start routine return nil }, contextPtr) }
-
19:26 - Pointing to tuple elements
func takesIntPointer(_: UnsafePointer<Int>) { /* elided */ } func testPointingToTuple() { let tuple = (0, 1, 2) withUnsafePointer(to: tuple) { (tuplePtr: UnsafePointer<(Int, Int, Int)>) in takesIntPointer(UnsafeRawPointer(tuplePtr).assumingMemoryBound(to: Int.self)) } }
-
20:26 - Pointing to struct properties
func takesIntPointer(_: UnsafePointer<Int>) { /* elided */ } struct MyStruct { var status: Bool var value: Int } func testPointingToStructProperty() { let myStruct = MyStruct(status: true, value: 0) withUnsafePointer(to: myStruct) { (ptr: UnsafePointer<MyStruct>) in let rawValuePtr = (UnsafeRawPointer(ptr) + MemoryLayout<MyStruct>.offset(of: \MyStruct.value)!) takesIntPointer(rawValuePtr.assumingMemoryBound(to: Int.self)) } }
-
21:17 - bindMemory(to:capacity:) invalidates pointers
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() }
-
23:13 - withMemoryRebound(to:capacity:) API
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 }
-
25:49 - BufferView: Layering types on top of raw memory
struct UnsafeBufferView<Element>: RandomAccessCollection { let rawBytes: UnsafeRawBufferPointer let count: Int init(reinterpret rawBytes: UnsafeRawBufferPointer, as: Element.Type) { self.rawBytes = rawBytes self.count = rawBytes.count / MemoryLayout<Element>.stride precondition(self.count * MemoryLayout<Element>.stride == rawBytes.count) precondition(Int(bitPattern: rawBytes.baseAddress).isMultiple(of: MemoryLayout<Element>.alignment)) } var startIndex: Int { 0 } var endIndex: Int { count } subscript(index: Int) -> Element { rawBytes.load(fromByteOffset: index * MemoryLayout<Element>.stride, as: Element.self) } } func testBufferView() { let array = [0,1,2,3] array.withUnsafeBytes { let view = UnsafeBufferView(reinterpret: $0, as: UInt.self) for val in view { print(val) } } }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。