功能

Cargo 的“功能”提供了一种表达条件编译可选依赖的机制。一个包在 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 图像,因此启用它时,应确保也启用了其他功能

[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 不兼容的更改,因此您应该确信您会保留这些功能。

可选依赖

可以将依赖项标记为“可选”,这意味着默认情况下不会编译它们。例如,假设我们的 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"] }

注意:这可能无法确保禁用默认功能。如果另一个依赖项在未指定 default-features = false 的情况下包含了 flate2,则将启用默认功能。有关更多详细信息,请参见下面的功能统一

也可以在 [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 包,它使用了大量功能。如果您的包依赖于启用了 winapi 的“fileapi”和“handleapi”功能的包 foo,并且另一个依赖项 bar 启用了 winapi 的“std”和“winnt”功能,则将构建 winapi 并启用所有这四个功能。

winapi features example

这样做的结果是功能应该是累加的。也就是说,启用功能不应禁用功能,并且通常可以安全地启用任何功能组合。功能不应引入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)将仅使用默认特性集运行。

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