在你的 Rust 中使用少量 C 代码
在 Rust 项目中使用 C 或 C++ 代码主要包括两个部分:
- 封装暴露的 C API 以便在 Rust 中使用
- 构建你的 C 或 C++ 代码,以便与 Rust 代码集成
由于 C++ 没有稳定的 ABI 供 Rust 编译器定位,因此建议在将 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 相同的规则来组织结构体中的数据。
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
,我们这里有一个原始指针。由于解引用此指针是 unsafe
的,并且该指针实际上可能是 null
指针,因此必须注意确保在与 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
为前缀,请不要忘记使用cty
crate。
构建你的 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
“shell out” 到你的其他构建系统可能是最简单的方法,方法是遍历相对路径,调用固定的命令(例如 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
目录中。