从 C 调用 Rust 及 ABI 相关知识
1. 编写安全的 FFI 接口指南
在编写从 C 调用 Rust 的代码时,为了确保安全和兼容性,需要遵循以下几个重要的指南:
- 平台相关类型 :C 语言有很多平台相关的类型,如 int 和 long ,它们的长度会因平台架构而异。在与使用这些类型的 C 函数交互时,可以使用 Rust 标准库 std::raw 模块,它提供了跨平台的类型别名,例如 c_char 和 c_uint 。此外, libc crate 也为这些数据类型提供了可移植的类型别名。
- 引用和指针 :由于 C 的指针类型和 Rust 的引用类型存在差异,在跨 FFI 边界工作时,Rust 代码应使用指针类型而非引用类型。任何解引用指针类型的 Rust 代码在使用前都必须进行空检查。
- 内存管理 :每种编程语言都有自己的内存管理方式。在跨语言边界传输数据时,必须明确哪个语言负责释放内存,以避免双重释放或使用后释放的问题。建议 Rust 代码不要为直接传输到外部代码的任何类型实现 Drop 特征,使用 Copy 类型跨 FFI 边界会更安全。
- Panic 处理 :当从其他语言代码调用 Rust 代码时,必须确保 Rust 代码不会发生 Panic,或者使用 std::panic::catch_unwind 或 #[panic_handler] 等 Panic 处理机制,以确保 Rust 代码不会异常终止或返回不稳定状态。
- 将 Rust 库暴露给外部语言 :将 Rust 库及其函数暴露给外部语言(如 Java、Python 或 Ruby)时,只能通过与 C 兼容的 API 进行。
2. 从 C 调用 Rust 的项目示例
下面我们将通过一个具体的项目示例,展示如何构建一个包含 FFI 接口的 Rust 共享库,并从 C 程序中调用它。具体步骤如下:
1. 创建新的 Cargo 项目 :
cargo new --lib ffi && cd ffi
- 修改
Cargo.toml文件 :指定要构建的是共享库。
[lib]
name = "ffitest"
crate-type = ["dylib"]
- 在 Rust 中编写 FFI 接口 :在
src/lib.rs文件中编写与 C 兼容的 API。
#[no_mangle]
pub extern "C" fn see_ffi_in_action() {
println!("Congrats! You have successfully invoked
Rust shared library from a C program");
}
#[no_mangle] 注解告诉 Rust 编译器, see_ffi_in_action() 函数应能以相同的名称被外部程序访问,否则编译器会默认修改其名称。 extern "C" 关键字表示该函数与 C 代码兼容, "C" 表示目标平台上的标准 C 调用约定。
4. 构建 Rust 共享库 :在 ffi 文件夹中执行以下命令。
cargo build --release
如果构建成功,将在 target/release 目录下生成一个名为 libffitest.so 的共享库(在 Mac 平台上扩展名为 .dylib )。
5. 验证共享库是否正确构建 :使用 nm 命令检查共享库中是否包含我们编写的函数。
nm -D target/release/libffitest.so | grep see_ffi_in_action
如果输出类似 000000000005df30 T see_ffi_in_action 的结果,则表示共享库构建正确;否则,需要重新检查之前的步骤。
6. 创建 C 程序 :在 ffi 项目文件夹的根目录下创建 rustffi.c 文件,并添加以下代码。
#include "rustffi.h"
int main(void) {
see_ffi_in_action();
}
同时,在同一目录下创建 rustffi.h 文件,声明函数签名。
void see_ffi_in_action();
- 构建 C 程序 :在项目根目录下执行以下命令,指定 Rust 共享库的路径。
gcc rustffi.c -Ltarget/release -lffitest -o ffitest
命令解释:
- gcc :调用 GCC 编译器。
- -Ltarget/release :告诉编译器在 target/release 文件夹中查找共享库。
- -lffitest :指定共享库的名称为 ffitest ,编译器会自动处理 lib 前缀和 .so 后缀。
- rustffi.c :要编译的源文件。
- -o ffitest :指定生成的可执行文件名为 ffitest 。
8. 设置环境变量 :在 Linux 系统中,设置 LD_LIBRARY_PATH 环境变量,指定库的搜索路径。
export LD_LIBRARY_PATH=$(rustc --print sysroot)/lib:target/release:$LD_LIBRARY_PATH
- 运行 C 程序 :执行以下命令运行生成的可执行文件。
./ffitest
如果一切正常,终端将显示以下消息:
Congrats! You have successfully invoked Rust shared library from a C program
3. 理解 ABI
ABI(应用程序二进制接口)是编译器和链接器遵循的一组约定和标准,用于函数调用约定和指定数据布局(类型、对齐、偏移)。可以将 ABI 看作是二进制级别的 API。与源代码不同,二进制文件中的一些细节(如整数长度、填充规则、函数参数存储位置等)会因平台架构和操作系统而异。例如,64 位操作系统对于 32 位和 64 位二进制文件可能有不同的 ABI,Windows 程序无法直接访问在 Linux 上构建的库,因为它们使用不同的 ABI。
Rust 提供了一些特性来指定与 ABI 相关的参数,包括条件编译选项、数据布局约定和链接选项:
- 条件编译选项 :Rust 允许使用 cfg 宏指定条件编译选项,例如:
#[cfg(target_arch = "x86_64")]
#[cfg(target_os = "linux")]
#[cfg(target_family = "windows")]
#[cfg(target_env = "gnu")]
#[cfg(target_pointer_width = "32")]
可以将这些注解附加到函数声明上,例如:
#[cfg(all(target_os = "linux", target_arch = "x86"))]
fn do_something() { // ... }
更多关于条件编译选项的详细信息可以参考 这里 。
- 数据布局约定 :在 Rust 中,数据元素与类型、对齐和偏移相关。例如,定义一个结构体:
struct MyStruct {
member1: u16,
member2: u8,
member3: u32,
}
在 32 位(4 字节)字长的处理器上,它可能会被内部表示为:
struct Mystruct {
member1: u16,
_padding1: [u8; 2], // 使整体大小为 4 的倍数
member2: u8,
_padding2: [u8; 3], // 对齐 `member2`
member3: u32,
}
Rust 数据结构的内部布局可以使用 #[repr(Rust)] 注解,但如果数据需要通过 FFI 边界,建议使用 C 的数据布局( #[repr(C)] ),以确保数据在 FFI 边界上的兼容性。Rust 保证,如果将 #[repr(C)] 属性应用于结构体,该结构体的布局将与平台上的 C 表示兼容。可以使用 cbindgen 等自动化工具从 Rust 程序生成 C 数据布局。
- 链接选项 :使用 #[link(...)] 属性可以指示链接器链接到特定的库,以解析符号。例如:
#[link(name = "my_library")]
extern {
static a_c_function() -> c_int;
}
还可以指定链接的库的类型(静态或动态),例如:
#[link(name = "my_other_library", kind = "static")]
通过以上步骤和知识,我们可以安全地从 C 调用 Rust 代码,并理解和处理与 ABI 相关的问题。
下面是一个简单的 mermaid 流程图,展示从 C 调用 Rust 的主要步骤:
graph LR
A[创建新的 Cargo 项目] --> B[修改 Cargo.toml]
B --> C[编写 Rust FFI 接口]
C --> D[构建 Rust 共享库]
D --> E[验证共享库]
E --> F[创建 C 程序]
F --> G[构建 C 程序]
G --> H[设置环境变量]
H --> I[运行 C 程序]
| 步骤 | 操作 | 命令或代码示例 |
|---|---|---|
| 1 | 创建新的 Cargo 项目 | cargo new --lib ffi && cd ffi |
| 2 | 修改 Cargo.toml | [lib]<br>name = "ffitest"<br>crate-type = ["dylib"] |
| 3 | 编写 Rust FFI 接口 | #[no_mangle]<br>pub extern "C" fn see_ffi_in_action() {<br> println!("Congrats! You have successfully invoked <br> Rust shared library from a C program");<br>} |
| 4 | 构建 Rust 共享库 | cargo build --release |
| 5 | 验证共享库 | nm -D target/release/libffitest.so | grep see_ffi_in_action |
| 6 | 创建 C 程序 | #include "rustffi.h"<br>int main(void) {<br> see_ffi_in_action();<br>} |
| 7 | 构建 C 程序 | gcc rustffi.c -Ltarget/release -lffitest -o ffitest |
| 8 | 设置环境变量 | export LD_LIBRARY_PATH=$(rustc --print sysroot)/lib:target/release:$LD_LIBRARY_PATH |
| 9 | 运行 C 程序 | ./ffitest |
从 C 调用 Rust 及 ABI 相关知识
4. 总结与拓展
通过前面的内容,我们了解了从 C 调用 Rust 的具体操作步骤和相关的 ABI 知识。在实际应用中,这些知识可以帮助我们更好地实现不同语言之间的交互,充分发挥各种语言的优势。
下面我们来回顾一下从 C 调用 Rust 的关键步骤:
1. 创建新的 Cargo 项目。
2. 修改 Cargo.toml 文件以指定构建共享库。
3. 编写与 C 兼容的 Rust FFI 接口。
4. 构建 Rust 共享库。
5. 验证共享库是否正确构建。
6. 创建 C 程序并声明所需的函数。
7. 构建 C 程序并指定 Rust 共享库的路径。
8. 设置环境变量。
9. 运行 C 程序。
同时,我们也学习了 Rust 中与 ABI 相关的特性,这些特性可以帮助我们处理不同平台和操作系统之间的兼容性问题。
在拓展方面,我们可以进一步探索以下内容:
- 更复杂的 FFI 交互 :前面的示例只是一个简单的调用,在实际应用中,可能需要传递更复杂的数据类型,如结构体、数组等。在处理这些复杂数据类型时,需要特别注意内存管理和数据布局的兼容性。
- 多线程环境下的 FFI :在多线程环境中使用 FFI 时,需要考虑线程安全问题。例如,在 Rust 中使用 std::sync 模块提供的同步原语来确保线程安全。
- 与其他语言的交互 :除了 C 语言,Rust 还可以与其他语言(如 Java、Python 等)进行交互。不同语言之间的交互可能会有不同的实现方式和注意事项。
5. 代码示例拓展
以下是一个更复杂的示例,展示如何在 Rust 和 C 之间传递结构体数据。
Rust 代码( src/lib.rs ) :
#[repr(C)]
pub struct Point {
x: i32,
y: i32,
}
#[no_mangle]
pub extern "C" fn print_point(point: Point) {
println!("Point: ({}, {})", point.x, point.y);
}
在这个示例中,我们定义了一个 Point 结构体,并使用 #[repr(C)] 注解确保其数据布局与 C 语言兼容。然后,我们实现了一个 print_point 函数,用于打印传入的 Point 结构体的坐标。
C 代码( rustffi.c ) :
#include <stdio.h>
typedef struct {
int x;
int y;
} Point;
extern void print_point(Point point);
int main(void) {
Point p = {10, 20};
print_point(p);
return 0;
}
在 C 代码中,我们定义了一个与 Rust 中 Point 结构体相同的结构体,并调用 print_point 函数来打印结构体的坐标。
构建和运行这个示例的步骤与前面的示例类似:
1. 按照前面的步骤创建和修改 Cargo.toml 文件。
2. 编写上述 Rust 和 C 代码。
3. 构建 Rust 共享库:
cargo build --release
- 验证共享库:
nm -D target/release/libffitest.so | grep print_point
- 构建 C 程序:
gcc rustffi.c -Ltarget/release -lffitest -o ffitest
- 设置环境变量:
export LD_LIBRARY_PATH=$(rustc --print sysroot)/lib:target/release:$LD_LIBRARY_PATH
- 运行 C 程序:
./ffitest
6. 总结
通过本文,我们详细介绍了从 C 调用 Rust 的方法和相关的 ABI 知识。从编写安全的 FFI 接口指南,到具体的项目示例,再到对 ABI 的深入理解,我们逐步掌握了如何在不同语言之间进行交互。同时,我们还通过拓展代码示例展示了如何处理更复杂的数据类型。
在实际开发中,我们需要根据具体的需求和场景,灵活运用这些知识,确保不同语言之间的交互安全、高效。希望本文能帮助你更好地理解和应用 Rust 的 FFI 特性。
以下是一个 mermaid 流程图,展示从 C 调用 Rust 并传递结构体数据的主要步骤:
graph LR
A[创建新的 Cargo 项目] --> B[修改 Cargo.toml]
B --> C[编写 Rust FFI 接口(含结构体)]
C --> D[构建 Rust 共享库]
D --> E[验证共享库]
E --> F[创建 C 程序(定义结构体)]
F --> G[构建 C 程序]
G --> H[设置环境变量]
H --> I[运行 C 程序]
| 步骤 | 操作 | 命令或代码示例 |
|---|---|---|
| 1 | 创建新的 Cargo 项目 | cargo new --lib ffi && cd ffi |
| 2 | 修改 Cargo.toml | [lib]<br>name = "ffitest"<br>crate-type = ["dylib"] |
| 3 | 编写 Rust FFI 接口(含结构体) | #[repr(C)]<br>pub struct Point {<br> x: i32,<br> y: i32,<br>}<br><br>#[no_mangle]<br>pub extern "C" fn print_point(point: Point) {<br> println!("Point: ({}, {})", point.x, point.y);<br>} |
| 4 | 构建 Rust 共享库 | cargo build --release |
| 5 | 验证共享库 | nm -D target/release/libffitest.so | grep print_point |
| 6 | 创建 C 程序(定义结构体) | #include <stdio.h><br><br>typedef struct {<br> int x;<br> int y;<br>} Point;<br><br>extern void print_point(Point point);<br><br>int main(void) {<br> Point p = {10, 20};<br> print_point(p);<br> return 0;<br>} |
| 7 | 构建 C 程序 | gcc rustffi.c -Ltarget/release -lffitest -o ffitest |
| 8 | 设置环境变量 | export LD_LIBRARY_PATH=$(rustc --print sysroot)/lib:target/release:$LD_LIBRARY_PATH |
| 9 | 运行 C 程序 | ./ffitest |
超级会员免费看
1万+

被折叠的 条评论
为什么被折叠?



