写过 Rust FFI 的人都踩过这个坑——自定义 ABI 函数每次都要套 extern “C”,这其实是 UB。今天 Rust 把这件事从根上原生化了
有些底层场景绕不开自定义调用约定(calling convention)。
比如 ARM 的 __aeabi_uidivmod,函数把被除数放在 r0 寄存器、除数放在 r1 寄存器,商和余数也通过寄存器返回——完全不遵守标准 C 调用约定。再比如 Linux 内核的 __fentry__,是 mcount 性能分析机制的入口点,也有自己独特的寄存器约定。
以前写 Rust 的做法是直接 extern "C":
“`rust
// 旧写法 —— 其实是不正确的!
extern “C” {
fn __aeabi_uidivmod();
fn fentry();
}
“`
这样写 Rust 会假设这些函数遵守 C 调用约定,但实际上它们不遵守。在编译器眼里这叫”未定义行为”(UB)——程序碰巧能跑,但答案是错的,是 UB,只是没人报错而已。
这个问题被 RFC 3980 和 extern "custom" 这个新 ABI 彻底修好了。
extern "custom" 的核心理念很简单:如果 Rust 编译器不认识这个调用约定,就不应该让你直接调用它。
“`rust
// 新写法 —— 正确!
[unsafe(naked)]
pub unsafe extern “custom” fn aeabi_uidivmod() {
core::arch::naked_asm!(
“push {{lr}}”,
“sub sp, sp, #4”,
“mov r2, sp”,
“bl {trampoline}”,
“ldr r1, [sp]”,
“add sp, sp, #4”,
“pop {{pc}}”,
trampoline = sym crate::arm::udivmodsi4
);
}
unsafe extern “custom” {
fn fentry();
}
“`
extern "custom" 有三条编译器强制约束:
1. 必须套 unsafe
Rust 要求你写一段 safety 注释说清楚这个函数怎么用。
2. 必须配合 #[unsafe(naked)]
naked 函数只有汇编,不允许 Rust 代码掺和进去。
3. 不允许直接调用
编译器直接报错:error: functions with the "custom" ABI cannot be called。你要调用只能走 inline assembly,强制你用正确方式接入。
4. 不能有参数和返回值
因为编译器根本不知道参数怎么传,所以干脆不让写。
哪些场景在用
这个特性的原始推动力来自 rust-lang/compiler-builtins:Rust 标准库有时要提供一些基础函数,这些函数的调用约定是芯片架构规定的,不是 Rust 能推导出来的。以前用 extern "C" 打掩护,现在有了正式的 extern "custom"。
在 OS 内核固件和嵌入式领域,这也是常见需求——中断处理器、__fentry__ 这类插桩函数,都有自己独特的 ABI。
下一步是什么
RFC 里列了两个未来方向:一个是允许返回类型写 -> !(永不返回),这对底层场景有用;另一个是支持有参数和返回值——不过这两个都没急迫需求,先稳定 extern "custom" 本身更重要。
如果你在写嵌入式固件、OS 内核,或者需要对接裸机工具链,extern "custom" 稳定化的消息值得升个优先级。以前那个 UB 风险,现在有官方解法了。
搜索来源
- RFC 3980 原稿(github.com/rust-lang/rfcs/commit/8912d81):
extern "custom"设计规范与限制 - Tracking Issue rust-lang/rust#140829:完整设计历史与 stabilization PR
- Rust Blog「Stabilizing naked functions」(2025-07):naked functions 与 custom ABI 的关系
- Rust 1.98.1 发布说明(blog.rust-lang.org):compiler-builtins 背景说明
评论区
登录后可评论。