网易首页 > 网易号 > 正文 申请入驻

从 Rust 调用 C 库函数 | Linux 中国

0
分享至

导读:Rust FFI 和 bindgen 工具是为 Rust 调用 C 库而设计的。Rust 很容易与 C 语言对话,从而与任何其它可以与 C 语言对话的语言对话。

本文字数:10066,阅读时长大约: 13分钟

Rust FFI 和 bindgen 工具是为 Rust 调用 C 库而设计的。Rust 很容易与 C 语言对话,从而与任何其它可以与 C 语言对话的语言对话。

为什么要从 Rust 调用 C 函数?简短的答案就是软件库。冗长的答案则触及到 C 在众多编程语言中的地位,特别是相对 Rust 而言。C、C++,还有 Rust 都是系统语言,这意味着程序员可以访问机器层面的数据类型与操作。在这三个系统语言中,C 依然占据主导地位。现代操作系统的内核主要是用 C 来写的,其余部分依靠汇编语言补充。在标准系统函数库中,输入与输出、数字处理、加密计算、安全、网络、国际化、字符串处理、内存管理等等,大多都是用 C 来写的。这些函数库所代表的是一个庞大的基础设施,支撑着用其他语言写出来的应用。Rust 发展至今也有着可观的函数库,但是 C 的函数库 —— 自 1970 年代就已存在,迄今还在蓬勃发展 —— 是一种无法被忽视的资源。最后一点是,C 依然还是编程语言中的 baike.baidu.com:大部分语言都可以与 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(平方根)。这些函数使用非指针标量参数并返回一个非指针标量值。第二段代码案例则涉及了字符串和结构体指针,在这里会介绍工具 github.com,其通过 C 接口(头文件)生成 Rust 代码,比如math.h以及time.h。C 头文件声明了 C 函数的调用语法,并定义了会被调用的结构体。两段代码都能在 condor.depaul.edu 找到。

调用相对简单的 C 函数

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

  1. use std::os::raw::c_int; // 32位

  2. use std::os::raw::c_double; // 64位

  3. // 从标准库 libc 中引入三个函数。

  4. // 此处是 Rust 对三个 C 函数的声明:

  5. extern "C" {

  6. fn abs(num: c_int) -> c_int;

  7. fn sqrt(num: c_double) -> c_double;

  8. fn pow(num: c_double, power: c_double) -> c_double;

  9. }

  10. fn main() {

  11. let x: i32 = -123;

  12. println!("\n{x}的绝对值是: {}.", unsafe { abs(x) });

  13. let n: f64 = 9.0;

  14. let p: f64 = 3.0;

  15. println!("\n{n}的{p}次方是: {}.", unsafe { pow(n, p) });

  16. let mut y: f64 = 64.0;

  17. println!("\n{y}的平方根是: {}.", unsafe { sqrt(y) });

  18. y = -3.14;

  19. println!("\n{y}的平方根是: {}.", unsafe { sqrt(y) }); //** NaN = NotaNumber(不是数字)

  20. }

顶部的两个use声明是 Rust 的数据类型c_intc_double,对应 C 类型里的intdouble。Rust 标准模块std::os::raw定义了 14 个类似的类型以确保跟 C 的兼容性。模块std::ffi中有 14 个同样的类型定义,以及对字符串的支持。

位于main函数上的extern "C"区域声明了 3 个 C 库函数,这些函数会在main函数内被调用。每次调用都使用了标准的 C 函数名,但每次调用都必须发生在一个unsafe区域内。正如每个新接触 Rust 的程序员所发现的那样,Rust 编译器极度强制内存安全。其他语言(特别是 C 和 C++)作不出相同的保证。unsafe区域其实是说:Rust 对外部调用中可能存在的不安全行为不负责。

第一个程序输出为:

  1. -123的绝对值是: 123.

  2. 9的3次方是: 729.

  3. 64的平方根是: 8.

  4. -3.14的平方根是: NaN.

输出的最后一行的NaN表示不是数字(Not a Number):C 库函数sqrt期待一个非负值作为参数,这使得参数-3.14生成了NaN作为返回值。

调用涉及指针的 C 函数

C 库函数为了提高效率,经常在安全、网络、字符串处理、内存管理,以及其他领域中使用指针。例如,库函数asctime(ASCII 字符串形式的时间)期待一个结构体指针作为其参数。Rust 调用类似asctime的 C 函数就会比调用sqrt要更加棘手一些,后者既没有牵扯到指针,也不涉及到结构体。

