Rust使用feature特性和条件编译,以及常用feature使用说明

Cargo Feature 是非常强大的机制,可以为大家提供条件编译和可选依赖的高级特性,可以为你省下不少的代码量来判断操作系统和条件编译等功能。rust官方条件编译文档:Conditional compilation - The Rust Reference

features特性

Featuure 可以通过 Cargo.toml 中的 [features] 部分来定义:其中每个 feature 通过列表的方式指定了它所能启用的其他 feature 或可选依赖。

假设我们有一个 2D 图像处理库,然后该库所支持的图片格式可以通过以下方式启用:

rust 复制代码
[features]# 定义一个feature并启动: webp, 但它并没有启用其它 featurewebp = []

当定义了 webp 后,我们就可以在代码中通过 cfg 表达式来进行条件编译(cfg表达式讲解说明:xiaoshen.blog.csdn.net/article/det...)。例如项目中的 lib.rs 可以使用以下代码对 webp 模块进行条件引入:

rust 复制代码
#[cfg(feature = "webp")]pub mod webp;

#[cfg(feature = "webp")] 的含义是:只有在 webp feature 被定义后,以下的 webp 模块才能被引入进来。由于我们之前在 [features] 里定义了 webp,因此以上代码的 webp 模块会被成功引入。

Cargo.toml 中定义的 feature 会被 Cargo 通过命令行参数 --cfg 传给 rustc,最终由后者完成编译:rustc --cfg ...。若项目中的代码想要测试 feature 是否存在,可以使用 cfg属性cfg 宏

之前我们提到了一个 feature 还可以开启其他 feature,举个例子,例如 ICO 图片格式包含 BMPPNG,因此当 ICO 图片格式被启用后,它还得确保启用 BMPPNG 格式:

rust 复制代码
[features]bmp = []png = []ico = ["bmp", "png"]webp = []

对此,我们可以理解为: bmp 和 png 是开启 ico 的先决条件。

Feature 名称可以包含来自 Unicode XID standard 定义的字母,允许使用 _ 或 0-9 的数字作为起始字符,在起始字符后,还可以使用 -、+ 或 . 。

但是我们还是推荐按照 crates.io 的方式来设置 Feature 名称 : crate.io 要求名称只能由 ASCII 字母数字、_、- 或 + 组成。

default feature

默认情况下,所有的 feature 都会被自动禁用,可以通过 default 来启用它们:

scss 复制代码
[features]default = ["ico", "webp"]bmp = []png = []ico = ["bmp", "png"]webp = []

使用如上配置的项目被构建时,default feature 首先会被启用,然后它接着启用了 icowebp feature,当然我们还可以关闭 default

  • --no-default-features 命令行参数可以禁用 default feature
  • default-features = false 选项可以在依赖声明中指定

当你要去改变某个依赖库的 default 启用的 feature 列表时(例如觉得该库引入的 feature 过多,导致最终编译出的文件过大),需要格外的小心,因为这可能会导致某些功能的缺失

系统自带feature

target_arch:目标的 CPU 架构

示例值:

rust 复制代码
"x86""x86_64""mips""powerpc""powerpc64""arm""aarch64"

target_feature:当前编译目标可用的每个平台特性

示例值:

rust 复制代码
"avx""avx2""crt-static""rdrand""sse""sse2""sse4.1"

target_os :目标操作系统

示例值:

rust 复制代码
"windows""macos""ios""linux""android""freebsd""dragonfly""openbsd""netbsd""none" (typical for embedded targets)

target_family :目标的更通用的描述,例如目标通常所属的操作系统或体系结构的系列。

示例值:

rust 复制代码
"unix""windows""wasm"

可选依赖

当依赖被标记为 "可选 optional" 时,意味着它默认不会被编译。假设我们的 2D 图片处理库需要用到一个外部的包来处理 GIF 图片:

rust 复制代码
[dependencies]gif = { version = "0.11.1", optional = true }

这种可选依赖的写法会自动定义一个与依赖同名的 feature,也就是 gif feature ,这样一来,当我们启用 gif feautre时,该依赖库也会被自动引入并启用:例如通过 --feature gif 的方式启用 feauture。

注意:目前来说,[fetuare] 中定义的 feature 还不能与已引入的依赖库同名。但是在 nightly 中已经提供了实验性的功能用于改变这一点: namespaced features

当然,我们还可以通过显式定义 feature 的方式来启用这些可选依赖库 ,例如为了支持 AVIF 图片格式,我们需要引入两个依赖包,由于 AVIF 是通过 feature 引入的可选格式,因此它依赖的两个包也必须声明为可选的:

rust 复制代码
[dependencies]ravif = { version = "0.6.3", optional = true }rgb = { version = "0.8.25", optional = true }[features]avif = ["ravif", "rgb"]

