26、从 C 调用 Rust 及 ABI 相关知识

从 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
  1. 修改 Cargo.toml 文件 :指定要构建的是共享库。
[lib]
name = "ffitest"
crate-type = ["dylib"]
  1. 在 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();
  1. 构建 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
  1. 运行 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
  1. 验证共享库:
nm -D target/release/libffitest.so | grep print_point
  1. 构建 C 程序:
gcc rustffi.c -Ltarget/release -lffitest -o ffitest
  1. 设置环境变量:
export LD_LIBRARY_PATH=$(rustc --print sysroot)/lib:target/release:$LD_LIBRARY_PATH
  1. 运行 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
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值