在你的 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++ 如何定义 intchar 的灵活性,建议使用 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 用户手册,但是典型的过程包括以下步骤:

  1. 收集所有定义你希望在 Rust 中使用的接口或数据类型的 C 或 C++ 头文件。
  2. 编写一个 bindings.h 文件,其中 #include "..." 包含你在第一步中收集的每个文件。
  3. 将此 bindings.h 文件以及用于编译代码的任何编译标志提供给 bindgen。提示:使用 Builder.ctypes_prefix("cty") / --ctypes-prefix=ctyBuilder.use_core() / --use-core 使生成的代码与 #![no_std] 兼容。
  4. 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++ 代码编译为静态库,方法是触发你现有的构建系统(例如 makeCMake 等),或者通过移植必要的编译步骤来使用名为 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 目录中。