在 Linux 环境里把 Rust 和 C 组合起来,常见诉求通常只有两个:一是复用现成的 C 库,二是把 Rust 模块作为库暴露给已有 C 程序。真正决定这件事能不能跑通的,不是语法表面,而是 FFI(Foreign Function Interface)和 C ABI 是否对齐。下面按调用方向拆开讲,并把最容易出问题的链接、类型、内存和错误处理放到后面集中说明,便于你判断该怎么落地。
互操作的核心:FFI 与 C ABI
Rust 和 C 在 Linux 上能互相调用,前提是双方遵守同一套调用约定。最常见的做法,就是通过 extern "C" 明确告诉 Rust 按 C ABI 组织函数调用、参数传递和符号导出。
这也是互操作成立的基础:语言可以不同,但只要二进制接口兼容,函数和数据就能对上。
Rust 调用 C:声明、链接和调用流程
Rust 调用 C,通常分成三步:先声明外部函数,再把对应的 C 库链接进来,最后在 unsafe 块里完成调用。

先用 extern "C" 声明 C 函数
在 Rust 里声明 C 函数时,要用 extern "C" 块标记,告诉编译器按 C 的 ABI 调用,避免 Rust 自己的名称修饰影响链接。
extern "C" {
fn gethostname(name: *mut c_char, len: usize) -> i32;
}
像 gethostname 这类系统或标准库函数,声明正确后就可以在 Rust 中直接调用。
静态库和动态库怎么链接
C 库常见有两种形式:静态库 .a 和动态库 .so。两种方式都能用,区别主要在构建和部署环节。
如果是静态库,可以在 build.rs 中通过 cc::Build 编译和链接,并输出类似下面的配置:
println!("cargo:rustc-link-lib=static=mylib");
如果是动态库,则可以直接通过 #[link(name = "mylib")] 指定库名:
#[link(name = "hello")]
extern "C" {
fn say_hello();
}
类型映射和字符串转换要对齐
跨语言调用时,数据类型不能“看着差不多就行”,必须一一对应。Rust 里通常使用 std::os::raw 提供的 C 兼容类型,例如 c_int 对应 C 的 int,c_char 对应 char。
字符串是最容易出错的一类数据。传给 C 的字符串通常要先转成 CString:
let c_str = CString::new("hello").unwrap();
这样可以避免中间包含空字符,影响 C 侧读取。
一个最小可跑通的示例
如果只是验证链路,最简单的方式是先写一个 C 动态库,再在 Rust 里调用它。
C 代码示例:
void say_hello() { printf("Hello from C!n"); }
编译成动态库:
gcc -shared -o libhello.so -fPIC hello.c
然后在 Rust 中声明并调用:
extern "C" {
fn say_hello();
}
unsafe {
say_hello();
}
这条路径适合接入已有 C 库,或者先把 FFI 的声明、链接和运行环境验证清楚。
C 调用 Rust:如何导出 Rust 库
反过来,如果你要把 Rust 功能嵌进现有 C 项目,重点就变成:怎样把 Rust 编译成 C 可调用的库,以及如何导出稳定符号。

导出函数时要用 #[no_mangle] 和 extern "C"
Rust 导出给 C 的函数,通常要同时满足两个条件:一是使用 extern "C" 指定 C ABI,二是使用 #[no_mangle] 防止函数名被 Rust 改写。
#[no_mangle]
pub extern "C" fn add(a: i32, b: i32) -> i32 { a + b }
同时,别忘了在 Cargo.toml 中配置 crate-type。如果输出动态库,用 cdylib;如果输出静态库,用 staticlib。
编译 Rust 库并生成产物
构建命令保持标准即可:
cargo build --release
执行后,生成的库文件会出现在 target/release 目录下,例如 librust_to_c.so。
C 侧声明、链接与运行
在 C 代码里,先按普通外部函数的方式声明 Rust 导出的接口:
extern int add(int a, int b);
编译时链接 Rust 库:
gcc -o main main.c -L./target/release -lrust_to_c
如果使用的是动态库,运行前还要确保系统能找到对应的 .so 文件,因此通常需要设置:
export LD_LIBRARY_PATH=./target/release:$LD_LIBRARY_PATH
这一步是很多“编译通过但运行失败”问题的直接原因。
真正决定稳定性的 4 个关键点
示例能跑通,只说明链路是通的;要在项目里长期使用,还得重点看下面这几个问题。

1. ABI 兼容性
Rust 和 C 默认可以通过 C ABI 协作,但只要涉及不同编译器、不同平台,或者更复杂的类型布局,就需要额外确认底层接口是否一致。这里一旦不匹配,问题通常不会在源码层直接暴露,而会变成运行时异常或难以定位的崩溃。
2. 内存管理边界
这是 FFI 最常见、也最容易埋隐患的一环。C 侧依赖手动管理内存,Rust 则由所有权系统负责释放。跨语言传递堆内存时,必须约定“谁分配、谁释放”。
例如 Rust 这边把堆对象交给外部时,可以这样处理:
let ptr = Box::into_raw(Box::new(42));
等到需要回收时,再把裸指针交还给 Rust:
unsafe { Box::from_raw(ptr); }
如果分配方和释放方不一致,或者同一块内存被重复释放,问题会非常直接:要么泄漏,要么崩溃。
3. 错误处理方式
C 风格接口通常通过返回值或错误码传达失败信息,例如返回 -1 表示出错。Rust 侧如果直接暴露更高级的逻辑,通常会先在内部把这些错误码转换成 Result:
pub fn safe_c_function() -> Result
这样既能保留底层调用方式,也能让 Rust 代码继续沿用熟悉的错误处理模型。
4. 复杂头文件优先考虑 bindgen
当 C 库接口很多、结构体复杂、宏定义又多时,手写 Rust 绑定很容易出错,也难维护。这种情况下更实际的做法,是直接用 bindgen 从头文件生成绑定代码:
bindgen wrapper.h -o src/bindings.rs
对于大型库,bindgen 往往不是“省事工具”,而是减少类型不一致和声明遗漏的必要手段。
怎么判断该走哪条互操作路径
如果你的目标是复用现成 Linux/C 生态,通常优先考虑“Rust 调用 C”;如果项目主体已经是 C,但希望把新模块用 Rust 重写并逐步接入,那么更适合“C 调用 Rust”。
无论走哪条路,真正要盯紧的都不是单个 extern "C" 语句,而是 ABI、库链接、类型映射和内存释放边界。把这几个点约束清楚,Rust 与 C 的互操作在 Linux 上就是一套可长期维护的工程方案,而不只是能跑一次的演示代码。







