将 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 库(自 20 世纪 70 年代以来一直存在并且仍在增长)是一种不容忽视的资源。 最后,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 函数

第一个代码示例有四个对标准数学库中的 C 函数的 Rust 调用:分别对 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 具有相同的十四个类型定义以及对字符串的支持。

main 函数上方的 extern "C" 块然后声明了在下面的 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 字符串)需要指向结构的指针作为其单个参数。 因此,对 C 函数(例如 asctime)的 Rust 调用比对 sqrt 的调用更棘手,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

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

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

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

asctimemktime 的 Rust 调用

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

Creative Commons License本作品采用 Creative Commons Attribution-Share Alike 4.0 International License 授权。
© . All rights reserved.