Rust 调用 C 库函数介绍

Rust FFI 和 bindgen 工具非常适合用于 Rust 调用 C 库。Rust 可以轻松地与 C 通信,从而与任何可以与 C 通信的其他语言通信。
2 位读者喜欢这个。
Woman programming

WOCinTech Chat。由 Opensource.com 修改。CC BY-SA 4.0

为什么从 Rust 调用 C 函数?简短的答案是软件库。更长的答案涉及 C 在编程语言中的地位,尤其是相对于 Rust 而言。C、C++ 和 Rust 都是系统语言,它们允许程序员访问机器级数据类型和操作。在这三种系统语言中,C 仍然占据主导地位。现代操作系统的内核主要用 C 编写,其余部分用汇编语言编写。用于输入和输出、数值计算、密码学、安全、网络、国际化、字符串处理、内存管理等等的标准系统库也大多用 C 编写。这些库代表了用任何其他语言编写的应用程序的庞大基础设施。Rust 也在努力提供自己的优秀库,但 C 库——自 1970 年代以来就存在并且仍在增长——是一个不容忽视的资源。最后,C 仍然是编程语言中的 通用语言:大多数语言都可以与 C 通信,并通过 C 与任何其他可以这样做语言通信。

两个概念验证示例

Rust 有一个 FFI(外部函数接口),支持调用 C 函数。任何 FFI 的一个问题是调用语言是否涵盖被调用语言中的数据类型。例如,ctypes 是一个用于从 Python 调用 C 的 FFI,但 Python 不涵盖 C 中可用的无符号整数类型。因此,ctypes 必须求助于变通方法。

相比之下,Rust 涵盖了 C 中的所有原始(即机器级)类型。例如,Rust i32 类型与 C int 类型匹配。C 仅指定 char 类型的大小必须为一个字节,而其他类型(例如 int)的大小必须至少为此大小;但如今,每个合理的 C 编译器都支持四字节的 int、八字节的 double(在 Rust 中为 f64 类型),等等。

针对 C 的 FFI 还面临另一个挑战:FFI 是否可以处理 C 的原始指针,包括指向在 C 中被视为字符串的数组的指针?C 没有字符串类型,而是将字符串实现为字符数组,并带有非打印终止字符,即传说中的空终止符。相比之下,Rust 有两种字符串类型:String&str(字符串切片)。那么,问题是 Rust FFI 是否可以将 C 字符串转换为 Rust 字符串——答案是可以

指向结构的指针在 C 中也很常见。原因是效率。默认情况下,当结构作为传递给函数的参数或从函数返回的值时,C 结构是值传递的(即,按字节复制)。C 结构,就像它们的 Rust 对等物一样,可以包含数组和嵌套其他结构,因此大小可以任意大。这两种语言的最佳实践都是通过引用传递和返回结构,即,通过传递或返回结构的地址而不是结构的副本。再次强调,Rust FFI 可以胜任处理 C 库中常见的 C 结构指针的任务。

第一个代码示例侧重于调用相对简单的 C 库函数,例如 abs(绝对值)和 sqrt(平方根)。这些函数接受非指针标量参数并返回非指针标量值。第二个代码示例涵盖字符串和指向结构的指针,介绍了 bindgen 工具,该工具从 C 接口(头)文件(例如 math.htime.h)生成 Rust 代码。C 头文件指定 C 函数的调用语法并定义此类调用中使用的结构。这两个代码示例在我的主页上可用

调用相对简单的 C 函数

第一个代码示例有四个 Rust 调用标准数学库中的 C 函数:分别调用 abs(绝对值)和 pow(求幂)各一次,以及调用 sqrt(平方根)两次。该程序可以直接使用 rustc 编译器构建,或者更方便地使用 cargo build 命令构建

use std::os::raw::c_int;    // 32 bits
use std::os::raw::c_double; // 64 bits

// Import three functions from the standard library libc.
// Here are the Rust declarations for the C functions:
extern "C" {
    fn abs(num: c_int) -> c_int;
    fn sqrt(num: c_double) -> c_double;
    fn pow(num: c_double, power: c_double) -> c_double;
}

