rustc -Z codegen-backend:替换 Rust 编译器后端

rustc 的 -Z codegen-backend 是 nightly 阶段的插件机制,允许把默认 LLVM 后端换成自定义实现——Cranelift、GCC、甚至 CUDA。详细讲它的协议、加载流程、CodegenBackend trait,以及自己写一个要注意什么。

📚 系列 compiler · 第 2 篇

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_craneliftLLVMDebug 编译速度——Cranelift 比 LLVM 快 5-10 倍,优化少。cargo +nightly checkcargo run 的开发循环利器
rustc_codegen_gccLLVM用 GCC 后端,支持 LLVM 没覆盖的 target(很多 embedded 平台) + GPL 工具链
rustc_codegen_cuda (cuda-oxide)仅 device 路径#[kernel] 函数翻成 PTX,host 路径还委托给 LLVM
第三方实验性后端LLVMWebAssembly、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_middlerustc_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 onlystable 没有 -Z,永远用不了。生产环境慎用
ABI 不稳定每个 nightly 都可能改 trait 签名,后端要持续跟版本
没有完整的 CodegenBackend 文档主要靠看 rustc_codegen_llvmrustc_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. 进一步阅读

10. 一句话总结

-Z codegen-backend=...so 让 rustc 在加载到这个 flag 时 dlopen 一个动态库,调约定符号 __rustc_codegen_backend() 拿到一个 Box<dyn CodegenBackend>,把所有 codegen 工作交给它处理。整个前端(类型推断、借用检查、MIR 生成)继续由 rustc 完成,后端只接 MIR 输出目标产物——这是 Cranelift、GCC、cuda-oxide 等替代后端能存在的基础。

系列上一篇: nm 符号类型速查

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