条件编译与 cfg

本页目录

cfg 决定某些代码是否参与构建,适合平台专用模块、可选依赖和测试辅助代码。它不同于根据程序启动环境做运行时选择,也不能代替对所有支持配置的编译检查。

cfg 与 cfg! 的关键区别

#[cfg(unix)] fn platform()不匹配时不进入相应后续语义检查if cfg!(unix) { ... } else { ... }两个分支仍需要合法名字和类型选择可存在的代码项适合平台专用 API选择一个常量条件的分支适合两侧都能编译的表达式条件编译不等于允许被禁用区域包含任意不可解析的语法。cfg 属性移除代码;cfg! 只产生一个布尔常量
#[cfg(unix)]
fn platform_name() -> &'static str { "unix" }

#[cfg(not(unix))]
fn platform_name() -> &'static str { "other" }

fn main() {
    println!("{}", platform_name());
    let word = if cfg!(debug_assertions) { "checks" } else { "optimized" };
    println!("{word}");
}

cfg! 展开成 true/false,但其外围普通 if 的两个分支仍会被名称解析和类型检查。只有 Linux 上存在的函数不能靠 if cfg!(target_os="linux") 隐藏到其他平台;应在项或块上使用 #[cfg]。条件编译规则

#[cfg] 是语法层面的配置移除,源文件仍要能按 Rust 语法读入,不能在被禁用区域放任意损坏的 token。对于模组级平台实现,可将不同文件分别用 cfg 选择,让每个实现维护同样公共接口。

先保证分支覆盖,再实现平台差异

同一个函数的多份实现需要满足两个条件:任何受支持配置下至少存在一份,而且不会同时选中两份。最容易维护的二分是条件 P 与 not(P),而不是手工罗列“当前想到的其他平台”。

#[cfg(all(unix, target_pointer_width = "64"))]
fn implementation() -> &'static str { "unix-64" }

#[cfg(not(all(unix, target_pointer_width = "64")))]
fn implementation() -> &'static str { "portable" }

fn main() {
    assert_eq!(implementation(),
        if cfg!(all(unix, target_pointer_width = "64")) {
            "unix-64"
        } else {
            "portable"
        });
}

这里两种实现有相同签名,调用者不用重复平台判断。all 表示所有条件成立,any 表示至少一个成立,not 反转一个条件;空 all() 为真,空 any() 为假。这两个空条件也适合构造最小实验:将一个包含未定义名字的函数标为 #[cfg(any())],它不会参与名称检查;改成普通 if cfg!(any()) 则仍会报未定义名字。

按操作系统选择实现时,不要用两个重叠条件分别定义同名函数,例如 unix 和 target_os="linux" 会同时成立。按 feature 选择时同样需要考虑并集:两个用户功能可能同时启用。只有产品契约确实禁止组合,才应使用 compile_error! 拒绝该组合;可加性功能通常应允许共存。配置谓词

false 分支为何仍然报类型错误?

不要从运行时会不会进入分支推断编译时会不会检查它。下面固定 linux_only 只存在于 Linux,比较两种写法到达名称检查阶段时还剩什么。#[cfg] 真正移除了调用,cfg! 则只替换成一个布尔常量。

正在呈现知识画面
false 分支为何仍然报类型错误?

#[cfg] 移除未启用代码;if cfg! 只是布尔分支,两个分支仍需通过名称与类型检查。

常见条件按维度区分

条件表达的事实注意
target_oslinux、windows、macos 等目标系统不是构建脚本运行的宿主系统
target_archx86_64、aarch64、wasm32 等不足以代表全部 ABI
target_envgnu、musl、msvc 等目标环境有些目标的值为空
target_pointer_width指针宽度不等于所有整数或地址实现宽度
target_feature此编译目标启用的指令特性不等于任意部署机器都支持
debug_assertions对应编译开关profile 可覆盖,不等同于固定的 dev/release 标签
test该 crate 以测试方式编译集成测试依赖的普通库不因此自动拥有 cfg(test)
feature="json"Cargo 启用的当前包 feature不是 CPU 特性

查看当前目标配置:

rustc --print cfg
rustc --print cfg --target aarch64-unknown-linux-gnu

第二条查看编译器知道的目标配置,不会替你安装目标标准库或完成链接。为一个目标查到名称,也不等于该环境已能运行产物。

cfg_attr 选择属性,而不是改变运行流程

#[cfg_attr(debug_assertions, derive(Debug))]
struct State { value: u32 }

fn main() {
    let state = State { value: 7 };
    assert_eq!(state.value, 7);
    #[cfg(debug_assertions)]
    println!("{state:?}");
}

条件满足时附加 derive(Debug),否则不附加该属性。示例中的 Debug 使用点也受同一条件控制;若把 println! 放进 if cfg!(debug_assertions),关闭调试断言的构建仍会要求 State 实现 Debug。类似地可以选择链接属性、文档标注或表示属性,但若跨平台改变 repr,需要考虑 FFI、存储格式与依赖方的布局预期。

同一组条件反复出现时,可以集中到模块边界;大量散落的 cfg 可能使某个配置漏掉方法、导入或 trait 实现。编译默认配置只证明这一种分支组合有效。

编译期 CPU 特性与运行时分派