fn main() {
    let x: i32 = -123;
    println!("\nAbsolute value of {x}: {}.",
	     unsafe { abs(x) });

    let n: f64 = 9.0;
    let p: f64 = 3.0;
    println!("\n{n} raised to {p}: {}.",
	     unsafe { pow(n, p) });

    let mut y: f64 = 64.0;
    println!("\nSquare root of {y}: {}.",
	     unsafe { sqrt(y) });
    y = -3.14;
    println!("\nSquare root of {y}: {}.",
	     unsafe { sqrt(y) }); //** NaN = NotaNumber
}

顶部的两个 use 声明用于 Rust 数据类型 c_intc_double,它们分别与 C 类型 intdouble 匹配。标准 Rust 模块 std::os::raw 为 C 兼容性定义了十四种此类类型。模块 std::ffi 具有相同的十四个类型定义以及对字符串的支持。

extern "C" 块在 main 函数之上声明了在下面的 main 函数中调用的三个 C 库函数。每个调用都使用标准 C 函数的名称,但每个调用都必须发生在 unsafe 块内。正如每个 Rust 新手程序员所发现的那样,Rust 编译器强制执行内存安全非常严格。其他语言(特别是 C 和 C++)不提供相同的保证。因此,unsafe 块表示:Rust 不对外部调用中可能发生的任何不安全操作负责。

第一个程序的输出是

Absolute value of -123: 123.
9 raised to 3: 729
Square root of 64: 8.
Square root of -3.14: NaN.

在最后一行输出中,NaN 代表非数字:C sqrt 库函数期望非负值作为其参数,这意味着参数 -3.14 生成 NaN 作为返回值。

调用涉及指针的 C 函数

安全、网络、字符串处理、内存管理和其他领域的 C 库函数通常使用指针来提高效率。例如,库函数 asctime(时间作为 ASCII 字符串)期望指向结构的指针作为其单个参数。因此,Rust 调用 C 函数(例如 asctime)比调用 sqrt 更棘手,后者不涉及指针和结构。

用于 asctime 函数调用的 C 结构类型为 struct tm。指向此类结构的指针也传递给库函数 mktime(创建时间值)。该结构将时间分解为年、月、小时等等单位。结构的字段类型为 time_t,它是 int(32 位)或 long(64 位)的别名。这两个库函数将这些分解的时间片段组合成一个值:asctime 返回时间的字符串表示形式,而 mktime 返回一个 time_t 值,该值表示自纪元以来经过的秒数,纪元是系统时钟和时间戳确定的相对时间。典型的纪元设置是 1900 年或 1970 年 1 月 1 日 00:00:00(零小时、分钟和秒)。

下面的 C 程序调用 asctimemktime,并使用另一个库函数 strftimemktime 返回值转换为格式化的字符串。此程序充当 Rust 版本的预热

#include <stdio.h>
#include <time.h>

int main () {
  struct tm sometime;  /* time broken out in detail */
  char buffer[80];
  int utc;

  sometime.tm_sec = 1;
  sometime.tm_min = 1;
  sometime.tm_hour = 1;
  sometime.tm_mday = 1;
  sometime.tm_mon = 1;
  sometime.tm_year = 1;
  sometime.tm_hour = 1;
  sometime.tm_wday = 1;
  sometime.tm_yday = 1;

  printf("Date and time: %s\n", asctime(&sometime));

  utc = mktime(&sometime);
  if( utc < 0 ) {
    fprintf(stderr, "Error: unable to make time using mktime\n");
  } else {
    printf("The integer value returned: %d\n", utc);
    strftime(buffer, sizeof(buffer), "%c", &sometime);
    printf("A more readable version: %s\n", buffer);
  }

  return 0;
}

程序输出

Date and time: Fri Feb  1 01:01:01 1901
The integer value returned: 2120218157
A more readable version: Fri Feb  1 01:01:01 1901

总而言之,Rust 调用库函数 asctimemktime 必须处理两个问题

  • 将原始指针作为每个库函数的单个参数传递。

  • 将从 asctime 返回的 C 字符串转换为 Rust 字符串。

