位置:首页 > Rust > Rust 与 C 在 Linux 系统中的互操作性:从 FFI 调用到工程落地

Rust 与 C 在 Linux 系统中的互操作性:从 FFI 调用到工程落地

时间:2026-08-23  |  作者:火苗实验室  |  阅读:0

目录

  1. 互操作的核心:FFI 与 C ABI
  2. Rust 调用 C:声明、链接和调用流程
  3. C 调用 Rust:如何导出 Rust 库
  4. 真正决定稳定性的 4 个关键点
  5. 怎么判断该走哪条互操作路径

前言

在 Linux 项目里把 Rust 和 C 组合使用,最常见的场景无非是两种:Rust 复用现成 C 库,或者把 Rust 模块嵌进已有 C 程序。要把这件事做稳,关键不只是会写 extern "C",还要弄清 ABI、链接方式、类型映射和内存释放边界。本文按这两条调用路径分别展开,并把最常见的坑单独拎出来说明,方便你判断哪种接入方式更适合自己的工程。

在 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 块里完成调用。

展示 Rust 调用 C 时,从函数声明、库链接到 unsafe 调用的完整链路信息图
Rust 调用 C 的最小闭环把 Rust 调用 C 的典型流程拆成声明、编译、链接和运行四个节点。

先用 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 的 intc_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 可调用的库,以及如何导出稳定符号。

展示 C 调用 Rust 时,导出函数、生成库文件、C 侧链接与运行环境配置的结构化信息图
C 调用 Rust 的导出与链接路径把 Rust 导出给 C 的关键条件放在一张图里,重点突出符号导出、crate。

导出函数时要用 #[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 个关键点

示例能跑通,只说明链路是通的;要在项目里长期使用,还得重点看下面这几个问题。

展示 Rust 与 C 互操作中 ABI、内存、错误处理与 bindgen 四类关键风险的对照信息图
FFI 落地时最容易出问题的 4 个点这张图不讲语法,专门用于排查 FFI 真正容易翻车的四类问题。

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 上就是一套可长期维护的工程方案,而不只是能跑一次的演示代码。

免责声明:文中图文均来自网络,如有侵权请联系删除,心愿游戏发布此文仅为传递信息,不代表心愿游戏认同其观点或证实其描述。

相关文章

更多

精选合集

更多

大家都在玩

热门话题

大家都在看

更多