Apple Swift 迁移 TrueType 字体提示解释器
速览
Apple 正在将其 Swift 代码库中的 TrueType 字体提示解释器进行迁移。这一举措旨在提升字体渲染的效率与准确性,是 Swift 语言在系统底层图形处理领域应用深化的体现。
AI 深度解读
Swift at Apple: Migrating the TrueType Hinting Interpreter
背景
TrueType 是一种广泛使用的矢量字体标准,用于在网页、PDF、操作系统和应用程序中渲染文本。Helvetica、Garamond 和 Monaco 等我们熟悉的字体均基于 TrueType 轮廓构建。该格式规范包含一个“提示解释器”(hinting interpreter),旨在帮助轮廓在低分辨率显示器上忠实地进行光栅化。
虽然现代高分辨率显示器使得仅凭轮廓即可呈现精美的排版,但许多仍需通过提示才能清晰可读的 TrueType 字体仍在广泛使用,Apple 平台也继续支持它们。
字体解析器需要处理来自不受信任来源的数据,这使得 TrueType 提示解释器成为一个安全关键型的攻击面。为了增强 Apple 平台上的格式韧性,Apple 将提示解释器从 C 语言重写为内存安全的 Swift,并计划集成到 2025 年秋季发布的版本中。除了内存安全性外,性能也得到了提升:平均而言,新的 Swift 解释器比被替换的 C 解释器快 13%。
此外,Apple 还发布了 Swift TrueType 提示解释器的源代码,希望分享这一经验能帮助其他从事类似 Swift 工作的开发者。
TrueType 由 Apple 在 20 世纪 80 年代末开发,并于 1991 年随 System 7 发布。在当时,这是一个重大突破:它赋予了字体开发者对字形显示方式的巨大控制权,拥有先进的网格拟合算法和围绕专用字节码解释器构建的复杂提示引擎。考虑到当时的计算机性能远不如今天,TrueType 必须在性能上进行极致的优化。
随后,互联网革命改变了字体的使用方式。TrueType 于 1994 年成为可嵌入 PDF 文件的格式,2008 年成为可嵌入网页的格式,至今依然重要。然而,这些新用例带来了额外的风险:TrueType 现在可能暴露在互联网上任何来源的不受信任字体之下。
TrueType 字体可能包含由提示引擎通过字节码解释器运行的程序。该解释器涉及输入驱动的控制流、复杂的数据结构以及细致的内存管理——这正是难以做到完美且内存错误更容易被利用的代码类型。这种固有的高复杂性也使得正确性变得尤为重要。
核心内容
重写为 Swift 的决策与约束
重写工作需要一种内存安全的语言,能够集成到现有代码库中,并提供与被替换实现相当的性能水平。Swift 是显而易见的选择。
二进制兼容性是该项目的关键成功因素:现有程序必须继续像以前一样运行,实际上无法察觉新实现的存在。这意味着不仅要保持接口兼容,还要实现像素级一致的字形渲染,相对于 C 实现而言。提示会极大地改变字形在屏幕上的外观,因此解释器行为的微小变化可能导致用户可见的重大变化。对于本项目,我们将“正确性”定义为与 C 实现输出的精确兼容。
验证正确性
为了确保正确性,团队开发了两套测试套件:
- 单元测试套件:可以针对两种实现,为两者提供详尽(99.7%)的代码覆盖率。该套件包含在 Swift 解释器的开源发布中。
- 模糊测试(Fuzzing):为了代表真实世界的工作负载,团队使用模糊测试器将 1000 万个 PDF 文件的语料库最小化至 4,200 个,且没有任何代码覆盖率的损失。最小化语料库中的文档嵌入了 25,572 个字体,总计 2700 万个字形。团队使用四种不同的变换对这些字形进行渲染,并将生成的位图与参考解释器进行比较。这增强了团队对新解释器兼容性的信心。
在项目结束时,团队编写的测试代码行数几乎是 Swift 解释器本身代码行数的四倍。
实现高性能
在新实现通过所有测试后,团队转而关注性能。他们使用 PDF 渲染时间进行高层级的性能评估,然后通过基准测试指导改进,这些基准测试渲染了三种不同字体中的所有字形。改进主要归结为以下四个方面:
1. 最小化运行时开销
Swift 使用自动引用计数(ARC)来管理共享引用类型的生命周期,并使用运行时排他性检查来防止对数据结构的重叠访问。这些开销源通常因别名(aliasing)而加剧,而别名在解释器规范的设计中是不可避免的。
可以通过放弃可复制性的便利性来消除这些开销源,在整个架构中采用 ~Copyable 值类型(参见 struct 而非 class),并将引用类型保留给高层抽象。Swift 6.2 引入的 Span(向后兼容至 macOS 10.14.4 和 iOS 12.2)使我们能够高效地操作这些类型的序列。
2. 数据移动与跨语言边界
有时我们需要在跨越语言边界时改变结构化数据的“形状”,以更好地匹配另一侧的习惯用法。在 Swift 中,字形轮廓表示为点的序列,每个点携带一个标志,指示其是否“在曲线上”,以及每个轴上是否已被“触碰”的标志,还有三对坐标:原始坐标(在字体的基本单位中)、缩放坐标(到所需的点大小)和提示坐标(解释器程序的输出)。
原始的 C 代码将这些点存储在一个包含八个数组的结构体中。从性能角度来看,这是良好的,因为它对缓存友好:你可以对许多点的维度进行长运行操作,这很快。但在 Swift 中将数据暴露为点元素集合会导致源代码更易于理解。
团队最初编写的跨语言桥接代码优先考虑便利性、安全性和简单性,将字形数据从 C 结构体复制到 Swift,然后在程序完成后复制回去。最初,这些复制操作占新解释器运行时间的约 20%。最终,团队使用了提供对底层 C 结构体安全访问的投影类型(projection types)。因此,Swift 在不复制或转换底层数据结构的情况下提供了可读性。
遵循 WebKit 的 Safer Swift 指南,示例展示了如何将来自 C 的桥接结构包装在使用 Ref 进行生命周期安全、协调对底层数据的边界安全访问,并向调用者返回惯用 Swift 类型的投影类型中。所有 unsafe 表达式都带有 // SAFETY: 注释,记录安全不变式以及保证其为真的推理过程。
@safe struct Zone: ~Copyable, ~Escapable {
let _element: Ref<fnt_ElementType>
@_lifetime(copy element)
init(wrapping element: Ref<fnt_ElementType>) {
// SAFETY: 调用者传递的 `fnt_ElementType` 必须满足:
// * `sp`, `ep` 指向长度 ≥ `maxContourCount` 的数组。
unsafe _element = element
}
func readContour(index: Int) -> ClosedRange<Int> {
precondition(0..<contourCount ~= index)
// SAFETY: `index` 已在上面进行边界检查;`_element.pointee.sp` 和 `_element.pointee.ep`
// 是根据 `init(wrapping:)` 前提条件的长度为 `maxContourCount` 的活动数组。
return unsafe Int(_element.value.sp[index])...Int(_element.value.ep[index])
}
}
3. 短期分配
虽然运行时开销以整体形式施加成本,而跨语言边界复制数据表现为特定的热点,但短期内存分配的成本可能以这两种方式出现。
filter 和 map 等操作会分配内存,但仅当值逃逸时才需要该分配。Swift 标准库提供了 .lazy.map 和 .lazy.filter,但它们并非在所有情况下都适用。对于仅迭代过滤或映射的逻辑,使用惰性求值更为高效。
关键要点
- 安全与性能并重:Apple 将 TrueType 提示解释器从 C 重写为 Swift,不仅解决了内存安全问题,还使平均性能提升了 13%。
- 二进制兼容性是核心约束:重写必须保证像素级一致的字形渲染,确保现有应用程序无需修改即可无缝运行,正确性定义为与 C 实现的精确输出兼容。
- 测试投入巨大:测试代码量是生产代码量的近四倍,包括覆盖率达 99.7% 的单元测试和基于 1000 万 PDF 文件最小化语料库的模糊测试。
- **Swift
