特性

Cargo 的“特性(features)”提供了一种机制来表达条件编译可选依赖。包在其 Cargo.toml 文件的 [features] 表中定义了一组命名特性,每个特性都可以被启用或禁用。可以通过命令行使用诸如 --features 的标志来启用正在构建的包的特性。依赖项的特性可以在 Cargo.toml 中的依赖项声明中启用。

注意:发布到 crates.io 上的新 crate 或新版本现在最多只能有 300 个特性。例外情况会根据具体情况批准。详情请参阅这篇博文。鼓励通过 crates.io Zulip 频道参与解决方案讨论。

另请参阅特性示例章节,了解特性的一些用法示例。

[features] 部分

特性在 Cargo.toml 文件的 [features] 表中定义。每个特性指定一个数组,其中包含它启用的其他特性或可选依赖项。以下示例展示了特性如何用于一个 2D 图像处理库,其中可以可选地包含对不同图像格式的支持

[features]
# Defines a feature named `webp` that does not enable any other features.
webp = []

定义此特性后,可以使用cfg 表达式在编译时有条件地包含支持所需特性的代码。例如,包的 lib.rs 文件内部可以包含以下内容

#![allow(unused)]
fn main() {
// This conditionally includes a module which implements WEBP support.
#[cfg(feature = "webp")]
pub mod webp;
}

Cargo 使用 rustc--cfg 标志在包中设置特性,代码可以使用cfg 属性cfg来测试它们是否存在。

特性可以列出它启用的其他特性。例如,ICO 图像格式可以包含 BMP 和 PNG 图像,因此当启用 ICO 特性时,它应该确保也启用了这些其他特性

[features]
bmp = []
png = []
ico = ["bmp", "png"]
webp = []

特性名称可以包含来自Unicode XID 标准(包括大多数字母)的字符,此外还允许以下划线 _ 或数字 09 开头,并且在第一个字符之后还可以包含 -+.

注意crates.io 对特性名称的语法施加了额外的限制,它们必须只能是ASCII 字母数字字符或 _-+

default 特性

默认情况下,所有特性都是禁用的,除非明确启用。这可以通过指定 default 特性来改变

[features]
default = ["ico", "webp"]
bmp = []
png = []
ico = ["bmp", "png"]
webp = []

构建包时,会启用 default 特性,进而启用列出的特性。此行为可以通过以下方式更改:

注意:选择默认特性集时要小心。默认特性是一种便利,使用户更容易使用包,而无需仔细选择要为常见用法启用哪些特性,但这也有一些缺点。除非指定了 default-features = false,否则依赖项会自动启用默认特性。这使得确保不启用默认特性变得困难,特别是对于在依赖关系图中出现多次的依赖项。每个包都必须确保指定了 default-features = false 以避免启用它们。

另一个问题是,从默认集中移除特性可能会导致SemVer 不兼容的更改,因此您应该确信您将保留这些特性。

可选依赖项

依赖项可以标记为“可选(optional)”,这意味着默认情况下不会编译它们。例如,假设我们的 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 特性将启用列出的两个依赖项。这也避免了创建隐式的 ravifrgb 特性,因为我们不希望用户单独启用它们,因为它们是我们的 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"] }

注意:这可能无法确保默认特性被禁用。如果另一个依赖项包含 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/feature-name" 语法如果 package-name 是一个可选依赖项,也会启用它。这通常不是您想要的。您可以添加一个 ?,例如 "package-name?/feature-name",这只会在其他地方启用了可选依赖项时才启用给定特性。

注意? 语法仅在 Rust 1.60 及更高版本中可用。

例如,假设我们为库添加了一些序列化支持,这需要启用一些可选依赖项中的相应特性。可以这样做:

[dependencies]
serde = { version = "1.0.133", optional = true }
rgb = { version = "0.8.25", optional = true }

[features]
serde = ["dep:serde", "rgb?/serde"]

在此示例中,启用 serde 特性将启用 serde 依赖项。它还会为 rgb 依赖项启用 serde 特性,但前提是其他地方已经启用了 rgb 依赖项。

命令行特性选项

可以使用以下命令行标志来控制哪些特性被启用:

  • --features FEATURES:启用列出的特性。多个特性可以用逗号或空格分隔。如果使用空格,请确保在 shell 中运行 Cargo 时使用引号将所有特性括起来(例如 --features "foo bar")。如果在工作空间中构建多个包,可以使用 package-name/feature-name 语法为特定的工作空间成员指定特性。

  • --all-features:激活命令行上所有选定包的所有特性。

  • --no-default-features:不激活选定包的default 特性

特性统一

特性对于定义它们的包来说是独有的。在一个包上启用某个特性不会在其他包上启用同名特性。

