首页 / Rust 入门教程 / FFI 与其他语言互操作

Rust 入门教程

FFI 与其他语言互操作

本教程共 78 篇 · 第 78 篇 · 更新于 2026-08-08 · 约 8 分钟阅读

RustRust 入门教程FFI外部函数接口C 互操作unsafebindgen

本节目标:理解 FFI(外部函数接口)是 Rust 与其它语言(主要是 C)对话的桥梁,学会用 extern "C" 调用 C 函数、用 #[no_mangle] pub extern "C" 导出 Rust 函数,并知道 rust-bindgen/cbindgen/cxx 这些好帮手。

Rust 还很年轻,有些领域生态不如 C/C++ 完善。当你需要用一个现成的 C 库、或者要把 Rust 嵌入到 C/C++ 项目里,就需要 FFI(Foreign Function Interface,外部函数接口)。这一章是概念性收尾,带你认全 FFI 的面貌。

1-1 为什么需要 FFI

现实里大量代码库是不同语言写的。“要用某个库,但它是别的语言写的”时,通常两条路:要么重写,要么用 FFI 直接调。重写成本高、易出错,所以 FFI 往往更现实。

典型场景:Rust 生态缺某个专业库,但 C 有成熟的;或者你有一段性能敏感的 C/C++ 老代码,想逐步用 Rust 重构——先用 FFI 把老代码接进来,再慢慢替换。除了 FFI,跨语言调用还有个替代方案:把功能做成独立服务,用 HTTP/gRPC 网络调用。但 FFI 是”同一进程内直接调用”,开销最低。

Note

不同语言叫法不同:Java 叫 JNI(Java Native Interface),Rust/C/C++ 这边就叫 FFI。本质都是”在同一进程里跨语言调用函数”。

1-2 从 Rust 调用 C 函数

调用 C 函数写在一个 extern 块里。块里的函数签名告诉 Rust”有这么个外部函数”,但 Rust 没法检查它的实现,所以调用必须 unsafe

unsafe extern "C" {
    fn abs(input: i32) -> i32;
}

fn main() {
    unsafe {
        println!("C 的 abs(-3) = {}", abs(-3));
    }
}

这里有几个要点:

  • extern "C" 里的 "C" 指定了 ABI(应用二进制接口),即函数如何被汇编层面调用。C 的 ABI 最常见、最通用,是跨语言调用的默认选择。
  • 块里只是声明函数长什么样,真正的实现在链接进来的 C 库里。
  • 调用 abs 必须包在 unsafe 中——因为别的语言不遵守 Rust 的规则,Rust 无法校验,安全责任在你。
Warning

2024 Edition 起,extern 块必须写成 unsafe extern "C" { ... },强制标出”这里是不安全边界”。这是第 77 章讲过的 2024 Edition 收紧之一。

1-3 把 Rust 函数导出给 C 调用

反过来,你也可以让 C 调用你的 Rust 函数。在 extern 函数定义处加 pub extern "C",并配 #[no_mangle]

#[no_mangle]
pub extern "C" fn call_from_c() {
    println!("刚刚 C 调用了一个 Rust 函数!");
}

#[no_mangle] 的意思是”别让我函数名被编译器改掉”(Rust 默认会改函数名以便携带更多信息,那种改名 C 端找不到)。保留原名,C 代码才能按名字链接调用。当然,要把它编译成共享库(如 .so/.dll/.dylib),再链接到 C 项目里。

1-4 数据类型要小心

FFI 的坑大多在类型上。C 和 Rust 的基础类型内存布局不一定完全一致,直接传结构体尤其危险。稳妥做法是:用两边布局一致的类型(如 c_intc_char,来自 std::os::raw),或者用 #[repr(C)] 标注结构体,强制它按 C 的内存布局排列:

#[repr(C)]
pub struct Point {
    pub x: f64,
    pub y: f64,
}

#[repr(C)] 保证这个结构体的字段排布和 C 里的 struct 一致,跨语言传结构体时才不会错位。这是 FFI 里极重要的一条纪律。

