rustc 默认用 LLVM 当代码生成后端。但编译后端不是写死的——
-Z codegen-backend这个 nightly flag 允许你在编译期换成任何符合协议的动态库。本文讲清楚:这个 flag 怎么工作、协议长什么样、现有哪些替代后端、自己想写一个该从哪起手。
0. 一句话
-Z codegen-backend=/path/to/lib.so告诉 rustc:“别用你自带的 LLVM 后端,去dlopen这个.so,把 codegen 交给它处理。“
1. 这是个什么 flag
| 项 | 值 |
|---|---|
| 名字 | -Z codegen-backend |
| 稳定性 | unstable(nightly only) |
| 引入 | rustc 1.36(2019 年) |
| 用途 | 把 rustc 的 codegen 阶段委托给一个外部动态库 |
| 类似机制 | LLVM 的 -fuse-ld=、GCC 的 plugin |
为什么是 -Z 开头?rustc 把所有 unstable flag 放在 -Z 命名空间下,需要 rustc-Z unstable-options 显式打开(nightly 默认开)。
2. 最小用法
启动 rustc 时把 .so 路径塞进 RUSTFLAGS:
RUSTFLAGS="-Z codegen-backend=/path/to/librustc_codegen_xxx.so" \
cargo +nightly build
或者写进 ~/.cargo/config.toml:
[unstable]
codegen-backend = true
[profile.dev]
codegen-backend = "cranelift"
后者是 stable cargo 用 unstable.codegen-backend = true 解锁的名字解析方式(目前只识别 "cranelift" 等少数预置名)。写 RUSTFLAGS 是最通用的方式。
3. 协议:rustc 怎么加载这个 .so
整个加载过程发生在 rustc 进程内,只有三步:
rustc 启动
│
├──▶ ① dlopen("librustc_codegen_xxx.so")
│
├──▶ ② dlsym("__rustc_codegen_backend") ← 入口符号(约定俗成)
│
└──▶ ③ 调用 __rustc_codegen_backend()
│
▼
Box<dyn CodegenBackend>
│
▼
rustc 之后所有 codegen 调用都打在这个对象上
约定的入口符号名是 __rustc_codegen_backend。它必须:
- 非 mangle(
#[no_mangle],Rust 默认会改名) - 返回
Box<dyn CodegenBackend>(trait object) - 整个 dylib 加载只调一次
最小实现:
#[unsafe(no_mangle)]
pub fn __rustc_codegen_backend() -> Box<dyn CodegenBackend> {
Box::new(MyBackend::new())
}
怎么验证
.so里这个符号被正确导出,见系列上一篇 nm 符号类型速查。
4. CodegenBackend trait
rustc_codegen_ssa::traits::CodegenBackend 是 rustc 给后端定义的接口。十几个方法,核心几个:
| 方法 | 何时调用 | 作用 |
|---|---|---|
name(&self) -> &str | 启动时 | 后端名字,用于诊断输出 |
init(&self, sess: &Session) | 每个 crate 编译前 | 初始化(注册 lints、设置 hooks 等) |
target_cpu(&self, sess) -> String | 启动时 | 报告 target CPU 字符串 |
target_config(&self, sess) -> TargetConfig | 启动时 | 报告 target 特性 |
provide(&self, providers: &mut Providers) | 启动时 | 注册 query 提供者(rustc 内部 query system) |
codegen_crate(&self, tcx, crate_info) -> Box<dyn Any> | 每个 crate 一次 | 核心:把 MIR 翻译成目标产物 |
join_codegen(&self, ongoing, sess, outputs) -> ... | 编译末尾 | 收集 codegen 输出,准备链接 |
link(&self, sess, codegen_results, outputs) | 全部编完 | 调用链接器生成最终二进制 |
写后端的 99% 工作量都在 codegen_crate。其它方法要么是引导用的辅助 API,要么可以委托给现有 LLVM 后端。
类型签名里到处是 <'tcx> 生命周期——这是 rustc 内部 arena 的 marker,跟踪绑定在 TyCtxt 上的数据。
5. 现有的替代后端
-Z codegen-backend 不是只为 cuda-oxide 发明的。社区已经有几个成熟实现:
| 后端 | 替代什么 | 主要目的 |
|---|---|---|
| rustc_codegen_cranelift | LLVM | Debug 编译速度——Cranelift 比 LLVM 快 5-10 倍,优化少。cargo +nightly check 和 cargo run 的开发循环利器 |
| rustc_codegen_gcc | LLVM | 用 GCC 后端,支持 LLVM 没覆盖的 target(很多 embedded 平台) + GPL 工具链 |
| rustc_codegen_cuda (cuda-oxide) | 仅 device 路径 | 把 #[kernel] 函数翻成 PTX,host 路径还委托给 LLVM |
| 第三方实验性后端 | LLVM | WebAssembly、JVM、自研虚拟机等 |
注意 cuda-oxide 不替换整个 LLVM 后端——它包装一个 LLVM 后端实例,只截胡 device 那部分。这是 composition 模式,不是 replacement。详见这篇博客。
6. 写一个后端要做什么
写后端不只是实现 CodegenBackend trait——你的 dylib 要能被 rustc 在运行时加载、链接、调用,涉及几件麻烦事:
6.1 crate-type 必须是 dylib(不是 cdylib)
CodegenBackend 是 Rust trait,Box<dyn CodegenBackend> 是 trait object,C ABI 表达不了。必须用 Rust ABI:
[lib]
crate-type = ["dylib"]
不能用 cdylib。两者区别详见这篇。
6.2 锁定 nightly 工具链版本
rustc 的内部 API 每个 nightly 都在变。后端编出来的 .so 跟 rustc 的版本严格 ABI 绑定——换一个 nightly 多半就 load 不进去。rust-toolchain.toml 钉死:
[toolchain]
channel = "nightly-2026-04-03"
6.3 用 #![feature(rustc_private)] 访问内部 API
rustc 内部 crate(rustc_middle、rustc_codegen_ssa 等)标了 #![unstable(feature = "rustc_private")]。你需要在 lib.rs 顶上开:
#![feature(rustc_private)]
extern crate rustc_driver;
extern crate rustc_middle;
extern crate rustc_codegen_ssa;
// ...
注意 extern crate 不是 [dependencies]——这些 crate 不在 crates.io 上发布,rustc 在加载你的 dylib 时从自己 sysroot 里链接进来。
6.4 编译时注入 LIBRARY_PATH
链接器需要找到 librustc_driver-<hash>.so 等内部库:
LIBRARY_PATH=$(rustc --print sysroot)/lib \
LD_LIBRARY_PATH=$(rustc --print sysroot)/lib \
cargo build
(cargo-oxide 帮 cuda-oxide 自动做这件事,见 这篇。)
6.5 把 crate 隔离出外层 workspace
特殊编译需求跟外层 workspace 共用 target 目录会冲突。Cargo.toml 加一行空 [workspace] 把它从外层剥离:
[workspace]
7. 一次编译里的角色
-Z codegen-backend 不影响 rustc 前端——lexer / parser / HIR / 类型检查 / borrow check / MIR 生成全部跟普通编译一样。只在 codegen 阶段才换人:
src.rs
│
▼ (这部分跟普通编译完全一样)
parsing → name resolution → HIR → type check → borrow check → MIR
│
▼ ← Z codegen-backend 在这里介入
codegen_crate(tcx)
│
├── 默认 LLVM 后端 → 机器码 / 二进制
│
└── 自定义后端 → PTX / wasm / JVM bytecode / ...
这意味着所有 Rust 语言特性(trait、生命周期、闭包、宏)自动可用——前端 rustc 已经处理好了,你只接 MIR。这是这套机制设计哲学:最大化复用 rustc,只对 codegen 这一段开放。
8. 限制 & 坑
| 限制 | 说明 |
|---|---|
| Nightly only | stable 没有 -Z,永远用不了。生产环境慎用 |
| ABI 不稳定 | 每个 nightly 都可能改 trait 签名,后端要持续跟版本 |
| 没有完整的 CodegenBackend 文档 | 主要靠看 rustc_codegen_llvm、rustc_codegen_cranelift 源码学 |
| 只能替换 codegen | 前端阶段没法 hook(你想改类型推断?要 fork rustc) |
| 错误传递机制有限 | 后端报错经 Session::dcx().fatal(...) 传出,UI 不如 rustc 自己的诊断 |
| dylib 加载失败信息差 | 符号名错、ABI 不匹配,通常只会得到一句”could not load backend” |
最后一条最痛——.so 加载失败时,99% 时间你只能猜:#[no_mangle] 漏了?nightly 版本对不上?dependencies 里写错了某个 rustc crate?先用 nm -D 验证符号是不是 T,再说别的。
9. 进一步阅读
- rustc Dev Guide:https://rustc-dev-guide.rust-lang.org/backend/index.html
- rustc_codegen_cranelift 仓库:https://github.com/rust-lang/rustc_codegen_cranelift
- rustc_codegen_gcc 仓库:https://github.com/rust-lang/rustc_codegen_gcc
- cuda-oxide 怎么用这套机制:见 cuda-oxide:hello-constant 拆解 02——cargo-oxide driver
10. 一句话总结
-Z codegen-backend=...so让 rustc 在加载到这个 flag 时 dlopen 一个动态库,调约定符号__rustc_codegen_backend()拿到一个Box<dyn CodegenBackend>,把所有 codegen 工作交给它处理。整个前端(类型推断、借用检查、MIR 生成)继续由 rustc 完成,后端只接 MIR 输出目标产物——这是 Cranelift、GCC、cuda-oxide 等替代后端能存在的基础。
系列上一篇: nm 符号类型速查