当一个依赖项被多个包使用时,Cargo 在构建它时将使用该依赖项上所有启用特性的并集。这有助于确保只使用该依赖项的一个副本。有关更多详细信息,请参阅解析器文档的特性部分

例如,我们来看一下winapi 包,它使用了大量特性。如果您的包依赖于包 foo,该包启用了 winapi 的 “fileapi” 和 “handleapi” 特性,而另一个依赖项 bar 启用了 winapi 的 “std” 和 “winnt” 特性,那么 winapi 将在构建时启用所有这四个特性。

winapi features example

由此产生的一个结果是,特性应该是*可加的(additive)*。也就是说,启用特性不应该禁用功能,并且通常可以安全地启用任何特性组合。特性不应该引入SemVer 不兼容的更改

例如,如果您想可选地支持no_std 环境,不要使用 no_std 特性。相反,使用一个*启用* stdstd 特性。例如:

#![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" 解析器在一些不需要统一特性的情况下避免了统一。具体情况在解析器章节中有描述,但简而言之,它在以下情况中避免统一:

在某些情况下,避免统一是必要的。例如,如果构建依赖项启用了 std 特性,并且同一个依赖项在 no_std 环境中用作普通依赖项,那么启用 std 将导致构建失败。

然而,一个缺点是这会增加构建时间,因为依赖项会被多次构建(每次使用不同的特性)。使用版本 "2" 解析器时,建议检查多次构建的依赖项,以减少总体构建时间。如果不是*必须*使用不同的特性构建那些重复的包,考虑在依赖项声明features 列表中添加特性,这样重复的依赖项最终会具有相同的特性(因此 Cargo 只会构建一次)。您可以使用cargo tree --duplicates 命令检测这些重复的依赖项。它会显示哪些包被多次构建;查找列出相同版本的条目。有关获取已解析特性的更多信息,请参阅检查已解析的特性。对于构建依赖项,如果您使用 --target 标志进行交叉编译,则无需这样做,因为在这种情况下,构建依赖项总是与普通依赖项分开构建的。

解析器版本 2 命令行标志

resolver = "2" 设置也会改变 --features--no-default-features 命令行选项的行为。

使用版本 "1" 时,您只能为您当前工作目录中的包启用特性。例如,在一个包含 foobar 包的工作空间中,如果您位于包 foo 的目录中,并运行命令 cargo build -p bar --features bar-feat,这将失败,因为 --features 标志只允许在 foo 上启用特性。

使用 resolver = "2" 时,特性标志允许为命令行中使用 -p--workspace 标志选择的任何包启用特性。例如:

# This command is allowed with resolver = "2", regardless of which directory
# you are in.
cargo build -p foo -p bar --features foo-feat,bar-feat

# This explicit equivalent works with any resolver version:
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 部分找到。简而言之,遵循以下规则:

有关注意事项和示例,请参阅链接。

特性文档和发现

建议您记录包中可用的特性。这可以通过在 lib.rs 顶部添加文档注释来实现。例如,请参阅 regex crate 源码,它在渲染后可以在 docs.rs 上查看。如果您有其他文档,例如用户指南,考虑将特性文档添加到其中(例如,请参阅 serde.rs)。如果您的项目是二进制项目,考虑在 README 或项目的其他文档中记录特性(例如,请参阅 sccache)。

清晰地记录特性可以设定关于被视为“不稳定”或不应使用的特性的预期。例如,如果存在一个可选依赖项,但您不希望用户明确将该可选依赖项列为特性,则将其从文档列表中排除。

发布在 docs.rs 上的文档可以使用 Cargo.toml 中的元数据来控制构建文档时启用哪些特性。有关更多详细信息,请参阅docs.rs 元数据文档

注意:Rustdoc 实验性地支持在文档中添加标注,以指示使用某些 API 需要哪些特性。有关更多详细信息,请参阅doc_cfg 文档。一个示例是 syn 文档,您可以在其中看到彩色框,指示使用它需要哪些特性。

发现特性

当特性在库 API 中有文档记录时,您的用户可以更容易地发现哪些特性可用以及它们的作用。如果包的特性文档不容易获得,您可以查看 Cargo.toml 文件,但这有时很难找到。在 crates.io 上的 crate 页面如果提供了源码仓库链接,可以通过它查看。可以使用 cargo vendorcargo-clone-crate 等工具下载源码并进行检查。

特性组合

因为特性是一种条件编译形式,它们需要指数级的配置和测试用例才能实现 100% 覆盖。默认情况下,测试、文档和其他工具(如 Clippy)只会使用默认特性集运行。

我们鼓励您考虑针对不同特性组合的策略和工具 — 每个项目都会根据时间、资源以及覆盖特定场景的成本效益有不同的要求。常见的配置可能是使用或不使用默认特性、特定的特性组合或所有特性组合。