如何调用外部 C 函数?
解读
在国内 Rust 岗位面试中,“调用外部 C 函数” 是高频且必答的底层能力题。面试官想确认三件事:
- 你是否真正写过 FFI(Foreign Function Interface) 代码,而不是背概念;
- 能否在 Cargo 工程 里把 C 源码/动态库无缝集成,解决 交叉编译 与 持续集成 问题;
- 是否理解 Rust 内存模型 与 C 内存模型 的差异,能写出 UB-free 的安全封装。
回答时务必给出 最小可编译示例,并主动提及 bindgen、cc、cmake、pkg-config 等国内常用工具链,否则会被判定为“纸上谈兵”。
知识点
- extern "C" 与 ABI 约定:防止 Rust 符号被 mangle,确保与 C 侧符号一一对应。
- #[link(name = …)] 与 build.rs:告诉 rustc 如何找库,国内常用 静态库 .a(减少 dll 地狱)并配合 cargo:rustc-link-search=native= 输出。
- unsafe 块:调用 C 函数必须置于 unsafe,但 unsafe 不等于放弃安全,要在上层提供 Rust 安全包装。
- 原始指针与内存布局:
*const c_char、*mut c_void、#[repr(C)]结构体,必须保证内存对齐与大小端一致。 - bindgen 自动生成:国内大厂代码库往往有海量 C 头文件,bindgen 在 build.rs 里一键生成 Rust 绑定,避免手写出错。
- panic=abort 与 unwind 策略:若 C 侧调用 Rust 回调,必须设置 panic=abort,防止跨 FFI 边界 unwind 引发 UB。
- 交叉编译与 CI:国内常用 aarch64-unknown-linux-gnu、mips-unknown-linux-musl 等目标,docker 镜像 + cargo-zigbuild 可一次性搞定。
答案
下面给出一个 可在国产银河麒麟 x86_64 与 aarch64 上直接编译运行 的完整示例,目录结构符合国内工程规范:
ffi_demo/
├── Cargo.toml
├── build.rs
├── src/
│ └── main.rs
├── c_src/
│ ├── mathlib.c
│ └── mathlib.h
c_src/mathlib.h
#ifndef MATHLIB_H
#define MATHLIB_H
int add(int a, int b);
#endif
c_src/mathlib.c
#include "mathlib.h"
int add(int a, int b) { return a + b; }
build.rs(国内镜像加速 + 静态库)
fn main() {
// 使用 cc crate 编译 C 代码
cc::Build::new()
.file("c_src/mathlib.c")
.static_flag(true) // 强制静态链接,避免 .so 路径问题
.compile("mathlib"); // 输出 libmathlib.a
// 告诉 cargo 重新编译条件
println!("cargo:rerun-if-changed=c_src/mathlib.h");
println!("cargo:rerun-if-changed=c_src/mathlib.c");
}
src/main.rs
// 1. 声明外部函数
#[link(name = "mathlib", kind = "static")]
extern "C" {
fn add(a: libc::c_int, b: libc::c_int) -> libc::c_int;
}
// 2. 提供安全 Rust 包装
pub fn safe_add(a: i32, b: i32) -> i32 {
unsafe { add(a, b) }
}
fn main() {
let sum = safe_add(2, 3);
println!("2 + 3 = {}", sum); // 输出 2 + 3 = 5
}
Cargo.toml
[package]
name = "ffi_demo"
version = "0.1.0"
edition = "2021"
[dependencies]
libc = "0.2"
[build-dependencies]
cc = "1.0"
编译验证(国产麒麟 aarch64 示例)
$ cargo build --target aarch64-unknown-linux-gnu --release
$ ./target/aarch64-unknown-linux-gnu/release/ffi_demo
2 + 3 = 5
拓展思考
- 双向回调:如果 C 库需要注册 Rust 函数作为回调,务必使用 extern "C" fn 并在 no_mangle 后导出;同时设置 panic=abort,防止跨语言 unwind。
- 结构体字段对齐:国内不少硬件 SDK 使用 #pragma pack(1),Rust 侧必须用 #[repr(C, packed)] 且 禁止直接借用字段,需先拷贝再使用,否则触发 UB。
- 异步场景:在 Tokio 运行时中调用阻塞式 C 库,必须用 spawn_blocking 或 block_in_place,避免污染整个调度器;若 C 库本身支持 epoll,可封装为 mio 事件源,实现 零成本异步。
- 安全审计:国内等保 2.0 要求对 FFI 边界做 内存安全审计,建议开启 -Z sanitizer=address,leak,memory 在 CI 中跑单元测试,提前发现越界与泄漏。