第17章 Cargo Features 功能特性
Cargo “features” 提供了一套机制,用于实现条件编译和可选依赖。包在 Cargo.toml 的 [features] 表中定义一组命名 feature,每个 feature 都可以启用或禁用。在命令行中,可以通过 --features 等标志来启用当前正在构建的包的 feature。在 Cargo.toml 的依赖声明中,可以启用依赖项的 feature。
注意:发布到 crates.io 的新 crate 或新版本,其 feature 数量限制为 300 个。特殊情况可按个案申请例外。详情请参阅此博客文章。鼓励通过 crates.io 的 Zulip stream 参与解决方案的讨论。
请参见Features 示例章节,了解 feature 的用法示例。
[features] 部分
feature 定义在 Cargo.toml 的 [features] 表中。每个 feature 指定了一个数组,用于启用其他 feature 或可选依赖项。以下示例说明了如何在一个 2D 图像处理库中使用 feature,该库可以选择性支持不同的图像格式:
[features]
# 定义一个名为 `webp` 的 feature,它不启用其他任何 feature。
webp = []
定义此 feature 后,可以使用cfg 表达式在编译时有条件地包含支持所请求 feature 的代码。例如,该包的 lib.rs 中可以包含以下内容:
#![allow(unused)]
fn main() {
// 此条件语句包含一个模块,该模块实现了 WEBP 支持。
#[cfg(feature = "webp")]
pub mod webp;
}
Cargo 通过 rustc 的 --cfg 选项为包设置 feature,代码中可以用 cfg 属性或 cfg 宏来检测某个 feature 是否启用。
Feature 可以依赖其他 feature。例如,ICO 图像格式可以包含 BMP 和 PNG 图像,因此启用 ICO 时也应同时启用这两个 feature:
[features]
bmp = []
png = []
ico = ["bmp", "png"]
webp = []
Feature 名可以包含符合 Unicode XID 标准的字符(涵盖大多数字母),此外还允许以 _ 或数字 0 到 9 开头,首字符之后还可以使用 -、+ 或 .。
注意:crates.io 对 feature 名有更严格的限制,只能由 ASCII 字母数字字符或
_、-、+组成。
default feature
默认情况下,所有 feature 都是禁用的,除非显式启用。可以通过指定 default feature 来改变这一行为:
[features]
default = ["ico", "webp"]
bmp = []
png = []
ico = ["bmp", "png"]
webp = []
构建包时会自动启用 default feature,进而启用它列出的那些 feature。以下方式可以改变这一行为:
注意:选择默认特性集时要谨慎。默认特性是一种便利机制,让用户无需仔细挑选即可使用包,但存在一些缺点。除非指定
default-features = false,否则依赖会自动启用默认特性。这会导致很难确保默认特性未被启用,尤其当某个依赖在依赖图中出现多次时。每个包都必须指定default-features = false来避免启用这些特性。另一个问题是,从默认集合中移除某个特性可能会构成 SemVer 不兼容变更,因此你要确信会保留这些特性。
可选依赖
依赖可以标记为“可选”,这意味着它们默认不会编译。例如,假设我们的 2D 图像处理库使用一个外部包来处理 GIF 图像。可以这样表示:
[dependencies]
gif = { version = "0.11.1", optional = true }
默认情况下,这个可选依赖会隐式定义一个如下特性:
[features]
gif = ["dep:gif"]
这意味着只有当 gif 特性被启用时,该依赖才会包含进来。
代码中可以使用相同的 cfg(feature = "gif") 语法,并且可以像启用任何其他特性一样启用该依赖,例如 --features gif(参见下文命令行特性选项)。
在某些情况下,你可能不希望暴露一个与可选依赖同名的特性。
例如,可选依赖可能是一个内部细节,或者你希望将多个可选依赖分组在一起,又或者只是希望使用一个更好的名称。
如果在 [features] 表的任何地方使用 dep: 前缀指定可选依赖,就会禁用该隐式特性。
注意:
dep:语法仅在 Rust 1.60 及以上版本中可用。 旧版本只能使用隐式特性名称。
例如,为了支持 AVIF 图片格式,我们的库需要启用另外两个依赖项:
[dependencies]
ravif = { version = "0.6.3", optional = true }
rgb = { version = "0.8.25", optional = true }
[features]
avif = ["dep:ravif", "dep:rgb"]
在这个例子中,avif 特性会启用上面列出的两个依赖项。这样做还能避免创建隐式的 ravif 和 rgb 特性,因为我们不希望用户单独启用它们,它们对我们所在 crate 来说是内部实现细节。
注意:另一种可选包含依赖项的方式是使用平台特定的依赖项。与特性不同,这些依赖项是根据目标平台有条件地启用的。
依赖项的特性
依赖项的特性可以在依赖项声明中启用。features 键指定了要启用哪些特性:
[dependencies]
# Enables the `derive` feature of serde.
serde = { version = "1.0.118", features = ["derive"] }
可以使用 default-features = false 禁用 default 特性:
[dependencies]
flate2 = { version = "1.0.3", default-features = false, features = ["zlib-rs"] }
注意:这未必能确保禁用了默认特性。如果另一个依赖项包含了
flate2但没有指定default-features = false,那么默认特性仍会被启用。有关更多细节,请参阅下文关于特性统一的说明。
也可以通过 [features] 表启用依赖项的特性。语法为 "package-name/feature-name"。例如:
[dependencies]
jpeg-decoder = { version = "0.1.20", default-features = false }
[features]
# Enables parallel processing support by enabling the "rayon" feature of jpeg-decoder.
parallel = ["jpeg-decoder/rayon"]
当 package-name 是可选依赖时,"package-name/feature-name" 这种写法会同时启用该依赖,但这往往不是你想要的效果。可以写成 "package-name?/feature-name",加上 ? 后,只有当其他地方启用了这个可选依赖时,才会启用对应的 feature。
注意:
?语法从 Rust 1.60 开始才可用。
举个例子,假设我们的库增加了一些序列化支持,需要同时启用某些可选依赖中的对应 feature,可以这样写:
[dependencies]
serde = { version = "1.0.133", optional = true }
rgb = { version = "0.8.25", optional = true }
[features]
serde = ["dep:serde", "rgb?/serde"]
在这个例子里,启用 serde feature 会启用 serde 依赖,同时为 rgb 依赖启用 serde feature——但前提是 rgb 依赖已经被其他地方启用了。
命令行 feature 选项
以下命令行参数可以控制启用哪些 feature:
--featuresFEATURES:启用指定的 feature。多个 feature 可以用逗号或空格分隔。如果用空格分隔,在 shell 中运行 Cargo 时要给整个参数加引号(例如--features "foo bar")。如果在workspace 中构建多个包,可以用package-name/feature-name语法为特定的 workspace 成员指定 feature。--all-features:启用命令行所选所有包的全部 feature。--no-default-features:不启用所选包的defaultfeature。
注意:具体细节请查阅各子命令的文档,并非所有参数在所有子命令中都可用。
Feature 统一
feature 只对定义它的那个包有意义。在一个包上启用某个 feature,并不会启用其他包上的同名 feature。
当某个依赖项被多个包同时使用时,Cargo 在构建该依赖项时,会合并所有包上启用的特性。这有助于确保该依赖项只被构建一次。更多细节请参阅解析器文档中的 特性章节。
例如,看看 winapi 包,它使用了大量特性。如果你的包依赖于 foo,而 foo 启用了 winapi 的“fileapi”和“handleapi”特性;同时你的包还依赖于 bar,bar 启用了 winapi 的“std”和“winnt”特性,那么 winapi 最终会以这四个特性全部启用的方式构建。
由此带来一个要求:特性应当是可叠加的。也就是说,启用某个特性不应导致功能失效,且通常应允许安全地启用任意特性的组合。特性不应引入 SemVer 不兼容 的变更。
例如,如果你想可选地支持 no_std 环境,不要使用 no_std 特性,而应使用 std 特性来启用 std。示例:
#![allow(unused)]
#![no_std]
fn main() {
#[cfg(feature = "std")]
extern crate std;
#[cfg(feature = "std")]
pub fn function_that_requires_std() {
// ...
}
}
互斥特性
在极少数情况下,特性之间可能相互不兼容。应尽量避免这种情况,因为这需要协调依赖图中的所有包使用者,以确保它们不会同时启用这些特性。如果确实无法避免,可以考虑添加编译错误来检测此类场景。例如:
#[cfg(all(feature = "foo", feature = "bar"))]
compile_error!("feature \"foo\" and feature \"bar\" cannot be enabled at the same time");
与其使用互斥的功能特性,不如考虑以下替代方案:
- 将功能拆分到不同的包中。
- 当出现冲突时,在多个特性中选择一个。
cfg-if包可以辅助编写更复杂的cfg表达式。 - 设计代码架构,允许特性同时启用,并通过运行时选项控制具体使用哪一个。例如,可以通过配置文件、命令行参数或环境变量来选择要启用的行为。
查看解析后的特性
在复杂的依赖图中,理解不同特性如何在各个包中被启用有时颇具挑战性。cargo tree 命令提供了几种选项,帮助检查和可视化哪些特性处于启用状态。可以尝试以下选项:
cargo tree -e features:这会在依赖图中显示特性。每个特性都会展示是哪个包启用了它。cargo tree -f "{p} {f}":这是一种更紧凑的视图,以逗号分隔的列表形式显示每个包上启用的特性。cargo tree -e features -i foo:这会反转树形结构,展示特性如何流入给定的包“foo”。这在浏览整个图显得庞大且杂乱时非常有用。当你试图弄清楚某个特定包上启用了哪些特性以及原因时,请使用此命令。关于如何解读输出,请参阅cargo tree页面底部的示例。
特性解析器版本 2
可以在 Cargo.toml 中通过 resolver 字段指定不同的特性解析器,如下所示:
[package]
name = "my-package"
version = "1.0.0"
resolver = "2"
关于指定解析器版本的更多细节,请参见 解析器版本 一节。
版本 "2" 的解析器在几种情况下避免了特性合并,而这些合并有时是不受欢迎的。具体场景在解析器章节中有详细描述,简而言之,它会在以下情况避免合并:
- 针对当前未构建的目标架构,平台特定依赖上启用的 feature 会被忽略。
- 构建依赖和 proc-macro 不会与普通依赖共享 feature。
- 开发依赖只有在构建需要它们的 Cargo target(如 tests 或 examples)时才会激活 feature。
某些情况下必须避免 feature 统一。例如,构建依赖启用了 std feature,而同一个依赖同时作为普通依赖用于 no_std 环境,启用 std 就会导致构建失败。
不过这样做也有一个缺点:因为依赖会被多次构建(每次启用不同的 feature),构建时间可能变长。使用 "2" 版本 resolver 时,建议检查哪些依赖被重复构建,以缩短整体构建时间。如果这些重复的包并非必须以不同 feature 分别构建,可以考虑在依赖声明的 features 列表中添加相应 feature,让重复构建使用相同的 feature(这样 Cargo 就只会构建一次)。可以用 cargo tree --duplicates 命令检测这些重复的依赖,它会显示哪些包被构建了多次,注意查找版本号相同的条目。关于如何获取已解析 feature 的信息,参见检查已解析的 feature。至于构建依赖,如果你使用 --target 参数进行交叉编译,则无需担心这一点,因为在该场景下构建依赖本来就会与普通依赖分开构建。
Resolver version 2 的命令行参数
resolver = "2" 设置还会改变 --features 和 --no-default-features 这两个命令行选项的行为。
当使用版本 "1" 解析器时,你只能为当前工作目录中的包启用特性。例如,在一个包含包 foo 和 bar 的工作区中,如果你位于包 foo 的目录下并运行命令 cargo build -p bar --features bar-feat,该命令将失败,因为 --features 标志仅允许在 foo 上启用特性。
使用 resolver = "2" 时,特性标志允许为命令行中通过 -p 和 --workspace 标志选定的任何包启用特性。例如:
# 使用 resolver = "2" 时,无论当前在哪个目录,都允许执行此命令。
cargo build -p foo -p bar --features foo-feat,bar-feat
# 这种显式等价写法适用于任何 resolver 版本:
cargo build -p foo -p bar --features foo/foo-feat,bar/bar-feat
此外,使用 resolver = "1" 时,--no-default-features 标志仅禁用当前目录中包的默认特性。而在版本“2”中,它将禁用所有工作区成员的默认特性。
构建脚本
构建脚本 可以通过检查环境变量 CARGO_FEATURE_<name> 来检测包上启用了哪些特性,其中 <name> 是将特性名称转换为大写、并将 - 转换为 _ 后的结果。
必需特性
required-features 字段 可用于在特定特性未启用时禁用特定的 Cargo 目标。更多详情请参阅链接中的文档。
SemVer 兼容性
启用一个特性不应引入 SemVer 不兼容的更改。例如,特性不应以可能破坏现有用法的方式修改现有 API。有关哪些更改被视为兼容的更多细节,可查阅 SemVer 兼容性章节。
添加或删除特性定义和可选依赖时需要格外谨慎,因为这些变更有时并不向后兼容。更详细的内容请参阅《SemVer 兼容性》章节中的 Cargo 部分。简而言之,请遵循以下规则:- 以下操作在次版本(minor release)发布时通常是安全的:
- 添加新特性或可选依赖。
- 更改依赖项使用的特性。
- 以下操作在次版本(minor release)发布时通常不应执行:
详情请参见相关链接中的注意事项和示例。
特性的文档与发现机制
建议文档化包中可用的特性。可以在 lib.rs 顶部添加文档注释来实现。示例可参考regex 源码,渲染效果可见于docs.rs。如果你有其他文档(如用户指南),也考虑在那里补充特性说明(例如参见 serde.rs)。如果是二进制项目,建议在 README 或其他项目文档中说明特性(例如参见 sccache)。
清晰地文档化特性有助于设定预期,表明哪些特性被视为“不稳定”或不应被使用。例如,如果存在一个可选依赖,但不希望用户显式地将其作为特性列出,就应将其从文档化的列表中排除。
发布在 docs.rs 上的文档可以通过 Cargo.toml 中的元数据来控制构建文档时启用哪些 features。详情参见 docs.rs 元数据文档。
注意:Rustdoc 实验性地支持在文档中标注某些 API 需要哪些 feature 才能使用。详情参见
doc_cfg文档。一个例子是syn的文档,其中可以看到彩色方框,标注使用相应 API 所需的 feature。
发现 features
如果库在 API 文档中写明了 features,用户就更容易了解有哪些 feature 可用、各自的作用。如果某个包的 feature 文档不好找,可以直接查看 Cargo.toml 文件,但有时要找到它也不容易。crates.io 上的 crate 页面会在有源码仓库时提供链接,也可以用 cargo vendor 或 cargo-clone-crate 之类的工具下载源码来查看。
Feature 组合
Feature 本质上是条件编译,因此若要 100% 覆盖,测试配置和用例的数量会呈指数级增长。默认情况下,测试、文档以及 Clippy 等工具只会以默认的 feature 集合运行。
我们建议你针对不同的 feature 组合规划好测试策略和工具链——每个项目在时间、资源以及覆盖特定场景的成本收益方面都有不同的考量。常见的配置包括:带或不带默认 features、特定的 feature 组合,或所有 feature 的组合。
Features 示例
下面展示一些 feature 在实际项目中的应用示例。
尽量缩短构建时间、减小文件体积
有些包通过 features 来控制功能启用与否。禁用特定 feature 可以减小 crate 体积并缩短编译时间。以下是几个例子:
syn是一个用于解析 Rust 代码的流行 crate。鉴于其应用广泛,缩短编译时间对依赖它的众多项目很有意义。它提供了明确的文档列表,说明了可用于精简代码量的 features。regex拥有多个 features,且文档详尽。若移除 Unicode 支持,可删除部分大型查找表,从而减小最终文件体积。winapi拥有大量 features,用于限制其所支持的 Windows API 绑定范围。web-sys与winapi类似,提供广阔的 API 绑定表面,可通过 features 进行限制。
扩展行为
serde_json 包包含preserve_order feature,它改变了 JSON map 的行为,使其保留键的插入顺序。注意,该 feature 启用了可选依赖 indexmap 来实现这一新行为。
在修改此类行为时,务必确保变更符合SemVer 兼容原则。也就是说,启用该 feature 不应破坏默认(关闭 feature)下能正常构建的代码。
no_std 支持
一些包希望同时支持 no_std 和 std 环境。这在嵌入式和资源受限平台上非常实用,同时仍允许在支持完整标准库的平台上使用扩展功能。
wasm-bindgen 包定义了一个 std 特性,该特性 默认启用。在库的顶部,它 无条件启用了 no_std 属性。这确保了 std 和 std 预lude 不会自动进入作用域。随后,在代码的各个位置(如 示例 1 和 示例 2),它使用 #[cfg(feature = "std")] 属性来条件性启用需要 std 的额外功能。
重新导出依赖特性
重新导出依赖的特性可能很方便。这使得依赖该 crate 的用户能够控制这些特性,而无需直接指定这些依赖。例如,regex 重新导出了 来自 regex_syntax 包的特性。regex 的使用者不需要了解 regex_syntax 包,但仍可访问其中包含的特性。
C 库的 vendor 管理
有些包为常见的 C 库提供了绑定(有时也被称为"sys" crate)。这类包有时会允许你选择使用系统上已安装的 C 库,还是从源码构建。例如,openssl 包有一个 vendored feature,启用后会连带启用 openssl-sys 对应的 vendored feature。openssl-sys 的构建脚本里有相应的条件逻辑,使它改为从本地的 OpenSSL 源码副本构建,而不是使用系统里的版本。
curl-sys 包是另一个例子,它的 static-curl feature 会让它从源码构建 libcurl。注意它还有一个 force-system-lib-on-osx feature,可以强制使用系统的 libcurl,覆盖 static-curl 的设置。
Feature 优先级
有些包可能存在互斥的 feature。一种处理方式是让某个 feature 优先于另一个。log 包就是如此。它有多个 feature 用于在编译期选择最高日志级别,详见这里。它使用 cfg-if 来决定优先级:如果同时启用了多个 feature,级别较高的 "max" 会优先于较低的级别。
配套的 Proc-macro 包
某些包的 proc-macro 与其核心功能紧密绑定。但并非所有用户都需要使用这个 proc-macro。将 proc-macro 设为可选依赖,允许你灵活决定是否包含它。这样做很有用,因为有时 proc-macro 版本必须与父包保持同步,我们不想迫使用户同时指定两个依赖并保持它们同步。
一个例子是 serde,它有一个 derive feature,用于启用 serde_derive proc-macro。由于 serde_derive 包与 serde 紧密耦合,它使用等值版本要求来确保两者同步。
仅限 Nightly 通道的 feature
有些包希望试验仅在 Rust nightly 通道可用的 API 或语言特性。但它们可能不希望要求用户也使用 nightly 通道。例如,wasm-bindgen 有一个nightly feature,用于启用一种使用 Unsize 标记 trait 的扩展 API,而该 trait 在本文撰写时仅在 nightly 通道可用。
请注意,该 crate 的根目录使用cfg_attr 来启用 nightly feature。要记住,feature 属性与 Cargo features 无关,它用于选择启用实验性语言特性。
simd_support 特性是另一个例子,它依赖于一个仅在 nightly 通道上才能构建的依赖包。
实验特性
一些包包含了希望进行试验的新功能,但不想确保这些 API 的稳定性。通常,文档会标明这些特性属于实验性质,因此即使在小版本更新中,它们也可能发生变更或存在不兼容性。例如 async-std 包,它拥有一个 unstable 特性,该特性门控(gates)了用户可以主动启用的新 API,但这些 API 可能尚未完全准备好供依赖方稳定使用。