函数asctime调用的 C 结构体类型为struct tm。一个指向此结构体的指针会作为参数被传递给库函数mktime(时间作为值)。此结构体会将时间拆分成诸如年、月、小时之类的单位。此结构体的字段(field)类型为time_t,是int(32位)和long(64 位)的别名。两个库函数将这些破碎的时间片段组合成了一个单一值:asctime返回一个以字符串表示的时间,而mktime返回一个time_t值表示自 “ baike.baidu.com(Epoch) 以来所经历的秒数,这是一个系统的时钟和时间戳的相对时间。典型的纪元设置为 1900 年或 1970 年,1 月 1 日 0 时 0 分 0 秒。(LCTT 校注:Unix、Linux 乃至于如今所有主要的计算机和网络的时间纪元均采用 1970 年为起点。)

以下的 C 程序调用了asctimemktime,并使用了其他库函数strftime来将mktime的返回值转化成一个格式化的字符串。这个程序可被视作 Rust 对应版本的预热:

  1. #include

  2. #include

  3. int main () {

  4. struct tm sometime; /* 时间被打破细分 */

  5. char buffer[80];

  6. int utc;

  7. sometime.tm_sec = 1;

  8. sometime.tm_min = 1;

  9. sometime.tm_hour = 1;

  10. sometime.tm_mday = 1;

  11. sometime.tm_mon = 1;

  12. sometime.tm_year = 1; /*LCTT 校注:注意,相对于 1900 年的年数*/

  13. sometime.tm_hour = 1;

  14. sometime.tm_wday = 1;

  15. sometime.tm_yday = 1;

  16. printf("日期与时间: %s\n", asctime(&sometime));

  17. utc = mktime(&sometime);

  18. if( utc < 0 ) {

  19. fprintf(stderr, "错误: mktime 无法生成时间\n");

  20. } else {

  21. printf("返回的整数值: %d\n", utc);

  22. strftime(buffer, sizeof(buffer), "%c", &sometime);

  23. printf("更加可读的版本: %s\n", buffer);

  24. }

  25. return 0;

  26. }

程序输出为:

  1. 日期与时间: Fri Feb 1 01:01:01 1901

  2. 返回的整数值: 2120218157

  3. 更加可读的版本: Fri Feb 1 01:01:01 1901

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

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

◈ 将裸指针作为唯一参数传递给每个库函数。

◈ 把从asctime返回的 C 字符串转化为 Rust 字符串。

Rust 调用 asctime 和 mktime

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

◈ 内置类型int被用来取代别名类型time_t。工具 bindgen 可以处理time_t类,但是会生成一些烦人的警告,因为time_t不符合 Rust 的命名规范:time_t以下划线区分timet;Rust 更偏好驼峰式命名方法,比如TimeT

◈ 出于同样的原因,这里选择StructTM作为struct tm的别名。