Rust 调用 asctimemktime

bindgen 工具从 C 头文件(例如 math.htime.h)生成 Rust 支持代码。在此示例中,简化版本的 time.h 即可,但与原始版本相比有两个更改

  • 内置类型 int 用于代替别名类型 time_t。bindgen 工具可以处理 time_t 类型,但在处理过程中会生成一些分散注意力的警告,因为 time_t 不遵循 Rust 命名约定:在 time_t 中,下划线将末尾的 t 与前面的 time 分隔开;Rust 更喜欢 CamelCase 名称,例如 TimeT

  • 出于相同的原因,类型 struct tm 类型被赋予 StructTM 作为别名。

这是简化的头文件,底部声明了 mktimeasctime

typedef struct tm {
    int tm_sec;    /* seconds */
    int tm_min;    /* minutes */
    int tm_hour;   /* hours */
    int tm_mday;   /* day of the month */
    int tm_mon;    /* month */
    int tm_year;   /* year */
    int tm_wday;   /* day of the week */
    int tm_yday;   /* day in the year */
    int tm_isdst;  /* daylight saving time */
} StructTM;

extern int mktime(StructTM*);
extern char* asctime(StructTM*);

安装 bindgen 后,使用 % 作为命令行提示符,并使用 mytime.h 作为上面的头文件,以下命令生成所需的 Rust 代码并将其保存在文件 mytime.rs

% bindgen mytime.h > mytime.rs

这是 mytime.rs 的相关部分

/* automatically generated by rust-bindgen 0.61.0 */

#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct tm {
    pub tm_sec: ::std::os::raw::c_int,
    pub tm_min: ::std::os::raw::c_int,
    pub tm_hour: ::std::os::raw::c_int,
    pub tm_mday: ::std::os::raw::c_int,
    pub tm_mon: ::std::os::raw::c_int,
    pub tm_year: ::std::os::raw::c_int,
    pub tm_wday: ::std::os::raw::c_int,
    pub tm_yday: ::std::os::raw::c_int,
    pub tm_isdst: ::std::os::raw::c_int,
}

pub type StructTM = tm;

extern "C" {
    pub fn mktime(arg1: *mut StructTM) -> ::std::os::raw::c_int;
}

extern "C" {
    pub fn asctime(arg1: *mut StructTM) -> *mut ::std::os::raw::c_char;
}

