cuda-oxide:hello-constant 拆解 02——cargo-oxide driver 进程做了什么

拆 hello-constant 系列第二站。停在全景图最上面那个方框,看 cargo-oxide driver 进程在 spawn rustc 之前到底做了哪些事——找 backend .so、拼 RUSTFLAGS、然后挂起等子进程退出。它不做编译,只做配置。

📚 系列 cuda-oxide · 第 6 篇

上一篇(鸟瞰)画出了全景图——两个进程、七层 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 editcargo expandcargo 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.socrates/cargo-oxide/src/backend.rsfind_or_build_backend 按优先级查四个地方:

优先级来源适用场景
1CUDA_OXIDE_BACKEND 环境变量显式覆盖,调试用
2本仓库 crates/rustc-codegen-cuda/target/debug/librustc_codegen_cuda.so开发模式
3~/.cargo/cuda-oxide/librustc_codegen_cuda.so用户机器缓存
4git 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 --releasecrates/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 拿到的”原料”长什么样。

系列上一篇: cuda-oxide:hello-constant 拆解 01——鸟瞰整个管线

评论区
评论功能即将上线, 敬请期待。