之后,avif feature 一旦被启用,那这两个依赖库也将自动被引入。

注意:我们之前也讲过条件引入依赖的方法,那就是使用平台相关的依赖,与基于 feature 的可选依赖不同,它们是基于特定平台的可选依赖。

依赖库自身的 feature

就像我们的项目可以定义 feature 一样,依赖库也可以定义它自己的 feature、也有需要启用的 feature 列表,当引入该依赖库时,我们可以通过以下方式为其启用相关的 features :

rust 复制代码
[dependencies]serde = { version = "1.0.118", features = ["derive"] }

以上配置为 serde 依赖开启了 derive feature,还可以通过 default-features = false 来禁用依赖库的 default feature :

rust 复制代码
[dependencies]flate2 = { version = "1.0.3", default-features = false, features = ["zlib"] }

这里我们禁用了 flate2default feature,但又手动为它启用了 zlib feature。

注意:这种方式未必能成功禁用 default,原因是可能会有其它依赖也引入了 flate2,并且没有对 default 进行禁用,那此时 default 依然会被启用。

除此之外,还能通过下面的方式来间接开启依赖库的 feature :

rust 复制代码
[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"]

如上所示,我们定义了一个 parallel feature,同时为其启用了 jpeg-decoder 依赖的 rayon feature。

通过命令行参数启用feature

以下的命令行参数可以启用指定的 feature :

  • --features FEATURES: 启用给出的 feature 列表,可以使用逗号或空格进行分隔,若你是在终端中使用,还需要加上双引号,例如 --features "foo bar"。 若在工作空间中构建多个 package,可以使用 package-name/feature-name 为特定的成员启用 features
  • --all-features: 启用命令行上所选择的所有包的所有 features
  • --no-default-features: 对选择的包禁用 default featue

feature同一化

feature 只有在定义的包中才是唯一的,不同包之间的 feature 允许同名。因此,在一个包上启用 feature 不会导致另一个包的同名 feature 被误启用。

当一个依赖被多个包所使用时,这些包对该依赖所设置的 feature 将被进行合并,这样才能确保该依赖只有一个拷贝存在,这个过程就被称之为同一化

这里,我们使用 winapi 为例来说明这个过程。首先,winapi 使用了大量的 features;然后我们有两个包 foobar 分别使用了它的两个 features,那么在合并后,最终 winapi 将同时启四个 features :

由于这种不可控性,我们需要让 启用feature = 添加特性 这个等式成立,换而言之,启用一个 feature 不应该导致某个功能被禁止。这样才能的让多个包启用同一个依赖的不同features。

例如,如果我们想可选的支持 no_std 环境(不使用标准库),那么有两种做法:

  • 默认代码使用标准库的,当该 no_std feature 启用时,禁用相关的标准库代码
  • 默认代码使用非标准库的,当 std feature 启用时,才使用标准库的代码

前者就是功能削减,与之相对,后者是功能添加,根据之前的内容,我们应该选择后者的做法:

rust 复制代码
#![no_std]#[cfg(feature = "std")]extern crate std;#[cfg(feature = "std")]pub fn function_that_requires_std() {    // ...}

彼此互斥的feature

某极少数情况下,features 之间可能会互相不兼容。我们应该避免这种设计,因为如果一旦这么设计了,那你可能需要修改依赖图的很多地方才能避免两个不兼容 feature 的同时启用。

如果实在没有办法,可以考虑增加一个编译错误来让报错更清晰:

rust 复制代码
#[cfg(all(feature = "foo", feature = "bar"))]compile_error!("feature \"foo\" and feature \"bar\" cannot be enabled at the same time");

当同时启用 foobar 时,编译器就会爆出一个更清晰的错误:feature foobar 无法同时启用。

总之,我们还是应该在设计上避免这种情况的发生,例如:

  • 将某个功能分割到多个包中
  • 当冲突时,设置 feature 优先级,cfg-if 包可以帮助我们写出更复杂的 cfg 表达式

检视已解析的features

在复杂的依赖图中,如果想要了解不同的 features 是如何被多个包多启用的,这是相当困难的。好在 cargo tree 命令提供了几个选项可以帮组我们更好的检视哪些 features 被启用了:

cargo tree -e features ,该命令以依赖图的方式来展示已启用的 features,包含了每个依赖包所启用的特性:

rust 复制代码
$ cargo tree -e featurestest_cargo v0.1.0 (/Users/sunfei/development/rust/demos/test_cargo)└── uuid feature "default"    ├── uuid v0.8.2    └── uuid feature "std"        └── uuid v0.8.2

cargo tree -f "{p} {f}" 命令会提供一个更加紧凑的视图:

javascript 复制代码
% cargo tree -f "{p} {f}"test_cargo v0.1.0 (/Users/sunfei/development/rust/demos/test_cargo) └── uuid v0.8.2 default,std

cargo tree -e features -i foo,该命令会显示 features 会如何"流入"指定的包 foo 中:

ruby 复制代码
cargo tree -e features -i uuiduuid v0.8.2├── uuid feature "default"│   └── test_cargo v0.1.0 (/Users/sunfei/development/rust/demos/test_cargo)│       └── test_cargo feature "default" (command-line)└── uuid feature "std"    └── uuid feature "default" (*)

该命令在依赖图较为复杂时非常有用,使用它可以让你了解某个依赖包上开启了哪些 features 以及其中的原因。

大家可以查看官方的 cargo tree 文档获取更加详细的使用信息。

Feature解析器V2版本

我们还能通过以下配置指定使用 V2 版本的解析器( resolver ):

ini 复制代码
[package]name = "my-package"version = "1.0.0"resolver = "2"

V2 版本的解析器可以在某些情况下避免 feature 同一化的发生,具体的情况在这里有描述,下面做下简单的总结:

  • 为特定平台开启的 features 且此时并没有被构建,会被忽略
  • Build-dependenciesproc-macros 不再跟普通的依赖共享 features
  • Dev-dependenciesfeatures 不会被启用,除非正在构建的对象需要它们(例如测试对象、示例对象等)

对于部分场景而言,feature 同一化确实是需要避免的,例如,一个构建依赖开启了 std feature,而同一个依赖又被用于 no_std 环境,很明显,开启 std 将导致错误的发生。

说完优点,我们再来看看 V2 的缺点,其中增加编译构建时间就是其中之一,原因是同一个依赖会被构建多次(每个都拥有不同的 feature 列表)。

由于此部分内容可能只有极少数的用户需要,因此我们并没有对其进行扩展,如果大家希望了解更多关于 V2 的内容,可以查看官方文档

构建脚本

构建脚本可以通过 CARGO_FEATURE_<name> 环境变量获取启用的 feauture 列表,其中 <name> 是 feature 的名称,该名称被转换成大全写字母,且 - 被转换为 _

required-features

该字段可以用于禁用特定的 Cargo Target:当某个 feature 没有被启用时,查看这里获取更多信息。

SemVer兼容性

启用一个 feautre 不应该引入一个不兼容 SemVer 的改变。例如,启用的 feature 不应该改变现有的 API,因为这会给用户造成不兼容的破坏性变更。 如果大家想知道哪些变化是兼容的,可以参见官方文档

总之,在新增/移除 feature 或可选依赖时,你需要小心,因此这些可能会造成向后不兼容性。更多信息参见这里,简单总结如下:

feature文档和发现

将你的项目支持的 feature 信息写入到文档中是非常好的选择:

  • 我们可以通过在 lib.rs 的顶部添加文档注释的方式来实现。例如 regex 就是这么做的
  • 若项目拥有一个用户手册,那也可以在那里添加说明,例如 serde.rs
  • 若项目是二进制类型(可运行的应用服务,包含 fn main 入口),可以将说明放在 README 文件或其他文档中,例如 sccache

特别是对于不稳定的或者不该再被使用的 feature 而言,它们更应该被放在文档中进行清晰的说明。

当构建发布到 docs.rs 上的文档时,会使用 Cargo.toml 中的元数据来控制哪些 features 会被启用。查看 docs.rs 文档获取更多信息。

如何发现features

若依赖库的文档中对其使用的 features 做了详细描述,那你会更容易知道他们使用了哪些 features 以及该如何使用。

当依赖库的文档没有相关信息时,你也可以通过源码仓库的 Cargo.toml 文件来获取,但是有些时候,使用这种方式来跟踪并获取全部相关的信息是相当困难的。

相关推荐
Asthenia041220 分钟前
Pandas全面操作指南与电商销售数据分析
后端
绝顶少年32 分钟前
Spring Boot 注解:深度解析与应用场景
java·spring boot·后端
孪生质数-1 小时前
SQL server 2022和SSMS的使用案例1
网络·数据库·后端·科技·架构
uhakadotcom1 小时前
AWS Lightsail 简介与实践
后端·面试·github
程序员鱼皮2 小时前
2025最新 Java 面经:美团后端面试真实复盘,附答案模板,速速收藏!
java·后端·面试
有来技术2 小时前
从0到1手撸企业级权限系统:基于 youlai-boot(开源) + Java17 + Spring Boot 3 完整实战
java·spring boot·后端
陈明勇3 小时前
一文掌握 MCP 上下文协议:从理论到实践
人工智能·后端·mcp
SimonKing3 小时前
因为不知道条件注解@Conditional,错失15K的Offer!
java·后端·架构