#[test]
fn bindgen_test_layout_tm() {
    const UNINIT: ::std::mem::MaybeUninit<tm> =
       ::std::mem::MaybeUninit::uninit();
    let ptr = UNINIT.as_ptr();
    assert_eq!(
        ::std::mem::size_of::<tm>(),
        36usize,
        concat!("Size of: ", stringify!(tm))
    );
    ...

Rust 结构 struct tm,与原始 C 结构一样,包含九个 4 字节整数字段。字段名称在 C 和 Rust 中相同。extern "C" 块声明库函数 asctimemktime 各接受一个参数,即指向可变 StructTM 实例的原始指针。(库函数可能会通过作为参数传递的指针来改变结构。)

其余代码在 #[test] 属性下,测试时间结构的 Rust 版本的布局。可以使用 cargo test 命令运行测试。问题是 C 没有指定编译器必须如何布局结构的字段。例如,C struct tm 以秒的字段 tm_sec 开头;但 C 不要求编译后的版本将此字段作为第一个字段。无论如何,Rust 测试应该成功,并且 Rust 对库函数的调用应该按预期工作。

启动并运行第二个示例

bindgen 生成的代码不包含 main 函数,因此是一个自然模块。下面是带有 StructTM 初始化以及对 asctimemktime 的调用的 main 函数

mod mytime;
use mytime::*;
use std::ffi::CStr;

fn main() {
    let mut sometime  = StructTM {
        tm_year: 1,
        tm_mon: 1,
        tm_mday: 1,
        tm_hour: 1,
        tm_min: 1,
        tm_sec: 1,
        tm_isdst: -1,
        tm_wday: 1,
        tm_yday: 1
    };

    unsafe {
        let c_ptr = &mut sometime; // raw pointer

        // make the call, convert and then own
        // the returned C string
        let char_ptr = asctime(c_ptr);
        let c_str = CStr::from_ptr(char_ptr);
        println!("{:#?}", c_str.to_str());

        let utc = mktime(c_ptr);
        println!("{}", utc);
    }
}

Rust 代码可以编译(直接使用 rustccargo)然后运行。输出是

Ok(
    "Mon Feb  1 01:01:01 1901\n",
)
2120218157

对 C 函数 asctimemktime 的调用再次必须发生在 unsafe 块内,因为 Rust 编译器不能对这些外部函数中的任何内存安全问题负责。记录在案,asctimemktime 行为良好。在对这两个函数的调用中,参数是原始指针 ptr,它保存 sometime 结构的(堆栈)地址。

asctime 的调用是两个调用中更棘手的一个,因为此函数返回指向 C char 的指针,即文本输出中 Mon 中的字符 M。然而,Rust 编译器不知道 C 字符串(以 null 结尾的 char 数组)存储在哪里。在内存的静态区域中?在堆上?asctime 函数用于存储时间文本表示形式的数组实际上位于内存的静态区域中。无论如何,C 到 Rust 字符串的转换分两步完成以避免编译时错误

  1. 调用 Cstr::from_ptr(char_ptr) 将 C 字符串转换为 Rust 字符串,并返回存储在 c_str 变量中的引用。

  2. 调用 c_str.to_str() 确保 c_str 是所有者。

Rust 代码不会生成从 mktime 返回的整数值的可读版本,这留给有兴趣的人作为练习。Rust 模块 chrono::format 包含一个 strftime 函数,可以像同名的 C 函数一样使用它来获取时间的文本表示形式。

使用 FFI 和 bindgen 调用 C

Rust FFI 和 bindgen 工具非常适合用于 Rust 调用 C 库,无论是标准库还是第三方库。Rust 可以轻松地与 C 通信,从而与任何可以与 C 通信的其他语言通信。对于调用相对简单的库函数(例如 sqrt),Rust FFI 非常简单,因为 Rust 的原始数据类型涵盖了它们的 C 对等类型。

对于更复杂的交换——特别是 Rust 调用涉及结构和指针的 C 库函数(例如 asctimemktime)——bindgen 工具是最佳选择。此工具生成支持代码以及适当的测试。当然,Rust 编译器不能假设 C 代码在内存安全方面达到 Rust 标准;因此,从 Rust 到 C 的调用必须发生在 unsafe 块中。

标签
User profile image.
我是计算机科学领域的学者(德保罗大学计算与数字媒体学院),在软件开发方面拥有广泛的经验,主要是在生产计划和调度(钢铁行业)以及产品配置(卡车和公共汽车制造)方面。有关书籍和其他出版物的详细信息,请访问

4 评论

很高兴知道 rust FFI 很像 python ctypes。这对于许多人来说已经是众所周知的。
最好了解如何使新的 C 库更 rust 友好,充分利用 rust 安全功能,例如使用有界指针(指针和大小),以及如何使用 FFI 将其与 rust 概念紧密结合。

我认为 C 库已经有了显着的改进,尤其是在堆分配存储方面。例如,大多数库(例如,OpenSSL)现在提供自定义的释放函数,这些函数可以自动处理嵌套的堆分配,并且通常对原始指针强制执行约束。(Valgrind 等工具同样非常有帮助。) 尽管如此,我认为在强制执行内存安全方面,Rust 仍然是独一无二的。

回复 作者:Ronald

小错误
C 仅指定 char 类型的大小必须为一个字节,而其他类型(例如 int)的大小必须至少为此大小;
我认为您想说的是,C 标准指定 int 必须至少为两个字节,而不是像 char 那样的一个字节。

感谢您的指出:我同意。标准确实指定 int 必须至少为两个字节,并且如前所述,现在通常为四个字节。

回复 作者:havill

知识共享许可协议本作品根据知识共享署名-相同方式共享 4.0 国际许可协议获得许可。
© . All rights reserved.