如何生成代码到 OUT_DIR?

解读

在国内 Rust 岗位面试中,“生成代码到 OUT_DIR” 并不是单纯问“写文件”这个动作,而是考察候选人是否理解 Cargo 的构建生命周期build.rs 的约定编译期代码生成 以及 下游 crate 如何安全地引用生成的产物。面试官希望听到:

  1. 为什么必须用 OUT_DIR,而不是 src 下随意写;
  2. 如何保证生成的代码在 增量编译、交叉编译、CI 缓存 场景下依旧正确;
  3. 如何把生成的 Rust 源码无缝引入主工程,既不污染版本库,又能被 IDE 识别
  4. 对常见坑(Windows 路径、rerun-if-changed、并发构建)有无工程级应对方案。

一句话:“能写 build.rs 只是起点,能把它写‘稳’才是加分项。”

知识点

  1. Cargo 环境变量:CARGO_MANIFEST_DIR、OUT_DIR、TARGET、PROFILE 等。
  2. build.rs 执行时机:在依赖解析之后、rustc 调用之前;标准输出仅 rerun-if 指令有效
  3. PathBuf 与跨平台:使用 std::path::PathBuf,禁止手写 "/" 拼接
  4. rerun-if-changed / rerun-if-env-changed缺省会导致 Cargo 每次全量重跑 build.rs,CI 耗时翻倍。
  5. include! 与 std::include_bytes!:前者把生成代码当模块插入,后者用于静态资源。
  6. codegen-units & 增量编译:生成文件若不在 OUT_DIR,会被 Cargo 视为源码,导致缓存失效
  7. proc-mirror vs build.rs:面试常追问“为何不用 proc-macro 动态生成”,需答 “编译期常量、无语法树依赖、构建速度更快”
  8. 国内镜像源加速:生成过程若需联网拉取 .proto 或 JSON Schema,建议先 fallback 到码云镜像,避免 GitHub 抽风导致 CI 失败。

答案

  1. 在 crate 根目录放置 build.rs,Cargo 会自动编译并执行。
  2. 在 build.rs 中通过 std::env::var("OUT_DIR") 拿到输出目录,任何生成文件必须写在这里,否则发布到 crates.io 时会因源码目录被污染而遭拒。
  3. 生成代码示例(以根据 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(())
}
  1. src/lib.rs 中通过 include! 宏把产物当模块引入:
include!(concat!(env!("OUT_DIR"), "/protocol_generated.rs"));
  1. 若生成的是 静态数组(如打包 WASM 字节码),改用 include_bytes! 直接嵌入只读段,避免运行时读文件
  2. 国内 CI 场景,务必在 build.rs 里先检测 TARGET 是否为 wasm32-unknown-unknown,再决定生成内容,防止交叉编译时把 x86_64 的汇编头文件打进 WASM。
  3. 最后 cargo build --verbose 观察输出,确认 rerun-if 生效,增量编译时间应 <1 s;若每次全量重跑,面试直接扣分。

拓展思考

  1. 大规模代码生成如何提速?
    把生成任务拆成 多进程 + 消息队列,在 build.rs 里只负责 收集 rerun-if 并写桩文件,真正生成放到 build-dependencies 里的独立二进制,利用 sccache 缓存 LLVM IR,国内大厂 Rust 单体仓库常用此方案,可将 10 分钟构建降到 90 秒

  2. 生成的代码需要文档怎么办?
    在 build.rs 中同时写出 #[doc(hidden)]re-export 模块,并 touch .rustdoc-hash,让 docs.rs 国内镜像 也能正确渲染;否则用户在线文档一片空白,会被社区吐槽“不可维护”

  3. 与 Nix/Guix reproducible build 的冲突
    OUT_DIR 的绝对路径会被 file!() 宏捕获,导致构建产物 hash 不一致;需在生成代码里把 #![cfg_attr(rustfmt, rustfmt::skip)] 和 #[allow(clippy::all)] 加上,并在 CI 里 strip 掉绝对路径前缀否则国内用 Nix 做供应链安全审计时会直接判为“不可重现”