上一篇(鸟瞰)画出了全景图——两个进程、七层 IR。这一篇停在最上面那个方框,进程 A:cargo-oxide driver。它不做任何编译,只做配置加 spawn,但把这一步看清楚才能理解 rustc 是怎么”知道”要走我们的 backend 的。
1. 命令行解析
入口在 crates/cargo-oxide/src/main.rs,用 clap 解析子命令。
cargo oxide 本身是 cargo 的 subcommand 协议:cargo 看到没见过的 oxide,就会去 PATH 里找名叫 cargo-oxide 的二进制,把后续参数透传给它。这是 cargo 老牌扩展机制——cargo edit、cargo expand、cargo nextest 全用这套。
main() 干三件事:
fn main() {
init_tracing(); // ① 装日志订阅器
let cli = Cli::parse_from(effective_args); // ② clap 解析
tracing::info!("[PHASE 1/9] cargo-oxide driver started"); // ③ 第一条日志
match cli.command {
Commands::Run { example, .. } => commands::codegen_run(...),
// ...
}
}
你看到的 [PHASE 1/9] cargo-oxide driver started 就是从这里来的。
2. 找 backend .so(关键)
codegen_run 第一件事是定位 librustc_codegen_cuda.so。crates/cargo-oxide/src/backend.rs 的 find_or_build_backend 按优先级查四个地方:
| 优先级 | 来源 | 适用场景 |
|---|---|---|
| 1 | CUDA_OXIDE_BACKEND 环境变量 | 显式覆盖,调试用 |
| 2 | 本仓库 crates/rustc-codegen-cuda/target/debug/librustc_codegen_cuda.so | 开发模式 |
| 3 | ~/.cargo/cuda-oxide/librustc_codegen_cuda.so | 用户机器缓存 |
| 4 | git clone + 现场编译 | 第一次安装 |
你的开发场景命中优先级 2:在 cuda-oxide 仓库根目录跑命令,直接走本地 build。如果 .so 已经存在且代码没变,cargo 自己会跳过编译;变了就重新编。
这就是为什么你常看到这一段日志:
Building rustc-codegen-cuda backend...
...
✓ Backend built: /home/.../librustc_codegen_cuda.so
3. 计算 RUSTFLAGS
定位完 .so 之后,需要告诉 rustc”用这个文件当后端”。靠的是 RUSTFLAGS:
// crates/cargo-oxide/src/commands.rs
let rustflags = build_rustflags(&ctx.backend_so, false);
build_rustflags 拼出来的字符串大概长这样:
-Z codegen-backend=/home/.../librustc_codegen_cuda.so
-C opt-level=3
-C embed-bitcode=no
-Z mir-enable-passes=-JumpThreading
-C symbol-mangling-version=v0
最关键的就是 -Z codegen-backend=...so——这是 rustc 的官方插件机制(nightly feature codegen_backend),告诉 rustc:
“不要用你自带的 LLVM 后端,去
dlopen这个.so,调它的__rustc_codegen_backend()函数拿到 backend 对象,让那个 backend 来做 codegen。”
不需要 fork rustc,也不需要改 rustc 源码。就是个 plugin。
关于
__rustc_codegen_backend这个入口符号怎么验证,见系列第 02 篇 确认 codegen backend 符号导出。
4. Spawn 子进程
crates/cargo-oxide/src/commands.rs 里负责 spawn 子进程的那段,核心就十几行:
let mut cmd = Command::new("cargo");
cmd.args(["run", "--release"])
.current_dir(&example_dir) // cd 到 example 目录
.env("RUSTFLAGS", &rustflags) // 注入 codegen-backend
.env("CUDA_OXIDE_TARGET", target_arch) // 告诉 backend GPU 架构 sm_61
;
forward_env_var(&mut cmd, "RUST_LOG"); // 让子进程也能受 RUST_LOG 控制
let status = cmd.status().expect("Failed to run cargo");
然后 cargo run --release 在 crates/rustc-codegen-cuda/examples/hello_constant 目录里启动——就是普通 cargo 走 build → run 流程,但因为环境里有 RUSTFLAGS=-Z codegen-backend=...,rustc 加载的就不是自带的 LLVM 后端了。
forward_env_var(&mut cmd, "RUST_LOG") 这行很关键。它让你在 shell 里设的 RUST_LOG=info 透传给子进程,这样整条链路都受同一个日志开关控制。
5. Driver 就此挂起,等子进程退出
cargo-oxide 主进程整个挂起在 cmd.status(),被动等 child cargo 进程跑完。child 退出后,cargo-oxide 检查退出码:
if !status.success() {
eprintln!("\nFailed with exit code: {:?}", status.code());
std::process::exit(status.code().unwrap_or(1));
}
正常退出的话,cargo-oxide 在这里就直接 return 了——它不负责跑 host binary,是 child 进程里的 cargo run 自动跑的(cargo run 编译完会自动 exec 出来的 binary)。
所以你看到的 Output: 42 其实是孙进程输出的——cargo-oxide → cargo → rustc 编完之后 cargo 又 exec 出 hello_constant 二进制,二进制输出的。三层进程。
6. 为什么要两个进程
直接的疑问:能不能在 cargo-oxide 进程里直接 dlopen 那个 .so 自己干 codegen,省一层进程?
答案:不能。三个理由:
| 理由 | 解释 |
|---|---|
| rustc 是 driver,不是库 | rustc 的 -Z codegen-backend 机制是 rustc 自己调度的,必须由 rustc 进程加载 .so。绕过 rustc 自己拼一个出来要重写大量胶水代码 |
| cargo 才知道 crate graph | 编译 example 涉及 cuda-core / cuda-device / cuda-host 等好几个依赖 crate,每个都要编译。cargo 是依赖图调度器,cargo-oxide 没必要重新实现一遍 |
| 关注点分离 | cargo-oxide 只管”调用 rustc 时该带什么参数”,cargo 管”哪个 crate 该重编”,rustc 管”每个 crate 怎么编”。三个角色各管一段,干净 |
第三条是工程美学问题,但前两条是硬约束——就算想合并进程也合并不了。
7. 一句话总结
cargo-oxide driver 进程的全部任务就是:找到 backend
.so,拼好RUSTFLAGS=-Z codegen-backend=...so,启动cargo run --release子进程,然后挂起等它退出。它不做任何编译,不接触 MIR,不知道 PTX 是什么。它只是个”配置 + spawn”的薄层——但正是这个薄层把 rustc 的官方插件机制串通,让后面所有故事得以发生。
下一篇会进入子进程,看 rustc 的前端流程——从 src/main.rs 文本一步步变成 MIR,我们 backend 拿到的”原料”长什么样。