如何生成代码到 OUT_DIR?
解读
在国内 Rust 岗位面试中,“生成代码到 OUT_DIR” 并不是单纯问“写文件”这个动作,而是考察候选人是否理解 Cargo 的构建生命周期、build.rs 的约定、编译期代码生成 以及 下游 crate 如何安全地引用生成的产物。面试官希望听到:
- 为什么必须用 OUT_DIR,而不是 src 下随意写;
- 如何保证生成的代码在 增量编译、交叉编译、CI 缓存 场景下依旧正确;
- 如何把生成的 Rust 源码无缝引入主工程,既不污染版本库,又能被 IDE 识别;
- 对常见坑(Windows 路径、rerun-if-changed、并发构建)有无工程级应对方案。
一句话:“能写 build.rs 只是起点,能把它写‘稳’才是加分项。”
知识点
- Cargo 环境变量:CARGO_MANIFEST_DIR、OUT_DIR、TARGET、PROFILE 等。
- build.rs 执行时机:在依赖解析之后、rustc 调用之前;标准输出仅 rerun-if 指令有效。
- PathBuf 与跨平台:使用 std::path::PathBuf,禁止手写 "/" 拼接。
- rerun-if-changed / rerun-if-env-changed:缺省会导致 Cargo 每次全量重跑 build.rs,CI 耗时翻倍。
- include! 与 std::include_bytes!:前者把生成代码当模块插入,后者用于静态资源。
- codegen-units & 增量编译:生成文件若不在 OUT_DIR,会被 Cargo 视为源码,导致缓存失效。
- proc-mirror vs build.rs:面试常追问“为何不用 proc-macro 动态生成”,需答 “编译期常量、无语法树依赖、构建速度更快”。
- 国内镜像源加速:生成过程若需联网拉取 .proto 或 JSON Schema,建议先 fallback 到码云镜像,避免 GitHub 抽风导致 CI 失败。
答案
- 在 crate 根目录放置 build.rs,Cargo 会自动编译并执行。
- 在 build.rs 中通过 std::env::var("OUT_DIR") 拿到输出目录,任何生成文件必须写在这里,否则发布到 crates.io 时会因源码目录被污染而遭拒。
- 生成代码示例(以根据 protocol.json 生成 Rust 结构体为例):
use std::{env, fs, path::PathBuf};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// 1. 告诉 Cargo 何时重新运行
println!("cargo:rerun-if-changed=protocol.json");
// 2. 拿到 OUT_DIR
let out = PathBuf::from(env::var("OUT_DIR")?);
// 3. 读入模板,生成代码
let schema = fs::read_to_string("protocol.json")?;
let tokens = generate_rust_module(&schema)?; // 自定义逻辑
fs::write(out.join("protocol_generated.rs"), tokens)?;
Ok(())
}
- 在 src/lib.rs 中通过 include! 宏把产物当模块引入:
include!(concat!(env!("OUT_DIR"), "/protocol_generated.rs"));
- 若生成的是 静态数组(如打包 WASM 字节码),改用 include_bytes! 直接嵌入只读段,避免运行时读文件。
- 国内 CI 场景,务必在 build.rs 里先检测 TARGET 是否为 wasm32-unknown-unknown,再决定生成内容,防止交叉编译时把 x86_64 的汇编头文件打进 WASM。
- 最后 cargo build --verbose 观察输出,确认 rerun-if 生效,增量编译时间应 <1 s;若每次全量重跑,面试直接扣分。
拓展思考
-
大规模代码生成如何提速?
把生成任务拆成 多进程 + 消息队列,在 build.rs 里只负责 收集 rerun-if 并写桩文件,真正生成放到 build-dependencies 里的独立二进制,利用 sccache 缓存 LLVM IR,国内大厂 Rust 单体仓库常用此方案,可将 10 分钟构建降到 90 秒。 -
生成的代码需要文档怎么办?
在 build.rs 中同时写出 #[doc(hidden)] 的 re-export 模块,并 touch .rustdoc-hash,让 docs.rs 国内镜像 也能正确渲染;否则用户在线文档一片空白,会被社区吐槽“不可维护”。 -
与 Nix/Guix reproducible build 的冲突:
OUT_DIR 的绝对路径会被 file!() 宏捕获,导致构建产物 hash 不一致;需在生成代码里把 #![cfg_attr(rustfmt, rustfmt::skip)] 和 #[allow(clippy::all)] 加上,并在 CI 里 strip 掉绝对路径前缀,否则国内用 Nix 做供应链安全审计时会直接判为“不可重现”。