0. 问题
普通 Rust 库的 Cargo.toml 是这样的:
[package]
name = "my_lib"
version = "0.1.0"
[dependencies]
serde = "1.0"
但 crates/rustc-codegen-cuda/Cargo.toml 长这样:
[package]
name = "rustc_codegen_cuda"
version = "0.1.0"
edition = "2024"
# IMPORTANT: Must be dylib for rustc to load as codegen backend
[lib]
crate-type = ["dylib"]
# Keep out of workspace - requires special nightly build
[workspace]
[dependencies]
# ...
# Note: rustc internal crates are NOT listed here.
# They're accessed via `extern crate` with #![feature(rustc_private)]
# rustc will link them when loading this dylib as a codegen backend.
三处 unusual:
crate-type = ["dylib"](不是cdylib,也不是默认的rlib)- 空的
[workspace]区段 - 注释说 rustc 内部 crate 不在 dependencies 里,靠
#![feature(rustc_private)]解决
每一处都对应一个独立的设计问题。下面逐个拆。
1. crate-type = ["dylib"] ≠ cdylib
Rust 有五种 crate-type,跟动态库相关的两种长得很像但完全不同:
| 项 | dylib | cdylib |
|---|---|---|
| ABI | Rust ABI(不稳定) | C ABI(稳定) |
| 符号 | 保留 Rust 类型信息(Box、Vec、Result) | 只暴露 extern "C" 函数 |
| 给谁用 | 给别的 Rust 程序 dlopen | 给 C / C++ / Python / Node 等外部语言 dlopen |
| stdlib | 多个 dylib 共享同一份 stdlib | 每个 cdylib 自带一份 stdlib(更大) |
| 典型用法 | rustc plugin、proc-macro 宏(早期) | FFI 出去给非 Rust 调用方 |
rustc 加载我们的后端时,要调一个返回 Box<dyn CodegenBackend> 的函数:
#[unsafe(no_mangle)]
pub fn __rustc_codegen_backend() -> Box<dyn CodegenBackend> {
// ...
}
Box<dyn CodegenBackend> 是 Rust trait object——胖指针 + vtable,C ABI 根本无法表达。所以必须 dylib,让 rustc 进程和我们的 .so 共享同一份 Rust ABI 和同一份 stdlib。
这意味着我们的 backend
.so跟 rustc 二进制是严格 ABI 绑定的——换一个 nightly 版本的 rustc,我们的.so多半就 load 不进去了。所以仓库里有rust-toolchain.toml把版本钉死。
2. 空的 [workspace] 区段
# Keep out of workspace - requires special nightly build
[workspace]
写一个完全空的 [workspace] 表是一个鲜为人知的 cargo 用法,意思:
“我这个 crate 自己就是一个独立 workspace 根,不要被外层 workspace 收编。”
为什么要这样?因为这个 crate 有特殊编译需求,跟 cuda-oxide 主 workspace 的其它 crate 不兼容:
- 它必须用 nightly toolchain(普通 crate 可能能用 stable)
- 它要开
# - 它的依赖里有几个 git pin 死了特定 commit(避免 ABI 漂移)
如果不写空 [workspace],cargo 会沿着目录树往上找,自动把它纳入外层 workspace。被纳入之后:
cargo build在外层根执行时会连带编它,但环境变量(LIBRARY_PATH等)没准备好,炸- target 目录共享,版本冲突,build cache 互相污染
- rust-analyzer 跨 workspace 的 IDE 体验会乱
用一行空的 [workspace] 把它隔离出去,代价小,效果干净。这个技巧除了 rustc plugin 之外还常出现在:proc-macro 单独发布、build.rs 子项目、bootstrap 脚本等场景。
3. rustc 内部 crate 不在 dependencies 里
Cargo.toml 第 40-42 行的注释:
# Note: rustc internal crates are NOT listed here.
# They're accessed via `extern crate` with #![feature(rustc_private)]
# rustc will link them when loading this dylib as a codegen backend.
我们的 backend 代码顶部一定有这两件事:
#![feature(rustc_private)] // 解锁内部 API
extern crate rustc_driver;
extern crate rustc_middle;
extern crate rustc_codegen_llvm;
extern crate rustc_codegen_ssa;
extern crate rustc_session;
extern crate rustc_hir;
// ... 等等
三个关键点:
3.1 rustc_private 是什么
rustc 团队把 rustc 内部的 crate(rustc_middle、rustc_codegen_llvm 等)标成 #![unstable(feature = "rustc_private")]。意思:
“这些是我们的内部实现,API 随时变。我们留个 nightly 后门让你能用,但别指望它稳定。”
打开 #![feature(rustc_private)] 就是按下这个后门开关。
3.2 为什么 extern crate 而不是 [dependencies]
如果在 [dependencies] 里写 rustc_middle = "...",cargo 会去 crates.io 找——找不到,这些 crate 从来没发布过。
正确做法是 extern crate rustc_middle;——告诉 rustc”这个 crate 我用,你帮我连”。然后:
- 编译时:rustc 看到
extern crate rustc_middle,从自己的 sysroot(~/.rustup/toolchains/<nightly>/lib/rustlib/)找 - 链接时:链接器需要找到
librustc_middle-<hash>.so,这就是为什么 cargo-oxide 编译时要注入LIBRARY_PATH指向 sysroot 的 lib 目录
3.3 运行时怎么找到这些库
backend .so 编完之后,rustc dlopen 它时,rustc 自己进程里已经加载了 librustc_middle-<hash>.so 等。我们的 .so 通过同进程符号引用直接用。
这就是为什么我们的 .so 只能被 rustc 加载,不能被普通 Rust 程序加载——它依赖的符号只存在于 rustc 进程地址空间。
4. cargo-oxide 编译它时干了什么
crates/cargo-oxide/src/backend.rs:
pub fn build_backend_from_source(codegen_crate: &Path) {
println!("Building rustc-codegen-cuda backend...");
let rustc_sysroot = get_rustc_sysroot();
let lib_path = rustc_sysroot.as_ref().map(|s| format!("{}/lib", s));
let mut cmd = Command::new("cargo");
cmd.args(["build"]).current_dir(codegen_crate); // ← 就一句 cargo build
if let Some(ref path) = lib_path {
cmd.env("LIBRARY_PATH", path); // ← 关键 ①
cmd.env("LD_LIBRARY_PATH", build_ld_library_path(path)); // ← 关键 ②
}
cmd.status();
}
整段构建逻辑就是个 cargo build,多了两件事:
| 注入项 | 作用 |
|---|---|
current_dir(codegen_crate) | 切到 crates/rustc-codegen-cuda/,让 cargo 找到那个特殊的 Cargo.toml |
LIBRARY_PATH=<sysroot>/lib | 链接时找到 librustc_driver.so 等 |
LD_LIBRARY_PATH=<sysroot>/lib | 编译完跑测试或后续步骤时还能找到 |
<sysroot> 通常长这样:
~/.rustup/toolchains/nightly-2026-04-03-x86_64-unknown-linux-gnu/
里面 lib/ 目录下放着十几个 librustc_*.so。
5. 整条链路串起来
cargo oxide run hello_constant
│
▼
cargo-oxide find_or_build_backend
│
├── 命中本地 repo
│
▼
build_backend_from_source(codegen_crate)
│
├── Command::new("cargo")
│ .args(["build"])
│ .current_dir("crates/rustc-codegen-cuda/")
│ .env("LIBRARY_PATH", "<sysroot>/lib")
│ .env("LD_LIBRARY_PATH", ...)
│ .status()
│
▼
cargo build 看 Cargo.toml
│
├── [lib] crate-type = ["dylib"] ← 输出 .so 而不是 .rlib
├── [workspace] (空) ← 不被外层 workspace 收编
│
▼
rustc 编译源码
│
├── #![feature(rustc_private)] ← 解锁内部 API
├── extern crate rustc_middle 等 ← 从 sysroot 引入
├── 链接器找 librustc_*.so ← 靠 LIBRARY_PATH
│
▼
target/debug/librustc_codegen_cuda.so ← 出炉
│
▼
回到 cargo-oxide
│
▼
RUSTFLAGS="-Z codegen-backend=...so"
│
▼
spawn 第二个 cargo 进程
│
▼
rustc dlopen 这个 .so
│
├── 同进程里已经有 librustc_middle 等
├── 我们的 .so 的符号引用直接被 resolve
│
▼
dlsym("__rustc_codegen_backend") → 接管 codegen
6. 三件套的关系
这三个”不寻常设计”其实共同解决一个问题:
怎么让一个 Rust crate 既能直接用 rustc 内部 API、又能被 rustc 在运行时动态加载、又不污染外层 workspace 的常规构建?
对应关系:
| 设计 | 解决的子问题 |
|---|---|
crate-type = ["dylib"] | rustc 要 dlopen + 调返回 trait object 的函数 → Rust ABI 动态库 |
空 [workspace] | 外层 workspace 没准备好这个 crate 的特殊环境 → 隔离它 |
#![feature(rustc_private)] + extern crate | 要用 rustc 内部 crate 但它们不发布 crates.io → 走 sysroot 链接 |
少任何一个都不能工作。
一句话总结
librustc_codegen_cuda.so不是普通 Rust 库,而是一个为了被 rustc 在运行时 dlopen 而生的”特殊 dylib”——Rust ABI 而非 C ABI,跟外层 workspace 物理隔离,链接 rustc 内部 crate 而不是 crates.io 的依赖。Cargo.toml 里那三行不寻常的设计各自解决一个子问题,合起来才让这件事成立。
相关阅读: cuda-oxide:hello-constant 拆解 02——cargo-oxide driver 进程做了什么