标题: 从 Rust 调用 C 库函数 [打印本页] 作者: 3AAA 时间: 2022-12-18 23:48 标题: 从 Rust 调用 C 库函数
Rust FFI 和 bindgen 工具是为 Rust 调用 C 库而设计的。Rust 很容易与 C 语言对话,从而与任何其它可以与 C 语言对话的语言对话。
为什么要从 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:s::raw::c_int; // 32位 use std:s::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) }); //** NaN = NotaNumber(不是数字) }
顶部的两个 use声明是 Rust 的数据类型c_int和c_double,对应 C 类型里的int和double。Rust 标准模块std:
s::raw 定义了 14 个类似的类型以确保跟 C 的兼容性。模块 std::ffi中有 14 个同样的类型定义,以及对字符串的支持。
位于 main函数上的extern "C"区域声明了 3 个 C 库函数,这些函数会在main函数内被调用。每次调用都使用了标准的 C 函数名,但每次调用都必须发生在一个unsafe区域内。正如每个新接触 Rust 的程序员所发现的那样,Rust 编译器极度强制内存安全。其他语言(特别是 C 和 C++)作不出相同的保证。unsafe区域其实是说: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是两个函数中调用起来更棘手的那个,因为这个函数返回的是一个指向 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 调用像 asctime和mktime一样,会涉及到结构体和指针的 C 库函数 ——bindgen工具是优秀的帮手。这个工具会生成支持代码以及所需要的测试。当然,Rust 编译器无法假设 C 代码对内存安全的考虑会符合 Rust 的标准;因此,Rust 必须在unsafe区域内调用 C。