Rust学习之Features
-
- [一 什么是 Features](#一 什么是 Features)
- [二 默认 feature](#二 默认 feature)
- [三 简单的features应用示例](#三 简单的features应用示例)
- [四 可选(optional)的依赖](#四 可选(optional)的依赖)
- [五 依赖的特性](#五 依赖的特性)
-
- [5.1 在依赖表中指定](#5.1 在依赖表中指定)
- [5.2 在features表中指定](#5.2 在features表中指定)
- [六 命令行中特性控制](#六 命令行中特性控制)
- [七 特性统一路径](#七 特性统一路径)
- [八 其它](#八 其它)
-
- [8.1 相互排斥特性](#8.1 相互排斥特性)
- [8.2 观察启用特性](#8.2 观察启用特性)
- [8.3 Feature resolver version 2](#8.3 Feature resolver version 2)
本文是学习Solana 程序库合约(SPL)的Rust 预先知识部分,需要有Rust基础
本文学习课程为https://doc.rust-lang.org/cargo/reference/features.html 。下面的内容为一些简单记录。
下面的内容中,feature和特性会交叉出现,但是均指同一概念。
一 什么是 Features
Features 是用来表达条件编译或者条件依赖的机制。
定义在Cargo.toml
中的[features]
表中的features 可以启用或者不启用。在构建时通过命令行参数--features
来启用需要的特性,作为依赖启用特性时,直接在Cargo.toml
中定义。
基本的features
块定义为:
toml
[features]
# Defines a feature named `webp` that does not enable any other features.
webp = []
在Rust中使用webp特性的代码示例:
rust
// This conditionally includes a module which implements WEBP support.
#[cfg(feature = "webp")]
pub mod webp;
// 下面是不启用"no-entrypoint"才包含entrypoint模块的定义
#[cfg(not(feature = "no-entrypoint"))]
mod entrypoint;
特性可以包含其它特性
toml
[features]
bmp = []
png = []
ico = ["bmp", "png"]
webp = []
// 注意,下面bmp和png的定义顺序任意,可以放在ico的下面
二 默认 feature
默认程序时不启用任何特性的,但是我们可以定义程序默认启用的features
toml
[features]
default = ["ico", "webp"]
bmp = []
png = []
ico = ["bmp", "png"]
webp = []
三 简单的features应用示例
接着上面的Cargo.toml
定义,一个简单的应用示例:
rust
fn main() {
#[cfg(feature = "webp")]
println!("Hello webp!");
#[cfg(not(feature = "webp"))]
println!("Hello not webp!");
#[cfg(feature = "ico")]
println!("Hello ico!");
#[cfg(feature = "gif")]
println!("Hello gif!");
}
运行结果如下:
bash
$ cargo run
Hello not webp!
Hello ico!
$ cargo run --features webp
Hello webp!
Hello ico!
默认时仅启用了default
未启用,所以上面cargo run
输出了ico
。
注意:就算指定了--features
参数,默认特性还是会启用。关闭它有两种方法:
- 命令行参数使用
--no-default-features
- 作为依赖库,在定义时,设定
default-features = false
选项。
因此,我们如果在cargo run
时不想启用默认特性,运行如下命令:
bash
$ cargo run --features webp --no-default-features
Hello webp!
注意:原文提到了要小心默认特性设置,它通常启用了一些方便用户使用常用功能。但万一用户不想启用这些功能时,需要在所有依赖定义中限定default-features = false
.特别当一个包被多处依赖时,每处定义都要指定default-features = false
.
四 可选(optional)的依赖
依赖库也可以标记为optional
,它意味着默认情况下不会被编译。例如如下定义:
toml
[dependencies]
gif = { version = "0.11.1", optional = true }
默认时gif
依赖是未启用的,那怎么启用它呢?其实上面的定义同时隐式定义了一个gif
特性,类似如下定义:
toml
[features]
gif = ["dep:gif"]
当然,我们无需手动写出上面的定义,但是如果你不想使用默认名称(和库名相同),就得手写一个了,如果你手写了,那么隐式特性就不存在了。例如如下示例:
toml
[dependencies]
ravif = { version = "0.6.3", optional = true }
rgb = { version = "0.8.25", optional = true }
[features]
avif = ["dep:ravif", "dep:rgb"]
上面的定义中,用户只能选择avif
特性,阻止用户单独选择ravif
或者rgb
,因为也许这两者是必须同时启用的。
切记,采用上面的方式时,依赖必须是optional
。
五 依赖的特性
5.1 在依赖表中指定
在定义外部依赖的同时还可以同时指定启用的特性,例如:
toml
[dependencies]
# Enables the `derive` feature of serde.
serde = { version = "1.0.118", features = ["derive"] }
flate2 = { version = "1.0.3", default-features = false, features = ["zlib"] }
注意:上面提到过,就算指定了依赖的features
(不是feature,因为可以指定启用多个特性,所以为一数组,是带复数的s),其默认特性依然是开启的,因此我们必须手动关掉它,正如flate2
定义。当然,如果别的地方同时也用到了flate2
,那么无法保证其默认特性是关闭的,前面提到过原因。
5.2 在features表中指定
依赖的特性也可以在features
表中定义(上面的是在dependencies
表中定义),语法为package-name/feature-name
,示例如下:
toml
[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"]
可以看到,这种方式定义时,关闭默认特性还是在dependencies
表中进行。
注意当依赖为可选依赖时,package-name/feature-name
语法还会同时启用该依赖,然而有时你却不想这么做,那么可以使用如下语法
package-name?/feature-name
。这样只有在其它别处启用该依赖后定义的特性才会被启用。示例如下:
toml
[dependencies]
serde = { version = "1.0.133", optional = true }
rgb = { version = "0.8.25", optional = true }
[features]
serde = ["dep:serde", "rgb?/serde"]
上面的定义中,启用serde
特性会启用 serde 依赖库,但是只有在其它地方启用rgb
依赖库了它才会启用rgb
的serde
特性,例如我们定义了一个新的特性,其它即启用了rgb
又包含了上面定义的serde
特性。
(下面代码未验证,仅为个人猜想)
toml
[features]
serde = ["dep:serde", "rgb?/serde"]
all= ["dep:rgb","serde"]
六 命令行中特性控制
-
--features
FEATURES : 指定启用的特性,注意可以指定多个特性,它是一个列表,使用逗号或者空格分隔。如果使用空格分隔,注意在所有特性整体名称上加上双引号。例如我们接最初的toml
及程序定义,运行示例:bash$ cargo run --features webp gif Hello webp! Hello ico! $ cargo run --features "webp gif" Hello webp! Hello ico! Hello gif! // 下面webp,gif中逗号前后可以有空格 $ cargo run --features webp,gif --no-default-features Hello webp! Hello gif!
-
--all-features
: 启用所有特性 -
--no-default-features
: 不启用默认特性
七 特性统一路径
当一个依赖在多个包中使用时,Cargo
会使用所有启用特性的统一路径来标记它,从而确保只有一份单一的代码被使用(无重复代码)。
如下图所示:
此时,构建my-package
时会启用winapi
的四个特性。
特性中有一个共识是增加性,也就是启用一个特性不会关闭已有的功能,并且任意特性之间都可以联合使用。
例如 ,当你想支持no_std
环境时,不要使用no_std
特性(不要做减法),而是使用一个std
特性来启用std
库。示例代码如下:
rust
#![no_std]
#[cfg(feature = "std")]
extern crate std;
#[cfg(feature = "std")]
pub fn function_that_requires_std() {
// ...
}
上面的代码中,如果启用std
特性,就使用std
库,同时启用相应的函数。在revm
(Rust EVM实现)中能见到大量这种用法,因为区块链运行环境并不一定支持std
,所以统一使用#![no_std]
.
八 其它
8.1 相互排斥特性
通常不要这么设计(因为在确保任意特性联合都可以安全使用),但万一存在两个排斥特性时,需要进行检测并给出编译错误,例如
rust
#[cfg(all(feature = "foo", feature = "bar"))]
compile_error!("feature \"foo\" and feature \"bar\" cannot be enabled at the same time");
三种替代方案为:
- 分离不同功能到不同的包
- 使用其中一个包替换另一个
- 重构代码消除排斥性特性。
8.2 观察启用特性
cargo tree
命令可以观察哪些特性被启用了,主要有这么几种用法
cargo tree -e features
: 待研究cargo tree -f "{p} {f}"
: 待研究cargo tree -e features -i foo
: 待研究
8.3 Feature resolver version 2
特研究