在 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++ 定义 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,我们改为使用原始指针。由于取消引用此指针是不安全的,并且指针实际上可能是一个空指针,因此必须注意确保在与 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 crate,如果生成的绑定中的类型以 cty 为前缀。

构建 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 通过遍历相对路径、调用固定命令(例如 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 目录中。