【Rust】19-FFI、ABI 与跨语言边界设计
FFI、ABI 与跨语言边界设计研究目标理解 FFI 不只是语法互调还包含 ABI、所有权和错误边界。掌握 Rust 与 C 交互时的基础表示和安全约束。学会设计清晰的跨语言 API 边界。FFI 与 ABIFFI 是 foreign function interface指不同语言之间互相调用。ABI 是 application binary interface描述函数调用约定、参数传递、返回值、符号命名、类型布局等底层规则。Rust 与 C 交互时常使用外部块声明 C 函数。下面示例使用 Rust 2024 兼容写法unsafeexternC{fnabs(input:i32)-i32;}extern C表示使用 C ABIunsafe extern表示声明者必须保证外部函数签名正确。调用外部函数是不安全的因为 Rust 编译器无法验证外部代码是否满足声明。fnmain(){letvalueunsafe{abs(-3)};println!({value});}导出 Rust 函数给 CRust 函数默认符号名会被改编。要导出稳定符号可以使用#[unsafe(no_mangle)]pubexternCfnadd(a:i32,b:i32)-i32{ab}#[unsafe(no_mangle)]保留函数名extern C使用 C 调用约定。在 Rust 2024 中no_mangle属于 unsafe attribute因为导出未改编符号可能和其他符号冲突。通常还需要在Cargo.toml配置库类型[lib] crate-type [cdylib, staticlib]cdylib用于生成动态库staticlib用于静态链接。FFI 安全类型跨 FFI 边界应使用 C 兼容类型i32、u32、usize等基础整数需注意平台宽度。#[repr(C)]结构体。裸指针*const T、*mut T。C 字符串指针*const c_char。不要直接在 C ABI 中暴露String、VecT、trait object、闭包、普通 Rust enum 等 Rust 专有布局类型。#[repr(C)]pubstructPoint{pubx:i32,puby:i32,}#[unsafe(no_mangle)]pubexternCfndistance_squared(point:Point)-i32{point.x*point.xpoint.y*point.y}字符串边界C 字符串通常是以\0结尾的字节序列。Rust 的String是 UTF-8、拥有所有权且包含长度和容量。两者不能直接等同。从 C 接收字符串usestd::ffi::CStr;usestd::os::raw::c_char;pubunsafefnread_name(ptr:*constc_char)-OptionString{ifptr.is_null(){returnNone;}letc_strunsafe{CStr::from_ptr(ptr)};Some(c_str.to_string_lossy().into_owned())}这里 unsafe 前提包括指针非空、指向有效 NUL 结尾字符串、内存在调用期间有效。把 Rust 字符串传给 C 时可使用CString它保证内部没有 NUL 字节并添加结尾 NUL。所有权边界跨语言边界最容易出错的是谁分配、谁释放。一种常见设计是成对提供创建和释放函数usestd::ffi::CString;usestd::os::raw::c_char;#[unsafe(no_mangle)]pubexternCfnmake_message()-*mutc_char{CString::new(hello).unwrap().into_raw()}#[unsafe(no_mangle)]pubunsafeexternCfnfree_message(ptr:*mutc_char){if!ptr.is_null(){let_unsafe{CString::from_raw(ptr)};}}into_raw把所有权交给调用方调用方必须把指针传回free_message。不能用 C 的free释放 Rust 分配的内存。不要跨 FFI unwindRust panic 不应跨越不支持 unwind 的 FFI 边界。C 异常也不应随意穿过 Rust 栈帧。跨语言边界应把错误转成显式返回值。常见 C 风格 API#[repr(C)]pubenumStatus{Ok0,InvalidInput1,InternalError2,}#[unsafe(no_mangle)]pubexternCfndo_work(input:i32,output:*muti32)-Status{ifoutput.is_null(){returnStatus::InvalidInput;}unsafe{*outputinput*2;}Status::Ok}通过状态码和输出参数表达失败调用者不需要理解 Rust 的Result。opaque pointer 模式复杂 Rust 类型可以通过不透明指针暴露pubstructEngine{count:usize,}#[unsafe(no_mangle)]pubexternCfnengine_new()-*mutEngine{Box::into_raw(Box::new(Engine{count:0}))}#[unsafe(no_mangle)]pubunsafeexternCfnengine_increment(engine:*mutEngine){ifletSome(engine)unsafe{engine.as_mut()}{engine.count1;}}#[unsafe(no_mangle)]pubunsafeexternCfnengine_free(engine:*mutEngine){if!engine.is_null(){let_unsafe{Box::from_raw(engine)};}}C 侧只持有Engine*不知道内部布局。Rust 保留实现细节和内存管理能力。bindgen 与 cbindgen工具可以减少手写绑定bindgen根据 C 头文件生成 Rust FFI 绑定。cbindgen根据 Rust 代码生成 C/C 头文件。工具能减少机械错误但不能自动解决所有权、线程安全、错误模型和生命周期设计。边界设计仍然需要人工审查。跨语言 API 设计建议使用小而稳定的 C ABI 表面。不暴露 Rust 专有类型。明确每个指针是否可空、是否拥有所有权、是否可变。提供成对释放函数。不让 panic 或异常跨边界。把错误转成状态码或显式错误对象。对 unsafe 函数写清 safety contract。常见误解extern C只解决调用约定unsafe extern也不自动保证内存安全。#[repr(C)]只影响布局不验证字段语义安全。指针非空不代表有效也不代表对齐正确。跨语言边界不要共享隐含所有权协议。继续研究RustonomiconFFI、repr、ownership across FFI。Rust Referenceexternal blocks、ABI、type layout。bindgen 和 cbindgen 文档。C ABI、平台 calling convention 和动态链接文档。后记2026年6月11日15点30分于上海。