功能
Cargo 的“功能”提供了一种机制来表达 条件编译 和 可选依赖项。一个包在 Cargo.toml
的 [features]
表中定义了一组命名功能,每个功能可以启用或禁用。要构建的包的功能可以通过命令行标志(如 --features
)启用。依赖项的功能可以在 Cargo.toml
中的依赖项声明中启用。
注意:现在发布到 crates.io 的新包或版本最多只能包含 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 标准 的字符(包括大多数字母),此外还允许以 _
或数字 0
到 9
开头,并且在第一个字符之后还可以包含 -
、+
或 .
。
注意: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
功能将启用列出的两个依赖项。这也避免了创建隐式 ravif
和 rgb
功能,因为我们不希望用户单独启用它们,因为它们是我们的包的内部细节。
注意:另一种可选地包含依赖项的方法是使用 特定于平台的依赖项。与使用功能不同,这些依赖项是根据目标平台有条件地确定的。
依赖项功能
可以在依赖项声明中启用依赖项的功能。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
包,它使用 大量 特性。 如果您的包依赖于一个启用 winapi
的“fileapi”和“handleapi”特性的包 foo
,以及另一个启用 winapi
的“std”和“winnt”特性的依赖项 bar
,那么 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"
解析器在一些情况下避免统一特性,在这些情况下,这种统一可能是不可取的。 确切的情况在 解析器章节 中描述,但简而言之,它在以下情况下避免统一
- 在当前未构建的目标上为 特定于平台的依赖项 启用的特性将被忽略。
- 构建依赖项 和 proc-macros 不与普通依赖项共享特性。
- 开发依赖项 除非构建需要它们的 target(如测试或示例),否则不会激活特性。
在某些情况下,避免统一是必要的。 例如,如果构建依赖项启用了 std
特性,并且相同的依赖项用作 no_std
环境的普通依赖项,则启用 std
将导致构建失败。
但是,一个缺点是这可能会增加构建时间,因为依赖项被多次构建(每次使用不同的特性)。 使用版本 "2"
解析器时,建议检查多次构建的依赖项以减少整体构建时间。 如果不需要使用单独的特性构建这些重复的包,请考虑将特性添加到 依赖项声明 中的 features
列表中,以便重复项最终具有相同的特性(因此 Cargo 将仅构建它一次)。 您可以使用 cargo tree --duplicates
命令检测这些重复的依赖项。 它将显示哪些包被多次构建;查找任何以相同版本列出的条目。 有关获取已解析特性的更多信息,请参阅 检查已解析的特性。 对于构建依赖项,如果您使用 --target
标志进行交叉编译,则不需要这样做,因为在这种情况下,构建依赖项始终与普通依赖项分开构建。
解析器版本 2 命令行标志
resolver = "2"
设置还会更改 命令行选项 的 --features
和 --no-default-features
的行为。
使用版本 "1"
,您只能为当前工作目录中的包启用特性。 例如,在一个包含包 foo
和 bar
的工作区中,您位于包 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 target。 有关更多详细信息,请参阅链接的文档。
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 vendor
或 cargo-clone-crate 等工具下载源代码并检查它。
功能组合
由于功能是条件编译的一种形式,因此它们需要指数级的配置和测试用例才能实现 100% 的覆盖率。默认情况下,测试、文档和其他工具(例如 Clippy)只会在默认功能集下运行。
我们鼓励您考虑您在不同功能组合方面的策略和工具——每个项目都将根据时间、资源和覆盖特定场景的成本效益,具有不同的要求。常见的配置可能是使用/不使用默认功能、特定功能组合或所有功能组合。