Rust API 绑定:CFStringGetBytes 很难,下篇

Always Processing 中文译文:Rust API 绑定:CFStringGetBytes 很难,下篇(原文:Rust API Bindings: CFStringGetBytes Is Hard, Part 2)

作者 TommyWu
封面圖片: Rust API 绑定:CFStringGetBytes 很难,下篇

译文 · 原文: Rust API Bindings: CFStringGetBytes Is Hard, Part 2 · 作者 Brian T. Kelley

原文:https://alwaysprocessing.blog/2024/01/07/rust-ffi-cfstr-getbytes-2 发布:2024-01-07 作者:Brian T. Kelley 译者:MiMo;代码块保留英文原样 | 仅供个人学习与交流


Rust API 绑定:CFStringGetBytes 之难,第二部分

运用 Rust 的语言特性,我们可以为 CFStringGetBytes 提供 API 绑定,在调用点阻止不受支持的参数组合,从而修复前文中指出的问题。

上一篇文章描述了我在为 CFStringGetBytes 构建 Rust 绑定时遇到的复杂情况。本文则分享我为第一层绑定所做设计决策背后的原理。这最底层的缓解了前文指出的四个问题。

我说 “第一层绑定”,是因为本文讨论的方法构成了最多四个其他 Rust 接口用于调用 CFStringGetBytes 的基础。然而,为何需要这么多层次?正如《Rust API 指南》所言:

函数应暴露中间结果以避免重复工作

许多用于解答问题的函数也会计算出相关的有趣数据。如果这些数据可能对客户端有用,考虑将其暴露在 API 中。

因此,在构建我认为是最地道的 Rust API 时,我层层递进地暴露了更低层次的接口,以提供更高的可定制性(代价是增加了复杂性)。

首先,我们回顾一下 Core Foundation 的 C API:

CFIndex CFStringGetBytes(CFStringRef theString, CFRange range, CFStringEncoding encoding, UInt8 lossByte, Boolean isExternalRepresentation, UInt8 *buffer, CFIndex maxBufLen, CFIndex *usedBufLen);

并将其与在我的 crate 中实现的最直接的 Rust 接口进行比较:

impl String {
pub fn get_bytes_unchecked(
&self,
range: impl RangeBounds<usize>,
encoding: GetBytesEncoding,
buf: Option<&mut [u8]>,
) -> GetBytesResult { /* ... */ }
}

&self 代表 CFStringRef(CF 字符串引用)指针,这是面向对象接口绑定的典型方式。

CFRange 参数的等效类型是 impl RangeBounds<usize>,允许调用者使用 Rust 的范围表达式。例如,调用者可以传递 .. 来指定字符串的完整范围。

CFRange 的字段类型为 CFIndex,这是一种有符号类型。本系列的第一篇文章讨论了实现无符号到有符号转换时的设计选择。

GetBytesEncoding 替代了 CFStringEncoding,并同时涵盖了 lossByteisExternalRepresentation 参数。以下部分将提供更多细节。

buf: Option<&mut [u8]> 捕获了可选的 UInt8 *bufferCFIndex maxBufLen 参数。Option 类型清晰地表达了缓冲区并非必需。如果提供了缓冲区,该切片将提供缓冲区的长度。

GetBytesResult 返回类型包含了 C API 的返回值(已转换的 UTF-16 代码单元数量)以及 C API 中的输出参数 usedBufLen

_unchecked 后缀暗示此方法存在一个调用者必须处理的特殊问题。后续章节将详细阐述这一行为。

该方法实现了对前文所述第四个问题的修复检查:

如果编码为 kCFStringEncodingUTF16isExternalRepresentation 为 true 且 maxBufLen 小于 2,Core Foundation 在写入 BOM(字节顺序标记)时将发生缓冲区溢出。(UTF-32 的 BOM 写入操作会验证缓冲区容量。)

若缺少此检查,该函数的使用可能导致不健全性(unsoundness),因此需要添加 unsafe 限定符。

GetBytesEncoding