1-5 好帮手工具

手写 extern 声明容易出错,社区有工具替你生成:

  • rust-bindgen:给定 C 的头文件,自动生成调用该 C 代码的 Rust 绑定(那些 extern 声明)。再也不用手抄函数签名。
  • cbindgen:反向操作,从 Rust 代码生成 C 的头文件,方便 C 调用 Rust。
  • cxx:专门用来和 C++ 交互的库,支持双向调用,而且大部分场景不需要写 unsafe,安全性高,强烈推荐要和 C++ 打交道时用。
  • miri:Rust 官方的 UB 检测器,能跑你的 unsafe/FFI 代码,抓内存越界、释放后再用、数据竞争等问题。装法:rustup component add miri,然后 cargo miri test。注意它只能检查执行到的代码路径。
Tip

哪怕用了 bindgen 生成绑定,调用点仍是 unsafe。工具减少的是”写错签名”的风险,不是”消除 unsafe 责任”。FFI 永远是 unsafe 的主战场之一。

1-6 FFI 的安全风险与纪律

FFI 是 unsafe 的主战场,风险集中在这几处:一是类型错配,C 和 Rust 对同一类型的理解可能不同,传错就内存错位甚至崩溃;二是生命周期,C 端拿到的指针如果指向 Rust 已释放的内存,就是典型的悬垂指针;三是 panic 穿越 FFI 边界是未定义行为,所以导出给 C 的函数里绝对不能让 panic 逃出去,要在边界处 catch_unwind 或确保不 panic。

纪律就几条:跨语言传数据优先用 #[repr(C)] 结构体或 std::os::raw 里的 C 类型,别直接用 Rust 默认布局的结构体;Rust 交给 C 的指针,确保它指向的内存在 C 使用期间一直有效;导出函数内部做好错误转换,别让 Rust 的 Result/panic 直接漏给 C。把这几点守住,FFI 就没那么可怕。

Warning

FFI bug 往往表现成”偶发崩溃”或”数据悄悄错乱”,极难复现。凡是跨语言传指针,先在脑子里过一遍”这块内存谁分配、谁释放、活到什么时候”,想清楚再写。

1-7 跨语言调用的替代方案

FFI 不是跨语言协作的唯一路。如果那个库能做成独立服务(比如用 Rust 写个 HTTP 微服务或 gRPC 服务),那直接用网络调用反而省心:没有 unsafe、没有 ABI 对齐烦恼、语言完全无关。FFI 胜在零开销、同进程,适合性能敏感或无法拆服务的场景。选型时权衡”开发成本”与”运行开销”:能网络调用就别 FFI,除非性能或架构逼你这么做。

一个务实的建议:先问”能不能用纯 Rust 的库替代”。Rust 生态这几年补得很猛,以前必须 FFI 的场景,现在往往有原生 crate。真找不到、又非用不可,再上 FFI,并用 rust-bindgen 生成绑定、用 miri 验证安全性,把风险压到最低。FFI 是 Rust 通向庞大 C/C++ 世界的桥,用好了如虎添翼,用不好就是 bug 温床;谨慎、测试、工具这三件套,能让你稳妥过桥,而不是掉进未定义行为的坑里。

1-8 小结

FFI 是 Rust 与 C/C++ 在同一进程内直接对话的桥梁:用 unsafe extern "C" 声明并调用 C 函数,用 #[no_mangle] pub extern "C" 把 Rust 函数交给 C,跨语言传结构体记得 #[repr(C)]。工具上 rust-bindgen/cbindgen 自动生成绑定、cxx 让 C++ 交互更安全、miri 帮你抓 UB。到这里,我们从所有权一路走到并发、异步、宏、生态,再到 unsafe 和 FFI,Rust 入门的主干知识就全了。接下来,把这些基础打牢,去读官方文档和优秀开源项目,才是真正进阶的开始。

上一篇
unsafe Rust 与原始指针
下一篇
已经是最后一篇啦