type
status
date
slug
summary
tags
category
icon
password
原文为 Effective Rust 第 33 条,笔者将其翻译后提交至 Effective Rust 中文版,同步转载至此处
Rust 附带一个名为
std
的标准库,其中包含了从标准数据结构到网络,从多线程支持到文件 I/O 等用于各种常见任务的代码。为了方便使用, std
中的一些项目会通过 prelude 自动导入到你的程序中,prelude 是一组 use
语句,可以在不需要指定完整名称的情况下直接使用这些常见类型(例如使用 Vec
而不是 std::vec::Vec
)。Rust 还支持为无法提供完整标准库的环境构建代码,如引导加载程序、固件或一般的嵌入式平台。通过在
src/lib.rs
文件顶部添加 #![no_std]
属性,可以指示 crate 在这类受限环境中编译。本节将探讨在
no_std
环境中编译时我们会失去哪些功能,以及仍然可用的库函数。我们会发现,即使在这种受限环境中,依然有相当多的功能可供使用。不过,这里特别关注的是针对库代码的
no_std
支持。构建 no_std
可执行文件的复杂性超出了本文的范围,因此本节重点是对那些不得不在如此极简环境中工作的可怜人来说,如何确保库代码是可用的。core
这些基本类型的不同名称以
core::
开头,表明它们来自 core
库,这是一个即使在大多数 no_std
环境中也可用的标准库。这些 core::
类型的行为与等效的 std::
类型完全相同,因为它们实际上是同个类型 —— 对应的 std::
版本都只是底层 core::
类型的重新导出。这意味着有一种快速而肮脏的方法来判断某个
std::
项目在 no_std
环境中是否可用: 访问 doc.rust-lang.org
并找到感兴趣的 std
项目,点击 “soure”(在右上角),如果它跳转到了 src/core/...
中,则说明该项可通过 core::
在 no_std
下使用。实际中,对于许多环境(甚至是
no_std
)来说,纯粹依赖 core
会有很大的限制,因为 core
的一个核心(双关语)约束是它不会进行堆分配。虽然 Rust 很擅长将数据放入栈中并安全地跟踪其相应的生命周期(第 14 条),但
core
的这种限制仍然意味着无法提供标准数据结构(vector, map, set),因为它们都需要分配堆空间来存放内部元素。所以这个限制也大大减少了在此环境中可用的 crate 数量。alloc
和
core
一样,这些 alloc
变体实际上在底层是相同的类型。例如 std::vec::Vec
的真实名称是 alloc::vec::Vec
。如果
no_std
的 crate 要使用 alloc
,需要在 src/lib.rs
额外显式声明 extern crate alloc;
:引入
alloc
库可以启用许多我们熟悉的朋友,现在他们以真实的名称被称呼:有了这些朋友,许多 crate 就可以兼容
no_std
—— 前提是不涉及 I/O 或网络。但
alloc
提供的数据结构显然还缺少了两个集合 —— HashMap
和 HashSet
,它们特定于 std
而不是 alloc
。这是因为这些基于 hash 的容器依靠随机种子来防止 hash 冲突攻击,但安全的随机数生成需要依赖操作系统的帮助,而 alloc
不能假设操作系统存在。另一个缺失的部分是同步功能,例如
std::sync::Mutex
,这是多线程代码所必需的,但这些类型同样特定于 std
,它们依赖操作系统的同步原语,如果没有操作系统,这些同步原语也将不可用。在 no_std
下编写多线程代码,第三方 crate 可能是唯一选择,例如 spin
。为 no_std
编写代码
前文已经指出,对于某些 crate 库,要使代码兼容
no_std
只需要以下修改:- 用相同的
core::
或alloc::
crate 替换std::
类型(由于缺少std
prelude,还需要use
完整的类型名称)
- 将
HashMap
/HashSet
迁移为BTreeMap
/BTreeSet
这里还有一个问题:Rust 编译器并不会告诉你一个
no_std
crate 中是否有引入使用了 std
的依赖。这意味着只要添加或更新一个使用了 std
的依赖,就能轻松破坏掉 no_std
crate 的兼容性。为了避免这个情况,请为
no_std
构建添加 CI 检查,以便 CI 系统(第 32 条)能在这种情况发生时发出警告。Rust 工具链支持开箱即用的交叉编译,因此只需为不支持 std
的目标系统(例如 --target thumbv6m-none-eabi
)执行交叉编译,任何无意中依赖 std
的代码就会在此时编译失败。所以,如果依赖项都支持,并且上面的简单修改就够了的话,不妨考虑让库代码兼容
no_std
。这不需要太多额外工作,却能让库有更广泛的适用性。如果这些转换并没有覆盖 crate 中的所有代码,但未覆盖的只是代码中的一小部分或包装良好的部分,那么可以考虑向 crate 添加一个 feature (第 26 条)以控制是否启用这部分代码。
这种允许使用
std
特定功能的 feature ,通常会将其命名为 std
:或是将控制是否启用了
alloc
派生功能的 feature 命名为 alloc
:注意,设计这类 feature 时有个容易忽视的陷阱:不要设置一个
no_std
feature 来禁用需要 std
的功能(或类似的 no_alloc
feature),正如第 26 条中提到的,feature 应当是可累加的,这样做会导致无法将启用了 no_std
和没有启用的两个 crate 用户组合在一起 —— 前者会删除后者所依赖的代码。可错分配(Fallible Allocation)
前面考虑了两种不同的
no_std
环境:不允许堆分配的完全嵌入式的环境(core
)和允许堆分配的更宽松的环境(core
+ alloc
)。然而,有一些重要的环境介于两者之间,特别是那些允许堆分配但可能会失败的环境,因为堆空间是有限的。
不幸的是,Rust 的标准
alloc
库假设堆分配不会失败,但这个假设并不总是成立。即使简单地使用
alloc::vec::Vec
,也可能在每一行代码中都触发分配:这些操作都不会返回
Result
,当这些分配失败了,会发生什么?答案取决于工具链、目标平台和配置,但很可能会导致
panic!
和程序终止。这里无法通过某种方式处理第 3 行的分配失败,并让程序继续执行第 4 行。这种可靠分配(infallible allocation)的假设简化了在“正常”用户空间中运行的代码,因为在这些环境中,内存几乎是无限的 —— 或者内存耗尽意味着计算机本身出现了更严重的问题。
但可靠分配不适用于内存有限且程序必须应对内存不足的环境。这是一个在一些较老、内存安全性较差的语言中反而有更好支持的(罕见)领域:
- C 的级别足够低,分配是手动的,因此可以检查
malloc
的返回值是否为NULL
。
- C++ 可以使用异常机制,捕获以
std::bad_alloc
异常形式出现的分配失败。
第一个尝试是添加“可错的集合分配”,它为许多涉及分配的集合 API 添加了允许可错分配的替代实现,通常是添加一个会返回
Result<_, AllocError>
的 try_<operation>
操作,例如:Vec::try_reserve
作为Vec::reserve
的替代品
Box::try_new
作为Box::new
的替代品(在 nightly toolchain 中可用)
但这些允许可错分配的 API 功能也仅限于此。例如,(目前为止)还没有与
Vec::push
等效的可错分配版本,因此编写组装 vector 的代码时,可能需要进行精确计算,以确保不会发生内存分配错误。除了增加允许可错分配的入口外,还可以通过关闭默认开启的
no_global_oom_handling
配置来禁用可靠分配操作。在堆内存有限的环境(如 Linux 内核)中,可以显式禁用此标志,以确保不会在代码中无意使用了可靠分配。要记住的事
std
crate 中的许多项目实际上来自core
或alloc
。
- 因此,使库代码兼容
no_std
可能比您想象的更简单。
- 通过在 CI 中检查
no_std
代码来确认no_std
代码保持no_std
兼容。
- 注意,当前支持在有限堆环境中工作的库十分有限。
注释
- 有关创建
no_std
可执行文件所涉及的内容,请参阅 The Embedonomicon 或 Philipp Opperamann 的早期博客文章。
- 在 Rust 2018 之前,
extern crate
声明用来引入依赖项。现在这些依赖完全由Cargo.toml
处理,但仍使用extern crate
机制引入那些no_std
环境中 Rust 标准库的可选部分(即 sysroot crates)。
- 还可以为
new
调用添加std::nothrow
重载,并检查是否返回nullptr
。但一些容器方法比如vector<T>::push_back
,在内部进行分配,因此只能通过抛出异常来表示分配失败。