在 Rust 中使用 C
在 Rust 项目中使用 C 或 C++ 包含两个主要部分
- 包装公开的 C API 以供 Rust 使用
- 构建 C 或 C++ 代码以与 Rust 代码集成
由于 C++ 对于 Rust 编译器没有稳定的 ABI,因此建议在将 Rust 与 C 或 C++ 结合使用时使用 C
ABI。
定义接口
在从 Rust 使用 C 或 C++ 代码之前,有必要在 Rust 中定义链接代码中存在的数据类型和函数签名。在 C 或 C++ 中,您将包含一个头文件(.h
或 .hpp
),其中定义了此数据。在 Rust 中,有必要将这些定义手动转换为 Rust,或者使用工具生成这些定义。
首先,我们将介绍如何将这些定义从 C/C++ 手动转换为 Rust。
包装 C 函数和数据类型
通常,用 C 或 C++ 编写的库将提供一个头文件,定义公共接口中使用的所有类型和函数。示例文件可能如下所示
/* File: cool.h */
typedef struct CoolStruct {
int x;
int y;
} CoolStruct;
void cool_function(int i, char c, CoolStruct* cs);
转换为 Rust 后,此接口将如下所示
/* File: cool_bindings.rs */
#[repr(C)]
pub struct CoolStruct {
pub x: cty::c_int,
pub y: cty::c_int,
}
extern "C" {
pub fn cool_function(
i: cty::c_int,
c: cty::c_char,
cs: *mut CoolStruct
);
}
让我们逐一查看此定义,以解释每个部分。
#[repr(C)]
pub struct CoolStruct { ... }
默认情况下,Rust 不保证 struct
中包含的数据的顺序、填充或大小。为了保证与 C 代码的兼容性,我们包含 #[repr(C)]
属性,它指示 Rust 编译器始终使用 C 用于组织 struct
中数据的相同规则。
pub x: cty::c_int,
pub y: cty::c_int,
由于 C 或 C++ 定义 int
或 char
的灵活性,建议使用 cty
中定义的原始数据类型,这些类型将 C 中的类型映射到 Rust 中的类型。
extern "C" { pub fn cool_function( ... ); }
此语句定义了一个使用 C ABI 的函数的签名,称为 cool_function
。通过定义签名而不定义函数体,此函数的定义需要在其他地方提供,或者从静态库链接到最终库或二进制文件。
i: cty::c_int,
c: cty::c_char,
cs: *mut CoolStruct
与上面的数据类型类似,我们使用与 C 兼容的定义来定义函数参数的数据类型。为了清晰起见,我们还保留了相同的参数名称。
这里我们有一个新类型,*mut CoolStruct
。由于 C 没有 Rust 的引用概念,它看起来像这样:&mut CoolStruct
,我们改为使用原始指针。由于取消引用此指针是不安全的,并且指针实际上可能是一个空指针,因此必须注意确保在与 C 或 C++ 代码交互时,Rust 的典型保证。
自动生成接口
与其手动生成这些接口,这可能很乏味且容易出错,不如使用一个名为 bindgen 的工具,它将自动执行这些转换。有关 bindgen 用法的说明,请参阅 bindgen 用户手册,但典型过程包括以下步骤
- 收集所有定义您想与 Rust 一起使用的接口或数据类型的 C 或 C++ 头文件。
- 编写一个
bindings.h
文件,它将您在步骤一中收集的每个文件#include "..."
。 - 将此
bindings.h
文件以及用于将代码编译到bindgen
中的任何编译标志一起提供。提示:使用Builder.ctypes_prefix("cty")
/--ctypes-prefix=cty
和Builder.use_core()
/--use-core
使生成的代码与#![no_std]
兼容。 bindgen
将在终端窗口的输出中生成生成的 Rust 代码。此文件可以被管道传输到您项目中的文件,例如bindings.rs
。您可以在 Rust 项目中使用此文件来与作为外部库编译和链接的 C/C++ 代码进行交互。提示:不要忘记使用cty
crate,如果生成的绑定中的类型以cty
为前缀。
构建 C/C++ 代码
由于 Rust 编译器不知道如何直接编译 C 或 C++ 代码(或来自任何其他语言的代码,它提供了一个 C 接口),因此有必要提前编译非 Rust 代码。
对于嵌入式项目,这通常意味着将 C/C++ 代码编译为静态存档(例如 cool-library.a
),然后可以在最终链接步骤中将其与 Rust 代码结合起来。
如果您想使用的库已经作为静态存档分发,则无需重新构建代码。只需按照上述步骤转换提供的接口头文件,并在编译/链接时包含静态存档。
如果您的代码作为源项目存在,则有必要将 C/C++ 代码编译为静态库,方法是触发现有的构建系统(例如 make
、CMake
等),或者将必要的编译步骤移植到使用一个名为 cc
crate 的工具。对于这两个步骤,有必要使用 build.rs
脚本。
Rust build.rs
构建脚本
build.rs
脚本是用 Rust 语法编写的文件,它在编译机器上执行,在项目依赖项构建后但项目构建之前执行。
完整的参考可以在 这里找到。build.rs
脚本对于生成代码(例如通过 bindgen)、调用外部构建系统(例如 Make
)或直接通过使用 cc
crate 编译 C/C++ 很有用。
触发外部构建系统
对于具有复杂外部项目或构建系统的项目,使用 std::process::Command
通过遍历相对路径、调用固定命令(例如 make library
)来“调用”其他构建系统,然后将生成的静态库复制到 target
构建目录中的适当位置,可能最容易。
虽然您的 crate 可能针对 no_std
嵌入式平台,但您的 build.rs
仅在编译您的 crate 的机器上执行。这意味着您可以使用在编译主机上运行的任何 Rust crate。
使用 cc
crate 构建 C/C++ 代码
对于依赖项或复杂性有限的项目,或者对于难以修改构建系统以生成静态库(而不是最终二进制文件或可执行文件)的项目,使用 cc
crate 可能更容易,它为主机提供的编译器提供了一个惯用的 Rust 接口。
在将单个 C 文件编译为静态库的依赖项的最简单情况下,使用 cc
crate 的示例 build.rs
脚本将如下所示
fn main() {
cc::Build::new()
.file("src/foo.c")
.compile("foo");
}
build.rs
放在包的根目录下。然后 cargo build
将在构建包之前编译并执行它。一个名为 libfoo.a
的静态存档被生成并放置在 target
目录中。