#[cfg(target_feature="avx2")] 说明当前编译环境启用了 AVX2,不是对运行机器执行检测。使用 target-cpu=native 构建后分发到不同 CPU,也可能失去兼容性。

需要通用二进制按机器选择专用实现时,可用目标平台的运行时特性检测,例如 x86 的 is_x86_feature_detected!,再调用符合 #[target_feature] 安全条件的函数,同时保留基线实现。不能先无条件执行专用指令,再用 if 判断是否支持。x86 特性检测

build.rs 生成自定义 cfg

例如某原生库是否提供 API,需要由构建脚本确定。应同时声明 cfg 的合法名字和值,使 rustc 的检查能发现拼写错误。下面用显式环境输入演示,不声称已经探测到真实系统库:

fn main() {
    println!("cargo::rustc-check-cfg=cfg(has_demo_api)");
    println!("cargo::rerun-if-env-changed=DEMO_API");
    if std::env::var("DEMO_API").as_deref() == Ok("1") {
        println!("cargo::rustc-cfg=has_demo_api");
    }
}
宿主:运行 build.rs读取 DEMO_API 或目标库信息Cargo 的目标配置TARGET / CARGO_CFG_TARGET_OSrustc-check-cfg声明合法名 has_demo_apirustc-cfg(有条件输出)真正启用 has_demo_api目标代码:合法性检查与条件选择分别发生build.rs 在宿主运行,输出影响当前包的目标代码build.rs 自己的 cfg!(target_os) 描述宿主;读取目标信息才能指导交叉构建。

将脚本保存为项目根的 build.rs,src/main.rs 使用完整的双分支接口:

#[cfg(has_demo_api)]
fn capability() -> &'static str { "enabled" }

#[cfg(not(has_demo_api))]
fn capability() -> &'static str { "fallback" }

fn main() {
    println!("{}", capability());
}

在 POSIX shell 中依次运行以下命令。第二次和第三次无需手动清理 target,环境输入变化会使脚本重新运行:

DEMO_API=0 cargo run --quiet
DEMO_API=1 cargo run --quiet
DEMO_API=0 cargo run --quiet

输出依次为 fallback、enabled、fallback。脚本明确约定只有字符串 "1" 才启用;"true"、空值和缺失值都不会启用。真实库探测通常需要区分“不支持”与“探测失败”,否则工具链缺失可能被误当作功能缺失,静默选到降级实现。

主代码使用 #[cfg(has_demo_api)]。check-cfg 只声明什么条件是合法的,不启用条件;rustc-cfg 才启用。cargo::KEY 形式需要相应 Cargo 版本支持,较旧项目可按其 MSRV 使用 cargo:KEY 形式。构建脚本输出

若检测对象是交叉目标,应检查目标库及其头文件,不能探测宿主安装的库后直接把结论写给目标。自定义 cfg 也不会自动传播给依赖它的其他 crate。

拼写检查与启用开关独立验证

把上面的 main.rs 单独复制到临时目录,可以直接验证 rustc 的两类输入:

rustc --edition 2024 --check-cfg 'cfg(has_demo_api)' -D unexpected_cfgs main.rs -o cfg-demo
./cfg-demo
rustc --edition 2024 --check-cfg 'cfg(has_demo_api)' --cfg has_demo_api -D unexpected_cfgs main.rs -o cfg-demo
./cfg-demo

第一次仅登记合法条件,打印 fallback;第二次额外启用该条件,打印 enabled。如果把源码里的名字拼成 has_demo_ap,unexpected_cfgs 会拒绝它,避免将拼写错误静默解释成“不支持”。声明所有可能名字应无条件执行,不能仅在探测成功的分支里声明,否则功能关闭时自身就会产生意外配置警告。rustc 配置检查

配置矩阵应覆盖接口组合

对一个声称支持可选功能的库,至少分别检查默认、关闭默认、单个关键功能及承诺支持的组合。all-features 仅是并集,不能代替关闭默认功能的测试;cfg(test) 的辅助导入也可能掩盖普通构建缺失。

可以结合 cargo check、cargo test 与 --target 检查不同层次。交叉 cargo check 不验证目标链接器、原生依赖或运行时行为,应准确区分这些结论。

按失败原因安排矩阵,而不是只增加命令数量

构建维度具体要发现的问题有用的检查
默认功能关闭默认导入掩盖缺失依赖、后备实现未定义cargo check --no-default-features
单个功能开启该功能错误依赖另一个功能分别 --no-default-features --features 名称
允许的功能组合重复定义、冲突实现、依赖功能合并对承诺组合分别 check/test
debug_assertions 关闭Debug 等属性的使用点未同步保护release 构建或显式覆盖该开关
自定义条件真/假后备分支、声明遗漏、输入变化未重建上面的 0 → 1 → 0 实验
不同目标平台 API、类型宽度、原生库链接目标 check,再在可用环境链接和运行

例如“json 功能 + has_demo_api”是两条独立轴,必须考虑两者都关闭、仅 json、仅 API、都开启四种配置。若实际契约允许某一轴只在特定目标出现,应把限制写清楚并验证不支持组合的诊断。不要为了让当前机器通过而人工给 rustc 注入 unix 等内建条件;这不会改变目标 ABI 或安装对应标准库。