Merge pull request #28250 from wxy/20221122.2-️-Introducing-Rust-calls-to-C-library-functions

RP:20221122.2 ️ introducing rust calls to c library functions
This commit is contained in:
Xingyu.Wang 2022-12-16 11:07:31 +08:00 committed by GitHub
commit 5916f5e172
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -3,26 +3,30 @@
[#]: author: "Marty Kalin https://opensource.com/users/mkalindepauledu"
[#]: collector: "lkxed"
[#]: translator: "yzuowei"
[#]: reviewer: " "
[#]: publisher: " "
[#]: url: " "
[#]: reviewer: "wxy"
[#]: publisher: "wxy"
[#]: url: "https://linux.cn/article-15353-1.html"
介绍从 Rust 调用 C 库函数
从 Rust 调用 C 库函数
======
为什么要从 Rust 调用 C 函数?简短的回答就是软件库。冗长的答案则触及到 C 在众多编程语言中的地位,特别是相对 Rust 而言。CC++,还有 Rust 都是系统语言这意味着程序员会访问机器层面的数据类型与操作。在这三个系统语言中C 依然占据主导地位。现代操作系统的内核大致用 C 来写,其余部分依靠汇编语言来补充。在标准系统函数库中,输入与输出、数字处理、加密计算、安全、网络、国际化、字符串处理、内存管理,还用更多,都大体用 C 来写。这些函数库所代表的是一个庞大的基础架构支撑着用其他语言写出来的应用。Rust 发展至今也有着可观的函数库,但是 C 的函数库——自1970年代就已存在迄今还在蓬勃发展——是一个无法被忽视的资源。最后一点是 C 依然还是编程语言中的 [lingua franca][1]:大部分语言都与 C 交流,透过 C语言互相交流。
![][0]
> Rust FFI 和 bindgen 工具是为 Rust 调用 C 库而设计的。Rust 很容易与 C 语言对话,从而与任何其它可以与 C 语言对话的语言对话。
为什么要从 Rust 调用 C 函数?简短的答案就是软件库。冗长的答案则触及到 C 在众多编程语言中的地位,特别是相对 Rust 而言。C、C++,还有 Rust 都是系统语言这意味着程序员可以访问机器层面的数据类型与操作。在这三个系统语言中C 依然占据主导地位。现代操作系统的内核主要是用 C 来写的,其余部分依靠汇编语言补充。在标准系统函数库中,输入与输出、数字处理、加密计算、安全、网络、国际化、字符串处理、内存管理等等,大多都是用 C 来写的。这些函数库所代表的是一个庞大的基础设施支撑着用其他语言写出来的应用。Rust 发展至今也有着可观的函数库,但是 C 的函数库 —— 自 1970 年代就已存在,迄今还在蓬勃发展 —— 是一种无法被忽视的资源。最后一点是C 依然还是编程语言中的 [通用语][1]:大部分语言都可以与 C 交流,透过 C语言之间可以互相交流。
### 两个概念证明的例子
Rust 支持 FFI (外部函数接口)用以调用 C 函数。任何 FFI 所需要面临的问题是调用方语言是否包括了被调用语言的数据类型。例如,`ctypes` 是 Python 调用 C 的 FFI但是 Python 并没有包括 C 所支持的无符号整数类型。结果就是,`ctypes` 必须寻求解决方案。
Rust 支持 FFI<ruby>外部函数接口<rt>Foreign Function Interface</rt></ruby>)用以调用 C 函数。任何 FFI 所需要面临的问题是调用方语言是否涵盖了被调用语言的数据类型。例如,`ctypes` 是 Python 调用 C 的 FFI但是 Python 并没有包括 C 所支持的无符号整数类型。结果就是,`ctypes` 必须寻求解决方案。
与之相对的是Rust 包含了所有 C 中的原始机器层面类型。比如说Rust 中的 `i32` 类对应 C 中的 `int` 类。C 特别声明了 `char` 类必须是一个字节大小,而其他类型,比如 `int`必须至少是这个大小LCTT 译注:原文处有评论指出 `int` 大小依照 C 标准应至少为2字节然而如今所有合理的 C 编译器都支持四字节的 `int`,以及八字节的 `double`Rust 中则是 `f64` 类),以此类推。
相比之下Rust 包含了所有 C 中的原始机器层面类型。比如说Rust 中的 `i32` 类对应 C 中的 `int` 类。C 特别声明了 `char` 类必须是一个字节大小,而其他类型,比如 `int`必须至少是这个大小LCTT 译注:原文处有评论指出 `int` 大小依照 C 标准应至少为 2 字节);然而如今所有合理的 C 编译器都支持四字节的 `int`,以及八字节的 `double`Rust 中则是 `f64` 类),以此类推。
面向 C 的 FFI 所面临的另一个挑战是FFI 是否能够处理 C 的裸指针包括指向被看作是字符串的数组指针。C 没有字符串类型,它通过结合字符组和一个不会被打印的终止符来实现字符串大名鼎鼎的_空终止符_。与之相对Rust 有两个字符串类型:`String` 和 `&str` 字符串切片。问题是Rust FFI 是否能将 C 字符串转化成 Rust 字符串——答案是_肯定的_。
针对 C 的 FFI 所面临的另一个挑战是FFI 是否能够处理 C 的裸指针包括指向被看作是字符串的数组指针。C 没有字符串类型,它通过结合字符组和一个非打印终止符(大名鼎鼎的 _空终止符_)来实现字符串。相比之下Rust 有两个字符串类型:`String` 和 `&str` 字符串切片。问题是Rust FFI 是否能将 C 字符串转化成 Rust 字符串——答案是 _肯定的_
出于对效率的追求,结构体指针在 C 中也很常见。一个 C 结构体在作为一个函数的参数或者返回值的时候,其默认行为是传递值(即,一个字节一个字节的复制。C 结构体,如同它在 Rust 中的对应部分一样,可以包含数组和嵌套其他结构体,所以其大小是不定的。结构体在两种语言中的最佳用法是传递或返回引用,也就是说,传递或返回结构体的地址而不是结构体本身的复制。Rust 再一次成功处理了 C 的结构体指针,其在 C 函数库中十分普遍。
出于对效率的追求,结构体指针在 C 中也很常见。一个 C 结构体在作为一个函数的参数或者返回值的时候,其默认行为是传递值(即,逐字节复制。C 结构体,如同它在 Rust 中的对应部分一样,可以包含数组和嵌套其他结构体,所以其大小是不定的。结构体在两种语言中的最佳用法是传递或返回引用,也就是说,传递或返回结构体的地址而不是结构体本身的副本。Rust FFI 再一次成功处理了 C 的结构体指针,其在 C 函数库中十分普遍。
第一段代码案例专注于调用相对简单的 C 库函数,比如 `abs`(绝对值)和 `sqrt`(平方根)。这些函数使用非指针标量参数并返回一个非指针标量值。第二段代码案例则涉及了字符串和结构体指针,在这里会介绍工具 [bindgen][2],其通过 C 接口(头文件生成 Rust 代码,比如 `math.h` 以及 `time.h`。C 头文件声明了 C 函数的调用语法并定义了会被调用的结构体。两段代码都能在[我的主页上][3]找到。
第一段代码案例专注于调用相对简单的 C 库函数,比如 `abs`(绝对值)和 `sqrt`(平方根)。这些函数使用非指针标量参数并返回一个非指针标量值。第二段代码案例则涉及了字符串和结构体指针,在这里会介绍工具 [bindgen][2],其通过 C 接口(头文件生成 Rust 代码,比如 `math.h` 以及 `time.h`。C 头文件声明了 C 函数的调用语法并定义了会被调用的结构体。两段代码都能在 [我的主页上][3] 找到。
### 调用相对简单的 C 函数
@ -56,9 +60,9 @@ fn main() {
}
```
顶部的两个 `use` 声明是 Rust 的数据类型 `c_int``c_double`,对应 C 类型里的 `int``double`。Rust 标准模块 `std::os::raw` 定义了十四个类似的类型以确保跟 C 的兼容性。模块 `std::ffi` 中有十四个同样的类型定义以及对字符串的支持。
顶部的两个 `use` 声明是 Rust 的数据类型 `c_int``c_double`,对应 C 类型里的 `int``double`。Rust 标准模块 `std::os::raw` 定义了 14 个类似的类型以确保跟 C 的兼容性。模块 `std::ffi` 中有 14 个同样的类型定义,以及对字符串的支持。
位于 `main` 函数上的 `extern "C"` 区域声明了个 C 库函数,这些函数会在 `main` 函数内被调用。每次调用都使用了标准的 C 函数名,但每次调用都必须发生在一个 `unsafe` 区域内。正如每个新接触 Rust 的程序员所发现的那样Rust 编译器极度强制内存安全。其他语言(特别是 C 和 C++)作不出相同的保证。`unsafe` 区域其实是说Rust 对外部调用中可能存在的不安全行为不负责。
位于 `main` 函数上的 `extern "C"` 区域声明了 3 个 C 库函数,这些函数会在 `main` 函数内被调用。每次调用都使用了标准的 C 函数名,但每次调用都必须发生在一个 `unsafe` 区域内。正如每个新接触 Rust 的程序员所发现的那样Rust 编译器极度强制内存安全。其他语言(特别是 C 和 C++)作不出相同的保证。`unsafe` 区域其实是说Rust 对外部调用中可能存在的不安全行为不负责。
第一个程序输出为:
@ -69,13 +73,13 @@ fn main() {
-3.14的平方根是: NaN.
```
输出的最后一行的 `NaN` 表示不是数字 (Not a Number)C 库函数 `sqrt` 期待一个非负值作为参数,这使得参数-3.14生成了 `NaN` 作为返回值。
输出的最后一行的 `NaN` 表示<ruby>不是数字<rt>Not a Number</rt></ruby>C 库函数 `sqrt` 期待一个非负值作为参数,这使得参数 `-3.14` 生成了 `NaN` 作为返回值。
### 调用涉及指针的 C 函数
C 库函数为了提高效率经常在安全、网络、字符串处理、内存管理,以及其他领域中使用指针。例如,库函数 `asctime`时间作为 ASCII 字符串期待一个结构体指针作为其参数。Rust 调用类似 `asctime` 的 C 函数就会比调用 `sqrt` 要更加棘手一些,后者既没有牵扯到指针,也不涉及到结构体。
C 库函数为了提高效率经常在安全、网络、字符串处理、内存管理,以及其他领域中使用指针。例如,库函数 `asctime`ASCII 字符串形式的时间期待一个结构体指针作为其参数。Rust 调用类似 `asctime` 的 C 函数就会比调用 `sqrt` 要更加棘手一些,后者既没有牵扯到指针,也不涉及到结构体。
函数 `asctime` 调用的 C 结构体类型为 `struct tm`。一个指向此结构体的指针会作为参数被传递给库函数 `mktime`(时间作为值)。此结构体会将时间拆分成诸如年、月、小时之类的单位。此结构体的字段 (fields) 类型为 `time_t`,是 `int`32位`long`64位的异名。两个库函数将这些破碎的时间碎片组合成了一个单一值`asctime` 返回一个字符串用以表达时间,而 `mktime` 返回一个 `time_t` 值表示自 [_epoch_][4],即系统时钟和时间戳被决定的那一刻,以来所经历的秒数。典型的 epoch 设置为1900年或1970年1月1日0时0分0秒。
函数 `asctime` 调用的 C 结构体类型为 `struct tm`。一个指向此结构体的指针会作为参数被传递给库函数 `mktime`(时间作为值)。此结构体会将时间拆分成诸如年、月、小时之类的单位。此结构体的<ruby>字段<rt>field</rt></ruby>类型为 `time_t`,是 `int`32位`long`64 位)的别名。两个库函数将这些破碎的时间片段组合成了一个单一值:`asctime` 返回一个以字符串表示的时间,而 `mktime` 返回一个 `time_t` 值表示自 “<ruby>[纪元][4]<rt>Epoch</rt></ruby> 以来所经历的秒数,这是一个系统的时钟和时间戳的相对时间。典型的纪元设置为 1900 年或 1970 年1 月 1 日 0 时 0 分 0 秒。LCTT 校注Unix、Linux 乃至于如今所有主要的计算机和网络的时间纪元均采用 1970 年为起点。)
以下的 C 程序调用了 `asctime``mktime`,并使用了其他库函数 `strftime` 来将 `mktime` 的返回值转化成一个格式化的字符串。这个程序可被视作 Rust 对应版本的预热:
@ -93,8 +97,8 @@ int main () {
sometime.tm_hour = 1;
sometime.tm_mday = 1;
sometime.tm_mon = 1;
sometime.tm_year = 1;
sometime.tm_hour = 1; /*LCTT 译注:这里作者多敲了一行*/
sometime.tm_year = 1; /*LCTT 校注:注意,相对于 1900 年的年数*/
sometime.tm_hour = 1;
sometime.tm_wday = 1;
sometime.tm_yday = 1;
@ -102,11 +106,11 @@ int main () {
utc = mktime(&sometime);
if( utc < 0 ) {
fprintf(stderr, "错误: mktime 无法生成时间\n");
fprintf(stderr, "错误: mktime 无法生成时间\n");
} else {
printf("返回的整数值: %d\n", utc);
strftime(buffer, sizeof(buffer), "%c", &sometime);
printf("更加可读的版本: %s\n", buffer);
printf("返回的整数值: %d\n", utc);
strftime(buffer, sizeof(buffer), "%c", &sometime);
printf("更加可读的版本: %s\n", buffer);
}
return 0;
@ -121,19 +125,19 @@ int main () {
更加可读的版本: Fri Feb  1 01:01:01 1901
```
LCTT 译注:如果你尝试在自己电脑上运行这段代码,然后得到了一行关于 `mktime` 的错误信息,然后又在网上随便找了个在线 C 编译器,复制代码然后得到了跟这里的结果有区别但是没有错误的结果,不要慌,我的电脑上也是这样的。导致本地机器上 `mktime` 失败的原因是作者没有设置 `tm_isdst`,这个是用来标记夏令时的 flag。[`tm_isdst` 大于零则夏令时生效中,等于零则不生效,小于零标记未知][5]。加入 `sometime.tm_isdst = 0``= -1` 后应该就能得到跟在线编译器大致一样的结果。不同的地方在于结果第一行我得到的是 `Mon Feb ...`,这个与作者代码中 `sometime.tm_wday = 1` 对应,这里因该是作者写错了;第二行我和作者和网上得到的数字都不一样,这大概是合理的,因为这与机器的 epoch 有关第三行我跟作者的结果是一样的1901年2月1日也确实是周五这是因为 [`mktime` 其实会修正时间参数中不合理的地方][6]。至于夏令时具体是如何影响 `mktime` 这个问题,我能查到的只有 `mktime` 的计算受时区影响,更底层的原因我也不知道了。)
LCTT 译注:如果你尝试在自己电脑上运行这段代码,然后得到了一行关于 `mktime` 的错误信息,然后又在网上随便找了个在线 C 编译器,复制代码然后得到了跟这里的结果有区别但是没有错误的结果,不要慌,我的电脑上也是这样的。导致本地机器上 `mktime` 失败的原因是作者没有设置 `tm_isdst`,这个是用来标记夏令时的标志。[`tm_isdst` 大于零则夏令时生效中,等于零则不生效,小于零标记未知][5]。加入 `sometime.tm_isdst = 0``= -1` 后应该就能得到跟在线编译器大致一样的结果。不同的地方在于结果第一行我得到的是 `Mon Feb ...`,这个与作者代码中 `sometime.tm_wday = 1` 对应,这里应该是作者**写错了**;第二行我和作者和网上得到的数字都不一样,这大概是合理的,因为这与机器的纪元有关第三行我跟作者的结果是一样的1901 2 1 日也确实是周五,这是因为 [`mktime` 其实会修正时间参数中不合理的地方][6]。至于夏令时具体是如何影响 `mktime` 这个问题,我能查到的只有 `mktime` 的计算受时区影响,更底层的原因我也不知道了。)
总的来说Rust 在调用库函数 `asctime``mktime` 时,必须处理以下两个问题:
- 将裸指针作为唯一参数传递给每个库函数。
- 把从 `asctime` 返回的 C 字符串转化为 Rust 字符串。
### Rust 调用 `asctime` `mktime`
### Rust 调用 asctime 和 mktime
工具 `bindgen` 会根据类似 `math.h``time.h` 之类的 C 头文件生成 Rust 支持的代码。下面这个简化版的 `time.h` 就可以用来做例子,简化版与原版主要有两个不同:
- 内置类型 `int` 被用来取代名类型 `time_t`。工具 bindgen 可以处理 `time_t` 类但是会生成一些烦人的警告,因为 `time_t` 不符合 Rust 的命名规范:`time_t` 以下划线区分 `time``t`Rust 更偏好驼峰式命名方法,比如 `TimeT`
- 出于同样的原因,这里选择 `StructTM` 作为 `struct tm`名。
- 内置类型 `int` 被用来取代名类型 `time_t`。工具 bindgen 可以处理 `time_t`但是会生成一些烦人的警告,因为 `time_t` 不符合 Rust 的命名规范:`time_t` 以下划线区分 `time``t`Rust 更偏好驼峰式命名方法,比如 `TimeT`
- 出于同样的原因,这里选择 `StructTM` 作为 `struct tm`名。
以下是一份简化版的头文件,`mktime` 和 `asctime` 在文件底部:
@ -154,7 +158,7 @@ extern int mktime(StructTM*);
extern char* asctime(StructTM*);
```
`bindgen` 安装好后,`%` 作为命令行提示,`mytime.h` 作为以上提到的头文件,以下命令可以生成所需的 Rust 代码并将其保存到文件 `mytime.rs`
`bindgen` 安装好后,`mytime.h` 作为以上提到的头文件,以下命令`%` 是命令行提示符)可以生成所需的 Rust 代码并将其保存到文件 `mytime.rs`
```
% bindgen mytime.h > mytime.rs
@ -201,9 +205,9 @@ fn bindgen_test_layout_tm() {
...
```
Rust 结构体 `struct tm`,跟原本在 C 中的一样,包含了九个4字节的整型字段。这些字段名称在 C 和 Rust 中是一样的。`extern "C"` 区域声明了库函数 `astime``mktime` 分别需要只一个参数,一个指向可变实例 `extern "C"` 的裸指针。(库函数可能会通过指针改变作为参数传递的结构体。)
Rust 结构体 `struct tm`,跟原本在 C 中的一样,包含了 9 个 4 字节的整型字段。这些字段名称在 C 和 Rust 中是一样的。`extern "C"` 区域声明了库函数 `astime``mktime` 分别需要只一个参数,一个指向可变实例 `StructTM` 的裸指针。(库函数可能会通过指针改变作为参数传递的结构体。)
`#[test]` 属性下的其余代码是用来测试 Rust 版的时间结构体的布局。通过命令 `cargo test` 可以进行这些测试。一个 C 不会声明的问题是编译器应该如何对结构体中的字段进行布局。比如说C 的 `struct tm` 以字段 `tm_sec` 开头用以表示秒;但是 C 不需要编译版本遵循这个排序。不管怎样Rust 测试应该会成功而 Rust 对库函数的调用也应如预期般工作。
`#[test]` 属性下的其余代码是用来测试 Rust 版的时间结构体的布局。通过命令 `cargo test` 可以进行这些测试。问题在于C 没有规定编译器应该如何对结构体中的字段进行布局。比如说C 的 `struct tm` 以字段 `tm_sec` 开头用以表示秒;但是 C 不需要编译版本遵循这个排序。不管怎样Rust 测试应该会成功而 Rust 对库函数的调用也应如预期般工作。
### 设置好第二个案例并开始运行
@ -251,9 +255,9 @@ Ok(
2120218157
```
对 C 函数 `asctime``mktime` 的调用必须再一次被放在 `unsafe` 区域内,因为 Rust 编译器无法对这些外部函数的潜在内存安全风险负责。此处声明一下,`asctime` 和 `mktime` 并没有安全风险。调用的两个函数的参数是裸指针 `ptr`,其指向结构体 `sometime` (在栈 (stack) 中)的地址。
对 C 函数 `asctime``mktime` 的调用必须再一次被放在 `unsafe` 区域内,因为 Rust 编译器无法对这些外部函数的潜在内存安全风险负责。此处声明一下,`asctime` 和 `mktime` 并没有安全风险。调用的两个函数的参数是裸指针 `ptr`,其指向结构体 `sometime` (在<ruby><rt>stack</rt></ruby>中)的地址。
`asctime` 是两个函数中调用起来更棘手的那个,因为这个函数返回的是一个指向 C `char` 的指针,如果函数返回 `Mon` 那么指针就指向 `M`。但是 Rust 编译器并不知道 C 字符串 `char` 的空终止数组)的储存位置。是内存里的静态空间?还是堆 (heap)`asctime` 函数内用来储存时间的文字表达的数组实际上是在内存的静态空间里。无论如何C 到 Rust 字符串转化需要两个步骤来避免编译错误:
`asctime` 是两个函数中调用起来更棘手的那个,因为这个函数返回的是一个指向 C `char` 的指针,如果函数返回 `Mon` 那么指针就指向 `M`。但是 Rust 编译器并不知道 C 字符串 `char` 的空终止数组)的储存位置。是内存里的静态空间?还是<ruby><rt>heap</rt></ruby>`asctime` 函数内用来储存时间的文字表达的数组实际上是在内存的静态空间里。无论如何C 到 Rust 字符串转化需要两个步骤来避免编译错误:
- 调用 `Cstr::from_ptr(char_ptr)` 来将 C 字符串转化为 Rust 字符串并返回一个引用储存在变量 `c_str` 中。
- 对 `c_str.to_str()` 的调用确保了 `c_str` 是所有者。
@ -262,9 +266,9 @@ Rust 代码不会增加从 `mktime` 返回的整型值的易读性,这一部
### 使用 FFI 和 bindgen 调用 C
Rust FFI 和工具 `bindgen` 都能够出色地协助 Rust 调用 C 库无论是标准库还是第三方库。Rust 轻松地与 C 交流,并透过 C 与其他语言交流。对于调用像 `sqrt` 一样简单的库函数Rust FFI 表现直截了当,这是因为 Rust 的原始数据类型覆盖了它们在 C 中的对应部分。
Rust FFI 和工具 `bindgen` 都能够出色地协助 Rust 调用 C 库无论是标准库还是第三方库。Rust 可以轻松地与 C 交流,并透过 C 与其他语言交流。对于调用像 `sqrt` 一样简单的库函数Rust FFI 表现直截了当,这是因为 Rust 的原始数据类型覆盖了它们在 C 中的对应部分。
对于更为复杂的交流——特别是 Rust 调用像 `asctime``mktime` 一样,会涉及到结构体和指针的 C 库函数——工具 `bindgen` 是优秀的帮手。这个工具会生成支持代码以及所需要的测试。当然Rust 编译器无法假设 C 代码对内存安全的考虑会符合 Rust 的标准因此Rust 必须在 `unsafe` 区域内调用 C。
对于更为复杂的交流 —— 特别是 Rust 调用像 `asctime``mktime` 一样,会涉及到结构体和指针的 C 库函数 —— `bindgen` 工具是优秀的帮手。这个工具会生成支持代码以及所需要的测试。当然Rust 编译器无法假设 C 代码对内存安全的考虑会符合 Rust 的标准因此Rust 必须在 `unsafe` 区域内调用 C。
--------------------------------------------------------------------------------
@ -273,7 +277,7 @@ via: https://opensource.com/article/22/11/rust-calls-c-library-functions
作者:[Marty Kalin][a]
选题:[lkxed][b]
译者:[yzuowei](https://github.com/yzuowei)
校对:[校对者ID](https://github.com/校对者ID)
校对:[wxy](https://github.com/wxy)
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出
@ -285,3 +289,4 @@ via: https://opensource.com/article/22/11/rust-calls-c-library-functions
[4]: https://baike.baidu.com/item/UNIX时间/8932323
[5]: https://cplusplus.com/reference/ctime/tm/
[6]: https://cplusplus.com/reference/ctime/mktime/
[0]: https://img.linux.net.cn/data/attachment/album/202212/16/110147q4kk0qoqe0e3m6bb.jpg