- 文档
【免费下载链接】rfcs
RFCs for changes to Rust
导读
RFC 1415(Trim thestd::os::rawmodule)是 Rust 标准库平台扩展模块演进史中的一次重要设计决策:它提出弃用std::os::$platform::raw中的 C 类型别名与结构体(如stat、ino_t、dev_t、mode_t),改由返回u64等 Rust 原生类型的 trait 访问器暴露底层系统数据。本文以该 RFC 为骨架,结合仓库内 RFC 517(io-os-reform) 与 RFC 1105(API evolution) 的背景,深入讲解其动机、详细设计、破坏性变更的处理方式、权衡与备选方案,帮助读者理解 Rust 标准库在处理"跨平台抽象 vs 原始 FFI 类型"这一永恒矛盾时的取舍逻辑。
背景:std::os::raw的由来与 RFC 517 的愿景
std::os::raw系列模块的设计源头可追溯至 RFC 517(io-os-reform)。该 RFC 为std::io与std::os的稳定化提出了一整套重构方案,其中针对平台特定 API 确立了"平台特定 opt-in"原则:以os::unix、os::linux、os::windows等按平台命名的子模块组织扩展 API,并由#[cfg]开关控制。
在这一框架下,raw模块被赋予了一项特殊职责:对标准库中的各种 Rust 类型执行 lowering 操作,将其降级为对应的底层平台类型。例如:
fs::Metadata结构可以被降级为底层的sys::stat结构;- 通过暴露这些从操作系统获取的底层数据,允许标准库外部的使用者自行构建更高层的抽象。
这一策略本身是合理的:它给了外部生态"读取全部原始字段"的能力。但正如 RFC 1415 在 Motivation 部分所指出的,把这条路线走到底,会遇到几个非常现实的问题。
动机:为什么raw模块必须被"裁剪"
RFC 1415 逐一列出了将 libc 结构体原样暴露为标准库公共 API 时暴露出的三大问题。
问题一:stat等 libc 结构并没有"唯一规范定义"
对于某些 libc 结构,并不存在一个公认的、唯一的定义。最典型的例子是stat:
- 在 32 位 Linux 上,
stat的定义会随是否启用LFS(Large File Support,大文件支持)而改变; - LFS 通过
-D_FILE_OFFSET_BITS宏控制,启用后字段会从 32 位切换为 64 位; - 因此,如果标准库把这些
raw类型宣传为"与 libc 具备 FFI 兼容性",这种说法在所有场景下都是不成立的——因为你无法保证使用者编译时的宏定义与标准库一致。
换句话说,"raw 类型 = FFI 兼容的 libc 类型"这一承诺,本身就是一个不稳定的地基。
问题二:暴露底层接口会锁死实现细节,阻碍演进
细腻地导出原始底层接口(例如从&fs::Metadata中取出&stat)会让标准库难以在后续版本中修改内部实现。
RFC 1415 给出了一个极具说服力的现实案例:当时的 32 位 Linux 标准库并没有使用 LFS 函数,导致大于 4GB 的文件无法打开。要修复这个问题,几乎必然涉及改变stat结构的字段布局;但一旦&stat已经成为公开 API,这种修改就会变成破坏性变更,实施难度陡增。
这揭示了一个核心矛盾:接口暴露得越彻底,实现层越难呼吸。FFI 结构体的布局是"历史债",一旦对外承诺,就会被钉死在 ABI 上。
问题三:平台差异导致静默数据截断(Android 案例)
raw模块中的 trait 扩展试图在所有平台返回对应的 libc 别名类型。例如DirEntryExt::ino函数声明返回ino_t。但"ino_t"这个别名在不同平台上的定义并不一致,且与实际存储的字段类型可能不匹配:
- 在 Android 上,
ino_t被定义为u32; - 但
dirent结构中实际存储的d_ino字段是u64; - 于是,当调用
DirEntryExt::ino()时,返回值在 Android 上会被静默截断——高位 32 位数据直接丢失,且没有任何错误提示。
这不是理论上的风险,而是当时已经真实存在的 bug。它说明:用"C 类型别名"作为跨平台 trait 的返回类型,本质上是在用一套并不统一的类型系统去套所有平台,必然在某些平台上出错。
详细设计:两条主线、一个明确边界
针对上述问题,RFC 1415 的详细设计围绕两条主线展开。
主线一:弃用raw模块中的类型与类型别名
首先,std::os::*::raw中的类型和类型别名将全部被弃用(deprecated),包括:
stat(结构体);ino_t、dev_t、mode_t等类型别名。
弃用后的推荐替代方案是直接使用libccrate 中对应的定义——既然标准库不再承诺 FFI 兼容,那么真正需要与 C 互操作的用户应该显式依赖libccrate。
这里有一条明确保留的例外:C 整数类型c_int及其同类(c_char、c_void等)不会被弃用。这是合理的——c_int等类型是 Rust 与 C 进行 FFI 时的基础砖块,与"暴露平台数据结构"是完全不同层面的问题。
主线二:扩展 trait 一律返回u64
其次,所有现有的扩展 trait 将不再返回平台特定的类型别名。以DirEntryExt::ino为例,其返回值将从ino_t改为u64。
RFC 1415 提出的规则非常干脆:
除非 100% 确定更少的位数足够,否则所有此类访问器一律返回
u64。
这一规则带来两重收益:
- 跨平台一致性:无论底层是 32 位还是 64 位
ino_t,Rust 侧 API 的返回类型始终是u64,使用者无需针对平台编写分支; - 彻底消灭截断问题:Android 上"
ino_t是 u32 而实际存储是 u64"这类静默数据丢失问题不再可能发生。
同时,这也把 FFI 兼容性的"脏活"正式移交给了libccrate:标准库负责以无损方式把数据交到 Rust 用户手中,用户若需要把数值再交还给 C,则由libccrate 负责类型定义与转换。
MetadataExt的改造:从as_raw_stat到字段级访问器
std::os::*::fs::MetadataExt是本次改造的重点对象:
- 其
as_raw_stat方法将被弃用——不再允许用户直接把Metadata降级为底层的&stat; - 取而代之的是:为
stat结构的所有关联字段逐一提供访问函数; - 每个平台各自持有一个 trait 来暴露这些信息(延续 RFC 517 的"平台特定 opt-in"组织方式);
- 按照上面的统一规则,这些方法也大概率全部返回
u64。
这种"从拿整个结构体,到逐个拿字段"的转变,本质上是把实现的自由度还给了标准库:只要Metadata内部仍然能解析出每个字段的值,底层到底存的是stat、stat64还是任何别的结构,对用户完全不可见、也不受影响。
破坏性变更的现实:as转换是唯一的"创可贴"
RFC 1415 坦率地承认,上述修改同时是破坏性变更,且不能立即修复所有现存问题:
- 代码
let a: ino_t = entry.ino()会编译失败,因为ino()现在返回u64,而ino_t的定义在部分平台上并不是u64; - 32 位 Linux 上的
stat结构体字段仍然是 32 位(即不镜像 libc 的stat64),大文件问题并没有随本次 RFC 一步到位地解决。
RFC 提出,如需更进一步,可以对平台特定模块做更彻底的修改:把所有类型别名切换为u64,并在 Linux 上将stat直接重新定义为stat64(保留原名)。但这样做的代价是明确的——std::os::raw将正式、永久地不再与 C 保持 FFI 兼容。
为了平滑这一转变:
- 所有破坏性变更都会在**弃用消息(deprecation message)**中清楚说明;
- 依据 RFC 1105(API evolution) 所确立的破坏性变更政策,这类变更被定性为"minor change":受影响的代码通常只需添加一处局部的
as类型转换即可同时兼容多个 Rust 版本。
也就是说,升级路径被刻意设计为"浅层修复"——entry.ino() as ino_t这类局部改动足以让代码在版本更替期间正常编译运行,这正是 RFC 1105 中"minor changes should require at most minor amounts of work upon upgrade"原则的体现。
权衡:Drawbacks
RFC 1415 对自己的缺点毫不回避:
- 严格来说是破坏性变更:预期真正受影响的代码不会很多,但当时并没有实际数据支撑这一判断(这一缺口后来主要由 Crater 等生态回归测试工具填补);
- 一律返回
u64可能造成困惑:u64与底层 C 类型相比,可能在符号性和位数上都有巨大差异(例如某些平台的有符号 32 位字段返回为无符号 64 位); - 转换回原始类型可能繁琐:把
u64转回合适的 C 类型需要用户自行处理。
RFC 给出的辩护是务实的:访问这些原始字段在理论上发生频率极低,标准库的主要职责是提供跨平台访问器,而非为每个原始字段都提供与 C 类型一一对应的便捷转换。
备选方案:Alternatives
RFC 1415 还评估了两种替代路线:
方案 A:保留结构、改写类型
修改raw模块的文档,明确声明其中的类型故意不保证 FFI 兼容,同时保留现有结构,但把所有类型改写为"实现本 RFC 后本来就会变成的样子":
- Android 上的
ino_t改为u64; - 32 位 Linux 上的
stat改为stat64。
被否决的理由:如果类型已经不是真正的 C 类型了,保留所有 C 命名和结构的意义就变得不清晰——徒增混乱,不如直接弃用。
方案 B:新增无损访问器,不破坏旧接口
不破坏现有功能,而是新增一批提供"无损版本"的访问器和类型:
- 在
DirEntryExt上新增ino64函数返回u64; - 为
stat新增as_raw_stat64。
被否决的理由:这会迫使Metadata内部同时存储两份不同的stat结构(普通版与 64 位版),内存与实现复杂度上升;而实践中真正受破坏影响的代码可能非常少,不值得为兼容付出这样的代价。
未决问题:Unresolved questions
RFC 1415 在结尾留下了一个开放问题,也是整个"一律u64"策略中最值得商榷的一点:
"几乎总是返回
u64"的策略是否过于严格?像mode_t这样的类型是否应该被明确允许返回i32?返回值是否至少应该尝试保留符号?
这个问题触及了"统一性"与"保真度"之间的张力:u64统一了位数,却抹掉了符号信息;而保留符号又会重新引入平台差异。最终如何取舍,很大程度上取决于标准库后续的实践反馈。
总结与延伸阅读
RFC 1415 的价值不仅在于具体 API 的增删,更在于它确立了一条清晰的分工原则:
- 标准库负责"无损、跨平台"地交付数据(统一返回
u64); libccrate 负责"FFI 兼容"的类型定义;- 两者解耦之后,标准库才真正获得了调整内部实现(如切换到 LFS)的自由。
这与 RFC 517 中"平台特定 opt-in"的设计哲学一脉相承,也与 RFC 1105 的 API 演进政策 中"通过弃用与as转换实现低成本升级"的机制互为表里。对于想要理解 Rust 标准库平台扩展模块设计脉络的读者,建议按以下顺序延伸阅读:
- RFC 517:io-os-reform(raw 模块愿景的源头);
- RFC 1105:API evolution(破坏性变更分级与升级路径);
- RFC 494:c_str 与 c_vec 稳定性(标准库中另一处 C 类型互操作设计的对照);
- RFC 1307:OsString 简单方法(平台敏感字符串 API 的补充演进)。
- 文档
【免费下载链接】rfcs
RFCs for changes to Rust
相关推荐
Rust RFC 0980 精读:为 `Read` trait 设计 `read_exact` 精确读取方法的完整推演
Rust RFC 0980 精读:为 Read trait 设计 read_exact 精确读取方法的完整推演 read_exact 是如今 Rust 标准库
文档Rust 标准库 IO 体系重构设计:RFC 0517(std::io 与 std::os 大改)深度解析
Rust 标准库 IO 体系重构设计:RFC 0517(std::io 与 std::os 大改)深度解析 本文围绕 Rust RFC 0517( text/0
文档如何3步跑通Video2X:从源码构建到GPU调优的视频超分辨率实战
如何3步跑通Video2X:从源码构建到GPU调优的视频超分辨率实战 场景切入 你有一段480p的老视频,想放大到1080p又不损失细节;或者一段24fps的动
音视频视频处理图像处理深度学习
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考