文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

从 Rust 调用 C 库函数

2024-12-01 00:54

关注

为什么要从 Rust 调用 C 函数?简短的答案就是软件库。冗长的答案则触及到 C 在众多编程语言中的地位,特别是相对 Rust 而言。C、C++,还有 Rust 都是系统语言,这意味着程序员可以访问机器层面的数据类型与操作。在这三个系统语言中,C 依然占据主导地位。现代操作系统的内核主要是用 C 来写的,其余部分依靠汇编语言补充。在标准系统函数库中,输入与输出、数字处理、加密计算、安全、网络、国际化、字符串处理、内存管理等等,大多都是用 C 来写的。这些函数库所代表的是一个庞大的基础设施,支撑着用其他语言写出来的应用。Rust 发展至今也有着可观的函数库,但是 C 的函数库 —— 自 1970 年代就已存在,迄今还在蓬勃发展 —— 是一种无法被忽视的资源。最后一点是,C 依然还是编程语言中的 ​​通用语​​:大部分语言都可以与 C 交流,透过 C,语言之间可以互相交流。

两个概念证明的例子

Rust 支持 FFI(外部函数接口Foreign Function Interface)用以调用 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​​ 类),以此类推。

针对 C 的 FFI 所面临的另一个挑战是:FFI 是否能够处理 C 的裸指针,包括指向被看作是字符串的数组指针。C 没有字符串类型,它通过结合字符组和一个非打印终止符(大名鼎鼎的 空终止符)来实现字符串。相比之下,Rust 有两个字符串类型:​​String​​​ 和 ​​&str​​ (字符串切片)。问题是,Rust FFI 是否能将 C 字符串转化成 Rust 字符串——答案是 肯定的

出于对效率的追求,结构体指针在 C 中也很常见。一个 C 结构体在作为一个函数的参数或者返回值的时候,其默认行为是传递值(即,逐字节复制)。C 结构体,如同它在 Rust 中的对应部分一样,可以包含数组和嵌套其他结构体,所以其大小是不定的。结构体在两种语言中的最佳用法是传递或返回引用,也就是说,传递或返回结构体的地址而不是结构体本身的副本。Rust FFI 再一次成功处理了 C 的结构体指针,其在 C 函数库中十分普遍。

第一段代码案例专注于调用相对简单的 C 库函数,比如 ​​abs​​​(绝对值)和 ​​sqrt​​​(平方根)。这些函数使用非指针标量参数并返回一个非指针标量值。第二段代码案例则涉及了字符串和结构体指针,在这里会介绍工具 ​​bindgen​​​,其通过 C 接口(头文件)生成 Rust 代码,比如 ​​math.h​​​ 以及 ​​time.h​​​。C 头文件声明了 C 函数的调用语法,并定义了会被调用的结构体。两段代码都能在 ​​我的主页上​​ 找到。

调用相对简单的 C 函数

第一段代码案例有四处 Rust 对标准数学库内的 C 函数的调用:两处分别调用了 ​​abs​​​(绝对值)和 ​​pow​​​(幂),两处重复调用了 ​​sqrt​​​(平方根)。这个程序可以直接用 ​​rustc​​​ 编译器进行构建,或者使用更方便的命令 ​​cargo build​​:

    use std::os::raw::c_int;  // 32位
use std::os::raw::c_double; // 64位
// 从标准库 libc 中引入三个函数。
// 此处是 Rust 对三个 C 函数的声明:
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!("\n{x}的绝对值是: {}.", unsafe { abs(x) });
let n: f64 = 9.0;
let p: f64 = 3.0;
println!("\n{n}的{p}次方是: {}.", unsafe { pow(n, p) });
let mut y: f64 = 64.0;
println!("\n{y}的平方根是: {}.", unsafe { sqrt(y) });
y = -3.14;
println!("\n{y}的平方根是: {}.", unsafe { sqrt(y) }); /
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("日期与时间: %s\n", asctime(&sometime));
utc = mktime(&sometime);
if( utc < 0 ) {
fprintf(stderr, "错误: mktime 无法生成时间\n");
} else {
printf("返回的整数值: %d\n", utc);
strftime(buffer, sizeof(buffer), "%c", &sometime);
printf("更加可读的版本: %s\n", buffer);
}
return 0;
}

程序输出为:

    日期与时间: Fri Feb  1 01:01:01 1901
返回的整数值: 2120218157
更加可读的版本: Fri Feb 1 01:01:01 1901