GetBytesEncoding 结构体封装了 CFStringEncodinglossByteisExternalRepresentation 这三个参数。

pub enum GetBytesEncoding {
CharacterSet {
character_set: CharacterSet,
/// **Note:** Core Foundation will process surrogate pairs as two individual lossy code
/// points, so the number of output code points will equal the number of input code units.
loss_byte: Option<NonZeroU8>,
},
Utf8,
Utf16 {
byte_order: GetBytesByteOrder,
},
Utf32 {
byte_order: GetBytesByteOrder,
loss_byte: Option<NonZeroU8>,
},
}

CharacterSet 枚举涵盖了所有非 Unicode 编码。(其对应的字符串创建函数 CFStringCreateWithBytes 同样具有特殊性。字符串构建绑定同样特别处理 UTF-8、UTF-16 和 UTF-32 编码,并通过 CharacterSet 枚举处理所有非 Unicode 表示。)

绑定中的 lossByte 类型是 Option<NonZeroU8>,这旨在通过类型系统表达:若使用有损字节,其值必须为非零。

遗憾的是,对于前文中指出的第二个问题,我唯一能找到的缓解措施是使用注释:

一个代码点(code point)若编码为代理对(surrogate pair),对于非 Unicode 编码而言将变成两个有损代码点。

Utf8 编码没有有损字节,这缓解了在阅读调用处和文档时可能出现的意外行为 —— 即前文中第一个问题 a 部分指出的情况:

即使调用者提供了 lossByte,该函数也不会将该代码单元(code unit)作为有损转换来处理。

缺少转换回退机制可能暗示 UTF-8 不会像 UTF-16 那样失败,这是该方法使用 _unchecked 后缀的一个因素 —— 直接调用的行为可能与默认预期不符。

UTF-16 有一个 GetBytesByteOrder 字段(下文讨论),它实现了 isExternalRepresentation 参数的功能。

UTF-32 与 UTF-16 类似,也有一个 GetBytesByteOrder 字段,并且与 CharacterSet 相似,它有一个损失字节(loss byte)用于处理无效的代理对(surrogates)(说明 UTF-32 是唯一实现损失字节支持的 Unicode 目标编码,正如上一篇文章中第一个问题的 b 部分所述)。

UTF-16 和 UTF-32 使用 16 位和 32 位整数标量(scalars),它们可能采用大端序(big endian)或小端序(little endian)的字节顺序。GetBytesByteOrder 枚举了支持的选项。

pub enum GetBytesByteOrder {
BigEndian,
HostNative {
include_bom: bool,
},
LittleEndian,
}

Core Foundation 仅支持在使用主机原生字节序时写入字节顺序标记(BOM)(isExternalRepresentation = true)。此枚举通过防止调用者指定不支持的组合,从而避免出现意外结果。

GetBytesEncoding::Utf8 与 GetBytesByteOrder 中 isExternalRepresentation 实现的组合,共同解决了前文指出的第三个问题:

尽管注释中有所暗示,isExternalRepresentation 并未为 UTF-8 编码 BOM。

GetBytesResult

此函数返回一个结构体,其中包含 C 语言 API 的返回值及输出参数。

pub struct GetBytesResult {
pub buf_len: usize,
pub remaining: Option<Range<usize>>,
}

如果 bufSome,则 buf_len 字段包含写入切片的字节数;否则,它包含转换处理范围所需的字节数。

如果调用已转换整个输入范围,则 remaining 字段为 None;否则,它包含调用期间未转换的输入范围部分。返回一个 Range 而非已转换的 UTF-16 码元数量,能通过提供明确的 “完成” 信号和下次调用的范围参数值来简化转换循环的实现。

如果调用方未提供损失字节,任何转换(UTF-16 转换除外)都可能失败。由于此函数是 “未检查的”(unchecked),调用方有责任检查是否取得前进进展,否则将面临永不终止的风险。

下一篇博文将讨论 get_bytes 方法,该方法明确处理有损转换并防止潜在的无限循环。