以下是一份简化版的头文件,mktimeasctime在文件底部:

  1. typedef struct tm {

  2. int tm_sec; /* 秒 */

  3. int tm_min; /* 分钟 */

  4. int tm_hour; /* 小时 */

  5. int tm_mday; /* 日 */

  6. int tm_mon; /* 月 */

  7. int tm_year; /* 年 */

  8. int tm_wday; /* 星期 */

  9. int tm_yday; /* 一年中的第几天 */

  10. int tm_isdst; /* 夏令时 */

  11. } StructTM;

  12. extern int mktime(StructTM*);

  13. extern char* asctime(StructTM*);

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

  1. % bindgen mytime.h > mytime.rs

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

  1. /* automatically generated by rust-bindgen 0.61.0 */

  2. #[repr(C)]

  3. #[derive(Debug, Copy, Clone)]

  4. pub struct tm {

  5. pub tm_sec: ::std::os::raw::c_int,

  6. pub tm_min: ::std::os::raw::c_int,

  7. pub tm_hour: ::std::os::raw::c_int,

  8. pub tm_mday: ::std::os::raw::c_int,

  9. pub tm_mon: ::std::os::raw::c_int,

  10. pub tm_year: ::std::os::raw::c_int,

  11. pub tm_wday: ::std::os::raw::c_int,

  12. pub tm_yday: ::std::os::raw::c_int,

  13. pub tm_isdst: ::std::os::raw::c_int,

  14. }

  15. pub type StructTM = tm;

  16. extern "C" {

  17. pub fn mktime(arg1: *mut StructTM) -> ::std::os::raw::c_int;

  18. }

  19. extern "C" {

  20. pub fn asctime(arg1: *mut StructTM) -> *mut ::std::os::raw::c_char;

  21. }

  22. #[test]

  23. fn bindgen_test_layout_tm() {

  24. const UNINIT: ::std::mem::MaybeUninit = ::std::mem::MaybeUninit::uninit();

  25. let ptr = UNINIT.as_ptr();

  26. assert_eq!(

  27. ::std::mem::size_of::(),

  28. 36usize,

  29. concat!("Size of: ", stringify!(tm))

  30. );

  31. ...

Rust 结构体struct tm,跟原本在 C 中的一样,包含了 9 个 4 字节的整型字段。这些字段名称在 C 和 Rust 中是一样的。extern "C"区域声明了库函数astimemktime分别需要只一个参数,一个指向可变实例StructTM的裸指针。(库函数可能会通过指针改变作为参数传递的结构体。)

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

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

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

  1. mod mytime;

  2. use mytime::*;

  3. use std::ffi::CStr;

  4. fn main() {

  5. let mut sometime = StructTM {

  6. tm_year: 1,

  7. tm_mon: 1,

  8. tm_mday: 1,

  9. tm_hour: 1,

  10. tm_min: 1,

  11. tm_sec: 1,

  12. tm_isdst: -1,

  13. tm_wday: 1,

  14. tm_yday: 1

  15. };

  16. unsafe {

  17. let c_ptr = &mut sometime; // 裸指针

  18. // 调用,转化,并拥有

  19. // 返回的 C 字符串

  20. let char_ptr = asctime(c_ptr);

  21. let c_str = CStr::from_ptr(char_ptr);

  22. println!("{:#?}", c_str.to_str());

  23. let utc = mktime(c_ptr);

  24. println!("{}", utc);

  25. }

  26. }

这段 Rust 代码可以被编译(直接用rustc或使用cargo)并运行。输出为:

  1. Ok(

  2. "Mon Feb 1 01:01:01 1901\n",

  3. )

  4. 2120218157

对 C 函数asctimemktime的调用必须再一次被放在unsafe区域内,因为 Rust 编译器无法对这些外部函数的潜在内存安全风险负责。此处声明一下,asctimemktime并没有安全风险。调用的两个函数的参数是裸指针ptr,其指向结构体sometime(在栈(stack)中)的地址。

asctime是两个函数中调用起来更棘手的那个,因为这个函数返回的是一个指向 Cchar的指针,如果函数返回Mon那么指针就指向M。但是 Rust 编译器并不知道 C 字符串 (char的空终止数组)的储存位置。是内存里的静态空间?还是堆(heap)?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 调用像asctimemktime一样,会涉及到结构体和指针的 C 库函数 ——bindgen工具是优秀的帮手。这个工具会生成支持代码以及所需要的测试。当然,Rust 编译器无法假设 C 代码对内存安全的考虑会符合 Rust 的标准;因此,Rust 必须在unsafe区域内调用 C。

via:

作者: 选题: 译者: 校对:

本文由 原创编译, 荣誉推出

LCTT 译者 :yzuowei

翻译: 3.0 篇

贡献: 4 天

2022-12-08

2022-12-12

https://linux.cn/lctt/yzuowei

欢迎遵照 CC-BY-SA 协议规定转载,

如需转载,请在文章下留言 “ 转载:公众号名称”,

我们将为您添加白名单,授权“ 转载文章时可以修改”。

特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相关推荐
热点推荐
霍启刚47岁生日,晒郭晶晶手捧蛋糕合照,动情发文:感恩一路有你

霍启刚47岁生日,晒郭晶晶手捧蛋糕合照,动情发文:感恩一路有你

叨唠
2026-07-04 03:43:18
中国男篮73-92日本,赛后球员表现评分:2满分3优秀 2及格

中国男篮73-92日本,赛后球员表现评分:2满分3优秀 2及格

画夕
2026-07-04 01:07:48
申裕斌颁奖笑成花儿!混双第9冠无愧世界第一 韩媒:越过乒坛高墙

申裕斌颁奖笑成花儿!混双第9冠无愧世界第一 韩媒:越过乒坛高墙

颜小白的篮球梦
2026-07-04 19:21:55
4日战报!国乒男双夺冠,世界冠军1-3爆冷,孙颖莎下轮对手敲定

4日战报!国乒男双夺冠,世界冠军1-3爆冷,孙颖莎下轮对手敲定

越岭寻踪
2026-07-04 07:05:46
养老金调整真相:企业退休涨幅高,事业退休涨钱多

养老金调整真相:企业退休涨幅高,事业退休涨钱多

职场资深秘书
2026-07-04 18:44:11
上海女子离婚20多年,户口仍挂前夫公房!征收拿200万后反悔

上海女子离婚20多年,户口仍挂前夫公房!征收拿200万后反悔

新民晚报
2026-07-04 20:13:26
神舟二十三号香港女航天员:失重环境隐患重重,如何保障隐私?

神舟二十三号香港女航天员:失重环境隐患重重,如何保障隐私?

古史青云啊
2026-07-04 09:34:58
死守二十天终落败,城内守军全部阵亡,顿巴斯决战箭在弦上

死守二十天终落败,城内守军全部阵亡,顿巴斯决战箭在弦上

安珈使者啊
2026-07-03 09:27:41
人过80岁,哪怕身体再健康,也要记住这四句话,晚年远离病痛

人过80岁,哪怕身体再健康,也要记住这四句话,晚年远离病痛

观星赏月
2026-07-03 12:03:48
重磅突发!这个板块马上大涨!重仓重仓!

重磅突发!这个板块马上大涨!重仓重仓!

星图金融研究院
2026-07-04 15:09:13
弹劾前夜突变!马科斯连拒露面,莎拉为一人掀桌,菲军方全线集结

弹劾前夜突变!马科斯连拒露面,莎拉为一人掀桌,菲军方全线集结

月下守候
2026-07-04 05:49:54
世界杯1/8决赛对阵出炉,夺冠赔率法国领跑,阿根廷第2葡萄牙第5

世界杯1/8决赛对阵出炉,夺冠赔率法国领跑,阿根廷第2葡萄牙第5

智道足球
2026-07-04 19:04:19
美国副国务卿:全球唯一能和中国相抗衡的,显然是印度而不是美国

美国副国务卿:全球唯一能和中国相抗衡的,显然是印度而不是美国

知法而形
2026-07-03 09:29:07
凌晨1点,72岁濮存昕用一根布绳,将自己和94岁老母狠狠绑在一起

凌晨1点,72岁濮存昕用一根布绳,将自己和94岁老母狠狠绑在一起

小椰的奶奶
2026-06-15 07:39:15
世纪婚礼落地!泰勒·斯威夫特官宣大婚,捐1.7亿超婚礼开销

世纪婚礼落地!泰勒·斯威夫特官宣大婚,捐1.7亿超婚礼开销

雅儿姐游世界
2026-07-04 14:47:40
NBA一夜变天!8笔签约两大交易诞生,詹姆斯6选1,湖人火箭成输家

NBA一夜变天!8笔签约两大交易诞生,詹姆斯6选1,湖人火箭成输家

老侃侃球
2026-07-04 10:52:59
世界杯也救不了中国电视市场!一季度全球增长6%,中国依然萎靡

世界杯也救不了中国电视市场!一季度全球增长6%,中国依然萎靡

杰夫视点
2026-07-03 22:15:49
网传烟草行业进入艰难时刻,难道他们也要裁员,评论区炸锅…

网传烟草行业进入艰难时刻,难道他们也要裁员,评论区炸锅…

慧翔百科
2026-07-02 17:43:15
揭秘:马英九三姐马冰如,在北京待了26年,她到底有什么故事?

揭秘:马英九三姐马冰如,在北京待了26年,她到底有什么故事?

杜榈手工制作
2026-07-04 15:32:40
大婚风波未过,香港政府“点名”霍启山,原来他和霍启刚处境一样

大婚风波未过,香港政府“点名”霍启山,原来他和霍启刚处境一样

翰飞观事
2026-07-03 22:02:57
2026-07-04 20:52:49
Linux
Linux
Linux 中国开源社区
8018文章数 73112关注度
往期回顾 全部

科技要闻

韬定律论文V2版,充工程细节和实测数据

头条要闻

数百人在开放水域体验桨板 专家:再不治理迟早出大事

头条要闻

数百人在开放水域体验桨板 专家:再不治理迟早出大事

体育要闻

揭法国锋线最大优势 有人比姆巴佩还快?

娱乐要闻

白鹿打戏抠图惹非议 连累丞磊遭扒皮

财经要闻

韩国股市杠杆失控:450亿美元资金狂飙

汽车要闻

方程豹钛9内饰曝光 用上了长联屏设计/下半年上市

态度原创

旅游
手机
时尚
公开课
军事航空

旅游要闻

萤火虫撬动大文旅 深圳这个暗夜社区今年已接待超90万人次

手机要闻

W26排名分析:vivo、OPPO位置互换,华为、苹果霸占前二

别再说"露肩衣服 "难驾驭!看看这几组日常穿搭,大方有回头率

公开课

李玫瑾:为什么性格比能力更重要?

军事要闻

普京宣布俄军“完全解放”卢甘斯克

无障碍浏览 进入关怀版