为什么要从 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.h
和 time.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_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 字符串)需要指向结构的指针作为其单个参数。 因此,对 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 程序调用 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
总而言之,对库函数 asctime
和 mktime
的 Rust 调用必须处理两个问题
-
将原始指针作为每个库函数的单个参数传递。
-
将从
asctime
返回的 C 字符串转换为 Rust 字符串。
对 asctime
和 mktime
的 Rust 调用
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 对应项。
对于更复杂的互换——特别是 Rust 调用 C 库函数,例如涉及结构体和指针的 asctime
和 mktime
——bindgen
实用程序是不二之选。 该实用程序生成支持代码以及适当的测试。 当然,Rust 编译器不能假设 C 代码在内存安全方面达到 Rust 的标准; 因此,从 Rust 到 C 的调用必须发生在 unsafe
代码块中。
4 条评论