为什么从 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 counterparts 一样,可以包含数组和嵌套其他结构,因此大小可以是任意大的。最佳实践是在任何一种语言中都通过引用传递和返回结构,即通过传递或返回结构的地址而不是结构的副本。再一次,Rust FFI 能够胜任处理 C 库中常见的 C 结构指针的任务。
第一个代码示例侧重于调用相对简单的 C 库函数,例如 abs
(绝对值)和 sqrt
(平方根)。这些函数接受非指针标量参数并返回非指针标量值。第二个代码示例涵盖字符串和指向结构的指针,介绍了 bindgen 工具,该工具从 C 接口(头)文件(例如 math.h
和 time.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_int
和 c_double
,它们分别与 C 类型 int
和 double
匹配。标准 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 字符串)期望指向结构的指针作为其单个参数。因此,Rust 调用诸如 asctime
之类的 C 函数比调用 sqrt
更棘手,后者不涉及指针或结构。
asctime
函数调用的 C 结构类型为 struct tm
。指向此类结构的指针也传递给库函数 mktime
(创建时间值)。该结构将时间分解为年、月、小时等单位。结构的字段类型为 time_t
,它是 int
(32 位)或 long
(64 位)的别名。这两个库函数将这些分解的时间片段组合成一个值:asctime
返回时间的字符串表示形式,而 mktime
返回一个 time_t
值,该值表示自纪元以来经过的秒数,纪元是系统时钟和时间戳相对于的时间。典型的纪元设置是 1900 年或 1970 年 1 月 1 日 00:00:00(零小时、分钟和秒)。
下面的 C 程序调用 asctime
和 mktime
,并使用另一个库函数 strftime
将 mktime
返回的值转换为格式化字符串。此程序充当 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 调用库函数 asctime
和 mktime
必须处理两个问题
-
将原始指针作为每个库函数的单个参数传递。
-
将从
asctime
返回的 C 字符串转换为 Rust 字符串。
Rust 调用 asctime
和 mktime
bindgen
工具从 C 头文件(例如 math.h
和 time.h
)生成 Rust 支持代码。在本示例中,简化版本的 time.h
即可,但与原始版本相比有两个更改
-
内置类型
int
用于代替别名类型time_t
。bindgen 工具可以处理time_t
类型,但在处理过程中会生成一些分散注意力的警告,因为time_t
不遵循 Rust 命名约定:在time_t
中,下划线将末尾的t
与前面的time
分隔开;Rust 更喜欢 CamelCase 名称,例如TimeT
。 -
类型
struct tm
类型被赋予StructTM
作为别名,原因相同。
以下是简化的头文件,底部声明了 mktime
和 asctime
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"
块声明库函数 asctime
和 mktime
各接受一个参数,即指向可变 StructTM
实例的原始指针。(库函数可能会通过作为参数传递的指针来改变结构。)
剩余代码在 #[test]
属性下,测试时间结构的 Rust 版本的布局。可以使用 cargo test
命令运行测试。问题是 C 没有指定编译器必须如何布局结构的字段。例如,C struct tm
从秒的字段 tm_sec
开始;但是 C 不要求编译后的版本将此字段作为第一个字段。无论如何,Rust 测试应该成功,并且 Rust 对库函数的调用应该按预期工作。
启动并运行第二个示例
从 bindgen
生成的代码不包含 main
函数,因此是一个自然模块。以下是带有 StructTM
初始化以及对 asctime
和 mktime
的调用的 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 代码可以编译(直接使用 rustc
或 cargo
)然后运行。输出为
Ok(
"Mon Feb 1 01:01:01 1901\n",
)
2120218157
对 C 函数 asctime
和 mktime
的调用再次必须发生在 unsafe
块内,因为 Rust 编译器不能对这些外部函数中的任何内存安全问题负责。记录在案,asctime
和 mktime
行为良好。在对这两个函数的调用中,参数是原始指针 ptr
,它保存 sometime
结构的(堆栈)地址。
对 asctime
的调用是两个调用中更棘手的一个,因为此函数返回指向 C char
的指针,即文本输出的 Mon
中的字符 M
。然而,Rust 编译器不知道 C 字符串(以 null 结尾的 char
数组)存储在哪里。在内存的静态区域?在堆上?asctime
函数用于存储时间文本表示形式的数组实际上位于内存的静态区域中。无论如何,C 到 Rust 字符串的转换分两步完成,以避免编译时错误
-
调用
Cstr::from_ptr(char_ptr)
将 C 字符串转换为 Rust 字符串,并返回存储在c_str
变量中的引用。 -
对
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 counterparts。
对于更复杂的交换——特别是 Rust 调用涉及结构和指针的 C 库函数(例如 asctime
和 mktime
)——bindgen
工具是首选方法。此工具生成支持代码以及适当的测试。当然,Rust 编译器不能假定 C 代码在内存安全方面达到 Rust 标准;因此,从 Rust 到 C 的调用必须发生在 unsafe
块中。
4 条评论