Rust 调用 C++ 之静态链接
# Rust 调用 C++
在 Rust 中调用 C++(.cc
文件)中的函数是一个常见的 FFI(Foreign Function Interface)场景。由于 C++ 的名称修饰(Name Mangling)和复杂的特性(如类、模板、重载),Rust 不能直接调用 C++ 函数。
标准的解决方案是通过 C ABI 作为桥梁。具体步骤如下:
- 在 C++ 中创建一个 C 风格的包装层:使用
extern "C"
关键字,将需要暴露给 Rust 的 C++ 函数封装成 C 函数。C ABI 是稳定且规范的,Rust 可以直接理解。 - 将 C++ 代码编译成静态库或动态库:例如
.a
(archive) 或.so
(shared object) /.dll
(dynamic-link library)。 - 在 Rust 中声明 C 函数签名:同样使用
extern "C"
关键字,告诉 Rust 编译器这个函数的调用约定是 C 风格的。 - 在 Rust 中使用
unsafe
块调用函数:因为 FFI 调用无法被 Rust 的安全检查器保证内存安全,所以必须在unsafe
块中进行。 - 配置 Rust 的构建脚本 (
build.rs
):使用cc
crate 来自动编译和链接 C++ 库,这是最推荐的做法。
# 详细步骤与实例
下面我们通过一个完整的例子来演示这个过程。
目标:在 Rust 中调用一个 C++ 类的方法。
项目结构:
.
├── Cargo.toml
├── build.rs // 构建脚本
├── cpp // C++ 源代码目录
│ ├── my_class.cc
│ └── my_class.h
└── src
└── main.rs
2
3
4
5
6
7
8
# 第 1 步:编写 C++ 代码 (.h
和 .cc
)
我们将创建一个简单的 C++ 类 MyClass
,然后为它创建 C 风格的包装函数。
cpp/my_class.h
#ifndef MY_CLASS_H
#define MY_CLASS_H
class MyClass {
private:
int value;
public:
MyClass(int val);
void add(int x);
int get_value() const;
};
#endif // MY_CLASS_H
2
3
4
5
6
7
8
9
10
11
12
13
14
cpp/my_class.cc
#include "my_class.h"
#include <iostream>
// C++ 类的实现
MyClass::MyClass(int val) : value(val) {
std::cout << "C++: MyClass instance created with value " << this->value << std::endl;
}
void MyClass::add(int x) {
this->value += x;
std::cout << "C++: add(" << x << ") called. New value: " << this->value << std::endl;
}
int MyClass::get_value() const {
return this->value;
}
// =======================================================
// C 风格的 FFI 包装层 (The C Bridge)
// =======================================================
extern "C" {
// 将 C++ 的 MyClass* 指针类型隐藏在一个不透明的 C 指针后面
// 这样 Rust 就不需要知道 MyClass 的内部结构
typedef struct MyClass MyClass;
// 创建对象的函数
MyClass* my_class_new(int val) {
return new MyClass(val);
}
// 销毁对象的函数
void my_class_free(MyClass* ptr) {
delete ptr;
}
// 调用类方法的函数
void my_class_add(MyClass* ptr, int x) {
if (ptr) {
ptr->add(x);
}
}
// 获取值的函数
int my_class_get_value(MyClass* ptr) {
if (ptr) {
return ptr->get_value();
}
return 0;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
关键点:
extern "C"
块告诉 C++ 编译器,这部分代码要按照 C 语言的规则进行编译(即关闭名称修饰)。- 我们通过
new
和delete
来手动管理 C++ 对象的生命周期,并将 C++ 对象指针MyClass*
在 C 接口中传递。对于 Rust 来说,这只是一个不透明的指针。
# 第 2 步:配置构建脚本 (build.rs
)
我们需要 cc
crate 来帮助我们编译 C++ 代码。
Cargo.toml
在 Cargo.toml
中添加 build-dependencies
。
[package]
name = "rust-calls-cpp-example"
version = "0.1.0"
edition = "2021"
[dependencies]
# 添加构建依赖
[build-dependencies]
cc = "1.0"
2
3
4
5
6
7
8
9
10
build.rs
在项目根目录下创建 build.rs
文件。
fn main() {
cc::Build::new()
.cpp(true) // 启用 C++ 编译
.file("cpp/my_class.cc") // 指定要编译的 C++ 文件
.compile("my_class_lib"); // 编译成名为 libmy_class_lib.a 的静态库
println!("cargo:rerun-if-changed=cpp/my_class.cc");
println!("cargo:rerun-if-changed=cpp/my_class.h");
}
2
3
4
5
6
7
8
9
这个脚本会在 cargo build
时自动执行:
- 调用系统中的 C++ 编译器(如 g++ 或 clang++)。
- 将
cpp/my_class.cc
编译成一个静态库libmy_class_lib.a
。 - 自动将这个库链接到最终的 Rust 可执行文件中。
# 第 3 步:在 Rust 中声明和调用函数
现在,我们在 src/main.rs
中编写 Rust 代码来调用这些 C 函数。
src/main.rs
use std::os::raw::{c_int, c_void};
// 定义一个不透明的结构体来代表 C++ 中的 MyClass*
// Rust 不知道它的内部布局,只知道它是一个指针
#[repr(C)]
pub struct MyClass {
_private: [u8; 0],
}
// 使用 extern "C" 块来声明 C 接口函数
// 函数签名必须与 C++ 包装层中的完全匹配
#[link(name = "my_class_lib")] // 虽然 cc crate 会自动链接,但显式声明也可以
unsafe extern "C" {
fn my_class_new(val: c_int) -> *mut MyClass;
fn my_class_free(ptr: *mut MyClass);
fn my_class_add(ptr: *mut MyClass, x: c_int);
fn my_class_get_value(ptr: *mut MyClass) -> c_int;
}
// =======================================================
// 创建一个安全的、符合 Rust 风格的包装器 (Safe Wrapper)
// 这是最佳实践,将 unsafe 代码限制在一个小的模块里
// =======================================================
struct MyClassWrapper {
ptr: *mut MyClass,
}
impl MyClassWrapper {
fn new(val: i32) -> Self {
// unsafe 代码块,因为我们在调用 FFI
let ptr = unsafe { my_class_new(val as c_int) };
assert!(!ptr.is_null(), "Failed to create MyClass instance");
Self { ptr }
}
fn add(&mut self, x: i32) {
unsafe {
my_class_add(self.ptr, x as c_int);
}
}
fn get_value(&self) -> i32 {
unsafe { my_class_get_value(self.ptr) as i32 }
}
}
// 实现 Drop trait 来自动管理 C++ 对象的生命周期 (RAII)
// 当 MyClassWrapper 离开作用域时,drop 会被调用,从而释放 C++ 对象
impl Drop for MyClassWrapper {
fn drop(&mut self) {
println!("Rust: Dropping MyClassWrapper, freeing C++ object.");
unsafe {
my_class_free(self.ptr);
}
}
}
fn main() {
println!("Rust: Calling C++ code...");
let mut my_object = MyClassWrapper::new(10);
println!("Rust: Initial value from C++: {}", my_object.get_value());
my_object.add(5);
println!("Rust: Value after adding 5: {}", my_object.get_value());
// my_object 在 main 函数结束时离开作用域,它的 drop 方法会自动被调用
// 无需手动调用 my_class_free
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# 第 4 步:编译和运行
现在,只需要在项目根目录下运行 cargo run
即可。
$ cargo run
Compiling rust-calls-cpp-example v0.1.0 (...)
Finished dev [unoptimized + debuginfo] target(s) in ...
Running `target/debug/rust-calls-cpp-example`
Rust: Calling C++ code...
C++: MyClass instance created with value 10
Rust: Initial value from C++: 10
C++: add(5) called. New value: 15
Rust: Value after adding 5: 15
Rust: Dropping MyClassWrapper, freeing C++ object.
2
3
4
5
6
7
8
9
10
# 自动化工具:bindgen
对于大型的 C/C++ 库,手动编写 extern "C"
声明非常繁琐且容易出错。这时可以使用 bindgen
工具。
bindgen
可以在构建时自动从 C/C++ 头文件生成 Rust FFI 声明。
使用 bindgen
的步骤:
添加依赖:
# Cargo.toml [build-dependencies] cc = "1.0" bindgen = "0.69" # 使用较新版本
1
2
3
4修改
build.rs
:// build.rs use std::env; use std::path::PathBuf; fn main() { // 1. 编译 C++ 库 (和之前一样) cc::Build::new() .cpp(true) .file("cpp/my_class.cc") .compile("my_class_lib"); // 2. 使用 bindgen 生成 Rust FFI 绑定 println!("cargo:rerun-if-changed=cpp/my_class.h"); let bindings = bindgen::Builder::default() // 提供 C/C++ 头文件 .header("cpp/my_class.h") // 只生成我们 extern "C" 的 C 风格函数 .allowlist_function("my_class_.*") // 告诉 cargo 何时重新运行 .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) .generate() .expect("Unable to generate bindings"); // 将生成的绑定写入 $OUT_DIR/bindings.rs let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); bindings .write_to_file(out_path.join("bindings.rs")) .expect("Couldn't write bindings!"); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30在
main.rs
中包含生成的代码:// src/main.rs // 不再需要手动写 extern "C" 块了 include!(concat!(env!("OUT_DIR"), "/bindings.rs")); // 后续的 safe wrapper 和 main 函数代码保持不变...
1
2
3
4
5
bindgen
会自动为你生成所有必要的 extern "C"
块和类型定义,极大地简化了与复杂 C/C++ API 的集成工作。
# 总结
方法 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
手动编写 FFI | 简单、无需额外依赖(除了cc )、控制力强 | 繁琐、易出错、需要手动同步 C++ 和 Rust 的函数签名 | 接口简单、函数数量少 |
使用 bindgen | 自动化、不易出错、能处理复杂的 C/C++ 头文件 | 增加构建依赖和复杂性、需要配置 bindgen | 接口复杂、函数数量多、需要与大型 C/C++ 库集成 |
对于绝大多数项目,build.rs
+ cc
crate + bindgen
是最稳健和高效的解决方案。
# main.rs 代码更详细解释
# [repr(C)]
#[repr(C)]
pub struct MyClass {
_private: [u8; 0],
}
2
3
4
我们来详细解释一下这段代码中 #[repr(C)]
的含义。
简单来说,#[repr(C)]
是一个 Rust 属性(attribute),它告诉编译器:“请按照 C 语言的内存布局规则来组织这个结构体”。
这是一个在进行 FFI (Foreign Function Interface,外部函数接口) 编程时至关重要的指令。
# 为什么需要它?Rust 和 C 的区别
# 1. Rust 的默认内存布局 (#[repr(Rust)]
)
默认情况下,为了性能和内存使用效率,Rust 编译器会自由地重排结构体(struct
)或枚举(enum
)中字段的顺序。它会尝试减少内存空洞(padding),让数据更紧凑。
例如,对于这样一个结构体:
struct MyData {
a: u8, // 1 字节
b: u32, // 4 字节
c: u16, // 2 字节
}
2
3
4
5
Rust 编译器可能会将它在内存中排列成 b, c, a
的顺序(u32, u16, u8
),这样可以更好地对齐内存,减少空隙。只要你的代码完全是 Rust,这种重排就是安全且高效的。
# 2. C 的内存布局 (#[repr(C)]
)
C 语言的规范保证了结构体中字段的内存布局会严格按照你声明它们的顺序。这是一种稳定且可预测的布局。当 C/C++ 代码需要读取一个结构体时,它会假设字段就在它期望的位置。
# 在你的代码中 #[repr(C)]
的作用
你的代码目标是从 Rust 中调用 C++ 函数。这意味着数据(或者说,指向数据的指针)需要在 Rust 和 C++ 之间传递。为了让这个过程不出错,双方必须对数据的内存结构有完全相同的认知。
#[repr(C)]
正是实现这一点的关键。它禁用了 Rust 编译器的自动重排优化,强制它使用 C 语言的、可预测的布局方式。
# 针对你代码中的 MyClass
结构体
#[repr(C)]
pub struct MyClass {
_private: [u8; 0],
}
2
3
4
这里的用法非常特殊,也很有代表性。这个 MyClass
结构体在 Rust 中被称为“不透明类型”(Opaque Type)。
- 代表一个外部类型:Rust 代码本身并不知道 C++ 中
MyClass
的内部结构(有哪些字段、多大等等)。_private: [u8; 0]
是一个零大小的字段,意味着这个 Rust 结构体本身不占用任何内存。它仅仅作为一个“类型标记”存在。 - 保证指针兼容性:尽管它是一个零大小的类型,
#[repr(C)]
在这里依然至关重要。它确保了指向这个类型 (*mut MyClass
) 的指针与 C/C++ ABI (Application Binary Interface) 兼容。这保证了当 Rust 将一个*mut MyClass
指针传递给 C++ 函数时,C++ 代码能正确地将其解释为一个指向 C++MyClass
对象的指针。 - 防止错误的优化:它向编译器明确表示,这是一个用于 FFI 的特殊类型,不能被当作普通的零大小类型被优化掉,它的指针必须被视为一个真实的、有意义的内存地址。
# 总结
#[repr(C)]
的核心目的就是确保 Rust 定义的数据结构在内存中的样子与 C/C++ 中的完全一致。
- 如果你的结构体需要在 Rust 和 C/C++ 之间传递,或者被 C/C++ 代码访问,必须使用
#[repr(C)]
。 - 如果你的结构体只在 Rust 代码内部使用,则不需要加这个属性,让 Rust 编译器自由优化以获得最佳性能即可。
在提供的代码中,它是实现 Rust 与 C++ 安全互操作的基石。
# [link(name = "my_class_lib")]
#[link(name = "my_class_lib")] // 虽然 cc crate 会自动链接,但显式声明也可以
unsafe extern "C" {
fn my_class_new(val: c_int) -> *mut MyClass;
fn my_class_free(ptr: *mut MyClass);
fn my_class_add(ptr: *mut MyClass, x: c_int);
fn my_class_get_value(ptr: *mut MyClass) -> c_int;
}
2
3
4
5
6
7
我们来逐行、逐个概念地详细解释这段代码。
这段代码是 Rust FFI (Foreign Function Interface,外部函数接口) 的核心部分。它的作用是在 Rust 代码中声明一系列由外部 C 或 C++ 库(在这里是 C++)提供的函数,以便 Rust 代码能够调用它们。
可以把它想象成 C/C++ 中的头文件 (.h
)。头文件只包含函数的声明(函数名、参数、返回值),而没有具体的实现。这个 extern "C"
块在 Rust 中扮演着完全相同的角色。
# 代码分解
// #[link(name = "my_class_lib")]
// extern "C" {
// ...
// }
2
3
4
我们来分析这里的每一个关键字和符号:
# 1. extern "C"
- “使用 C 语言的调用约定”
extern
: 这个关键字告诉 Rust 编译器,块内部声明的函数是外部的(external),它们的定义(实现)位于当前 Rust 代码之外的某个地方。编译器在编译时不需要找到这些函数的实现,但它需要相信这些函数是存在的。"C"
: 这是最重要的部分,它指定了 ABI (Application Binary Interface,应用程序二进制接口)。可以把它理解为函数调用的“通信协议”。- 为什么是 "C"? C 语言的 ABI 是跨语言编程的通用标准。几乎所有主流语言(C++, Python, Java, Go, Rust 等)都知道如何调用 C 函数。
- 它规定了什么? ABI 规定了函数调用的底层细节,例如:
- 函数参数是如何传递的(是通过 CPU 寄存器还是压入栈中)。
- 参数的传递顺序。
- 返回值是如何传回的。
- 函数名在编译后如何表示(C 语言不会像 C++ 那样进行复杂的“名字修饰”)。
- 通过指定
"C"
ABI,我们确保了 Rust 编译器和 C++ 编译器在如何调用这些函数上达成了一致,从而避免了混乱和崩溃。
# 2. #[link(name = "my_class_lib")]
- “链接到这个库”
- 这是一个属性 (Attribute),它向链接器 (Linker) 发出指令。
- 编译和链接:程序生成分为两步。第一步,编译器将源码(如
.rs
或.cpp
文件)编译成中间的目标文件(.o
或.obj
)。第二步,链接器将这些目标文件和所需的库文件组合起来,生成最终的可执行文件。 - 作用:这个属性告诉链接器:“当你在寻找
my_class_new
,my_class_free
等函数的具体实现时,请到名为my_class_lib
的库里面去找。” - 注释的解释:
// 虽然 cc crate 会自动链接,但显式声明也可以
。在现代 Rust 项目中,我们通常使用一个名为cc
的 crate(库)在构建脚本 (build.rs
) 中自动编译 C/C++ 源码。cc
crate 会自动处理链接步骤,因此#[link]
属性有时是多余的。但写上它可以让代码意图更清晰,或者在不使用构建脚本的简单场景中是必需的。
# 3. 函数签名 (Function Signatures)
fn my_class_new(val: c_int) -> *mut MyClass;
fn my_class_free(ptr: *mut MyClass);
fn my_class_add(ptr: *mut MyClass, x: c_int);
fn my_class_get_value(ptr: *mut MyClass) -> c_int;
2
3
4
这些是函数声明,而不是定义。它们是对 Rust 编译器的“承诺”。
fn my_class_new(...)
: 我们向编译器承诺,存在一个名为my_class_new
的外部函数。- 类型:
c_int
: 这是来自std::os::raw
模块的类型别名,它代表了与目标平台 C 语言中的int
类型相匹配的 Rust 类型。使用c_int
而不是i32
是为了保证跨平台兼容性。*mut MyClass
: 这是一个原始的可变指针 (raw mutable pointer)。它等同于 C/C++ 中的MyClass*
。它不受 Rust 的借用检查器和生命周期规则的保护,因此操作这种指针必须在unsafe
块中进行。这是 FFI 中不可避免的一部分,因为 Rust 无法保证外部 C/C++ 代码的内存安全。
# 总结
总的来说,这个代码块是在 Rust 和 C++ 之间搭建的一座“桥梁”的蓝图。它告诉 Rust 编译器:
- 存在哪些外部函数:
my_class_new
,my_class_free
等。 - 如何安全地调用它们:使用 C 语言的 ABI (
extern "C"
)。 - 它们的参数和返回值类型是什么:使用与 C 兼容的类型,如
c_int
和原始指针*mut T
。 - 去哪里找它们的实现:在链接阶段去
my_class_lib
库里寻找 (#[link(...)]
)。
有了这个“蓝图”,Rust 编译器就能在你调用这些外部函数时进行类型检查,确保你传递了正确数量和类型的参数,从而在编译期就捕获大量潜在的错误。但是,由于这些函数来自外部世界,Rust 无法保证它们的内部实现是安全的,所以调用它们的操作最终必须被标记为 unsafe
。