(LCTT 译注:如果你尝试在自己电脑上运行这段代码,然后得到了一行关于 ​​mktime​​​ 的错误信息,然后又在网上随便找了个在线 C 编译器,复制代码然后得到了跟这里的结果有区别但是没有错误的结果,不要慌,我的电脑上也是这样的。导致本地机器上 ​​mktime​​​ 失败的原因是作者没有设置 ​​tm_isdst​​​,这个是用来标记夏令时的标志。​​tm_isdst​​​。加入 ​​sometime.tm_isdst = 0​​​ 或 ​​= -1​​​ 后应该就能得到跟在线编译器大致一样的结果。不同的地方在于结果第一行我得到的是 ​​Mon Feb ...​​​,这个与作者代码中 ​​sometime.tm_wday = 1​​ 对应,这里应该是作者写错了;第二行我和作者和网上得到的数字都不一样,这大概是合理的,因为这与机器的纪元有关;第三行我跟作者的结果是一样的,1901 年 2 月 1 日也确实是周五,这是因为 ​​mktime​​​。至于夏令时具体是如何影响 ​​mktime​​​ 这个问题,我能查到的只有 ​​mktime​​ 的计算受时区影响,更底层的原因我也不知道了。)

总的来说,Rust 在调用库函数 ​​asctime​​​ 和 ​​mktime​​ 时,必须处理以下两个问题:

Rust 调用 asctime 和 mktime

工具 ​​bindgen​​​ 会根据类似 ​​math.h​​​ 和 ​​time.h​​​ 之类的 C 头文件生成 Rust 支持的代码。下面这个简化版的 ​​time.h​​ 就可以用来做例子,简化版与原版主要有两个不同:

以下是一份简化版的头文件,​​mktime​​​ 和 ​​asctime​​ 在文件底部:

    typedef struct tm {
int tm_sec;
int tm_min;
int tm_hour;
int tm_mday;
int tm_mon;
int tm_year;
int tm_wday;
int tm_yday;
int tm_isdst;
} StructTM;
extern int mktime(StructTM*);
extern char* asctime(StructTM*);

​bindgen​​​ 安装好后,​​mytime.h​​​ 作为以上提到的头文件,以下命令(​​%​​​ 是命令行提示符)可以生成所需的 Rust 代码并将其保存到文件 ​​mytime.rs​​:

% bindgen mytime.h > mytime.rs

以下是 ​​mytime.rs​​ 中的重要部分:

    
#[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 中的一样,包含了 9 个 4 字节的整型字段。这些字段名称在 C 和 Rust 中是一样的。​​extern "C"​​​ 区域声明了库函数 ​​astime​​​ 和 ​​mktime​​​ 分别需要只一个参数,一个指向可变实例 ​​StructTM​​ 的裸指针。(库函数可能会通过指针改变作为参数传递的结构体。)

​#[test]​​​ 属性下的其余代码是用来测试 Rust 版的时间结构体的布局。通过命令 ​​cargo test​​​ 可以进行这些测试。问题在于,C 没有规定编译器应该如何对结构体中的字段进行布局。比如说,C 的 ​​struct tm​​​ 以字段 ​​tm_sec​​ 开头用以表示秒;但是 C 不需要编译版本遵循这个排序。不管怎样,Rust 测试应该会成功,而 Rust 对库函数的调用也应如预期般工作。

设置好第二个案例并开始运行

从 ​​bindgen​​​ 生成的代码不包含 ​​main​​​ 函数,所以是一个天然的模块。以下是一个 ​​main​​​ 函数初始化了 ​​StructTM​​​ 并调用了 ​​asctime​​​ 和 ​​mktime​​:

    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; // 裸指针
// 调用,转化,并拥有
// 返回的 C 字符串
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​​ (在栈stack中)的地址。

​asctime​​​ 是两个函数中调用起来更棘手的那个,因为这个函数返回的是一个指向 C ​​char​​​ 的指针,如果函数返回 ​​Mon​​​ 那么指针就指向 ​​M​​​。但是 Rust 编译器并不知道 C 字符串 (​​char​​​ 的空终止数组)的储存位置。是内存里的静态空间?还是堆heap?​​asctime​​ 函数内用来储存时间的文字表达的数组实际上是在内存的静态空间里。无论如何,C 到 Rust 字符串转化需要两个步骤来避免编译错误:

Rust 代码不会增加从 ​​mktime​​​ 返回的整型值的易读性,这一部分留作课外作业给感兴趣的人去探究。Rust 模板 ​​chrono::format​​​ 也有一个 ​​strftime​​ 函数,它可以被当作 C 的同名函数来使用,两者都是获取时间的文字表达。

使用 FFI 和 bindgen 调用 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。

来源:Linux中国内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