功能
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 标准 的字符(包括大多数字母),并且还允许以 _
或数字 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
功能,因为我们不希望用户单独启用它们,因为它们是我们 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
并启用所有这四个功能。
这样做的结果是功能应该是累加的。也就是说,启用功能不应禁用功能,并且通常可以安全地启用任何功能组合。功能不应引入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"
的解析器在一些情况下会避免统一特性,因为这种统一可能是不希望发生的。具体情况在解析器章节中进行了描述,但简而言之,它避免在以下情况下进行统一:
在某些情况下,避免统一是必要的。例如,如果一个构建依赖项启用了 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 目标。有关更多详细信息,请参阅链接的文档。
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)将仅使用默认特性集运行。
我们鼓励您考虑关于不同特性组合的策略和工具 —— 每个项目在时间、资源以及覆盖特定场景的成本效益方面都有不同的要求。常见的配置可能包括使用/不使用默认特性、特定的特性组合或所有特性组合。