如何将 Rust 错误码映射到 C 负数?
解读
在国内做 Rust 与 C 的 FFI(Foreign Function Interface)时,**“把 Rust 的 Result 变成 C 能识别的负整数”**是面试官高频追问的点。
核心矛盾有三层:
- Rust 侧用 enum 或常量表达错误,值域通常从 0 开始;
- C 侧习惯用 < 0 表示失败,0 表示成功,>0 可能表示警告或句柄;
- ABI 边界必须保证编译器不会重新排布枚举值,且跨平台时
int32_t与c_int宽度一致。
面试官想听的不是“-1 就行”,而是**“怎么保证映射可维护、可扩展、零开销,还能让 C 用户一眼看懂”**。
知识点
- repr(C) 枚举的整型约定:不加
repr(C)的 Rust enum 不保证布局,加repr(i32)后才能与 Cint一一对应。 - 错误值域划分:把 0 留给成功,正数留给警告/句柄,负数留给错误,与 Linux errno 风格对齐。
- const 负常量的编译期折叠:Rust 支持
const ERR_XXX: i32 = -(模块基值 + 偏移),在编译期完成负数映射,无运行时开销。 - 宏或
build.rs生成映射表:国内大厂代码审查要求“禁止手写魔法数”,用build.rs扫描枚举自动生成 C 头文件,保证单一代码源。 - #[no_mangle] + extern "C" 导出函数签名:返回
c_int而非i32,避免i32与c_int在 64 位 Windows 上宽度不一致的坑。 - panic=abort 与 unwind 安全:Rust 侧必须
catch_unwind或直接用panic=abort,防止跨 FFI 边界 unwind 导致 C 进程崩溃——国内安全红线一票否决。
答案
步骤一:用 repr(i32) 定义错误枚举,并预留 0 为成功
#[repr(i32)]
pub enum MyError {
Ok = 0,
InvalidArg = -1,
Timeout = -2,
Io = -3,
}
步骤二:提供 into_c_errno 方法,保证零成本转换
impl MyError {
pub const fn into_c_errno(self) -> c_int {
self as c_int
}
}
步骤三:在 FFI 导出函数里统一使用
#[no_mangle]
pub extern "C" fn do_something(ptr: *const c_char) -> c_int {
if ptr.is_null() {
return MyError::InvalidArg.into_c_errno();
}
match unsafe { CStr::from_ptr(ptr).to_str() } {
Ok(_) => MyError::Ok.into_c_errno(),
Err(_) => MyError::InvalidArg.into_c_errno(),
}
}
步骤四:通过 build.rs 生成 C 头文件,避免手写魔法数
// build.rs
use std::io::Write;
fn main() {
let out = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap());
let mut f = std::fs::File::create(out.join("my_error.h")).unwrap();
writeln!(f, "#pragma once").unwrap();
writeln!(f, "#define MY_OK 0").unwrap();
writeln!(f, "#define MY_INVALID_ARG -1").unwrap();
writeln!(f, "#define MY_TIMEOUT -2").unwrap();
writeln!(f, "#define MY_IO -3").unwrap();
}
步骤五:在 Cargo.toml 开启 panic=abort,防止跨语言 unwind
[profile.release]
panic = "abort"
这样即可在编译期完成 Rust 错误码到 C 负数的映射,零运行时开销,且 C 用户只需包含自动生成的头文件即可。
拓展思考
- 分层错误空间:国内大型 C++ 存量系统往往把错误码拆成 “模块号 8bit + 错误号 24bit”,Rust 侧可用
#[repr(i32)]的const表达式实现const MOD_FOO: i32 = 0x02000000; const ERR_BAR: i32 = -(MOD_FOO | 123);,既兼容老协议,又保留 Rust 枚举可读性。 - 错误字符串回传:C 侧拿到负数后想打印日志,可再导出一个
extern "C" fn my_error_str(code: c_int) -> *const c_char,内部用phf静态哈希表做 O(1) 查表,避免每次分配。 - 多线程与重入:如果错误码需要携带
errno式线程局部上下文,可用std::cell::RefCell<Option<MyError>>的线程局部存储,但 FFI 边界必须保证无隐藏状态,否则审计会被打回。 - Rust 1.73+ 的
core::ffi::c_int保证:在#![no_std]嵌入式场景,同样可以用c_int做负数映射,无需依赖 std,满足国内车规与航天 Rust 试点要求。