为什么要从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标准,大小至少应为2个字节);然而,今天所有合理的C编译器都支持四字节int和八字节double(在Rust中是f64类),等等。C的FFI面临的另一个挑战是FFI是否可以处理C的原始指针,包括指向被视为字符串的数组的指针。C没有字符串类型,它通过组合字符组和非打印终止符(著名的空终止符)来实现字符串。相反,Rust有两种字符串类型:String和str(字符串切片)。问题是,RustFFI能否将C字符串转换为Rust字符串——答案是肯定的。为了提高效率,结构指针在C中也很常见。当用作函数的参数或返回值时,C结构的默认行为是传递值(即逐字节复制)。C结构,就像它的Rust对应物一样,可以包含数组并嵌套其他结构,因此它的大小是可变的。两种语言中对结构体的最佳使用是通过传递或返回引用,即传递或返回结构体的地址而不是结构体本身的副本。RustFFI再次成功处理了C结构指针,这在C库中无处不在。第一个代码示例侧重于调用相对简单的C库函数,例如abs(绝对值)和sqrt(平方根)。这些函数采用非指针标量参数并返回非指针标量值。第二个代码示例涉及字符串和结构指针。这里我们将介绍bindgen工具,它通过C接口(头文件)生成Rust代码,如math.h和time.h。C头文件声明了C函数的调用语法并定义了将被调用的结构。这两段代码都可以在我的主页上找到。调用一个相对简单的C函数第一个代码示例有四次Rust调用标准数学库中的C函数:两次调用abs(绝对值)和pow(幂),两次重复调用sqrt(平方根).这个程序可以直接用rustc编译器构建,或者更方便的命令cargobuild:usestd::os::raw::c_int;//32位使用std::os::raw::c_double;//64位//从标准库libc导入三个函数。//这里是三个C函数的Rust声明:extern"C"{fnabs(num:c_int)->c_int;fnsqrt(num:c_double)->c_double;fnpow(num:c_double,power:c_double)->c_double;}fnmain(){让x:i32=-123;println!("\n{x}的绝对值为:{}。",unsafe{abs(x)});让n:f64=9.0;让p:f64=3.0;println!("\n{n}的{p}次方是:{}。",unsafe{pow(n,p)});让muty: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::os::raw定义了14种相似的类型以确保与C的兼容性。模块std::ffi中有14种相同的类型定义,以及对字符串的支持。main函数上的extern"C"区域声明了3个将在main函数内部调用的C库函数。每次调用都使用标准C函数名称,但每次调用都必须发生在不安全的区域中。正如每个刚接触Rust的程序员所发现的那样,Rust编译器对内存安全极为严格。其他语言(特别是C和C++)不做同样的保证。不安全区域实际上意味着:Rust不对外部调用中可能出现的不安全行为负责。第一个程序的输出是:-123的绝对值是:123。9的三次方是:729。64的平方根是:8。-3.14的平方根是:NaN。最后一行输出中的NaNRepresentsNotaNumber:C库函数sqrt需要一个非负值作为参数,这使得参数-3.14生成NaN作为返回值。调用涉及指针的C函数C库函数通常出于效率原因在安全、网络、字符串处理、内存管理和其他领域使用指针。例如,库函数asctime(时间作为ASCII字符串)需要一个结构指针作为其参数。从Rust调用像asctime这样的C函数比调用sqrt更棘手,后者既不涉及指针也不涉及结构。函数asctime调用的C结构类型是structtm。指向此结构的指针作为参数传递给库函数mktime(时间作为值)。此结构将时间分解为年、月、小时等单位。该结构体的字段类型为time_t,是int(32位)和long(64位)的别名。两个库函数将这些零散的时间片段组合成一个单一的值:asctime返回一个时间作为字符串,mktime返回一个time_t值表示自“Epoch下面的C程序调用asctime和mktime,并使用其他库函数strftime将mktime的返回值转换为格式化字符串。这个程序可以看作是对应版本Rust的预热:#include#includeintmain(){structtmsometime;/*时间被细分*/charbuffer[80];国际协调中心;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);}返回0;}程序的输出是:Dateandtime:FriFeb101:01:011901ReturnIntegervalue:2120218157更具可读性的版本:FriFeb101:01:011901,然后随机在网上找了一个在线C编译器上网,复制代码得到和这里一样的结果不同但没有错误的结果。不要恐慌。在我的电脑上也是一样的。本机mktime失败的原因是作者没有设置tm_isdst。这是用来标明夏令时的标志。tm_isdst.添加sometime.tm_isdst=0或=-1应该会给出与在线编译器大致相同的结果。不同的是,在结果的第一行,我得到了MonFeb...,对应作者代码中的sometime.tm_wday=1,应该是作者的错误;第二行我从作者那里得到的数字和网上的不一样,这大概是合理的,因为跟机器的时代有关系;第三行我和作者的结果一样,1901年2月1日确实是星期五,这是因为mktime。至于夏令时如何影响mktime,我能找到的是mktime的计算受时区影响,我不知道背后的原因。)一般来说,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和time;Rust更喜欢驼峰命名法,例如TimeT。出于同样的原因,这里选择StructTM作为structtm的别名。这是一个简化的头文件,底部有mktime和asctime:typedefstructtm{inttm_sec;/*秒*/inttm_min;/*分钟*/inttm_hour;/*小时*/inttm_mday;/*天*/inttm_mon;/*月*/inttm_year;/*年份*/inttm_wday;/*星期*/inttm_yday;/*一年中的第几天*/inttm_isdst;/*夏令时*/}StructTM;外部intmktime(StructTM*);externchar*asctime(StructTM*);bindgen安装好后,mytime.h就是上面提到的头文件,下面的命令(%是命令行提示符)可以生成需要的Rust代码并保存到mytime.rs文件中:%bindgenmytime.h>mytime.rs以下是mytime.rs中的重要部分:/*由rust-bindgen0.61.0自动生成*/#[repr(C)]#[derive(Debug,Copy,克隆)]pubstructtm{pubtm_sec:::std::os::raw::c_int,pubtm_min:::std::os::raw::c_int,pubtm_hour:::std::os::raw::c_int,pubtm_mday:::std::os::raw::c_int,pubtm_mon:::std::os::raw::c_int,pubtm_year:::std::os::raw::c_int,pubtm_wday:::std::os::raw::c_int,pubtm_yday:::std::os::raw::c_int,pubtm_isdst:::std::os::raw::c_int,}pub类型StructTM=tm;extern"C"{pubfnmktime(arg1:*mutStructTM)->::std::os::raw::c_int;}extern"C"{pubfnasctime(arg1:*mutStructTM)->*mut::std::os::raw::c_char;}#[test]fnbindgen_test_layout_tm(){constUNINIT:::std::mem::MaybeUninit=::std::mem::MaybeUninit::uninit();让ptr=UNINIT。as_ptr();assert_eq!(::std::mem::size_of::(),36usize,concat!("Sizeof:",stringify!(tm)));...Ruststructstructtm,就像在C中一样,包含九个4字节整数字段,它们的名称在C和Rust中是相同的。extern"C"区域声明库函数astime和mktime只需要一个参数,一个指向StructTM可变实例的原始指针。(库函数可能会改变通过指针作为参数传递的结构。)#[test]属性下的其余代码用于测试时间结构的Rust版本的布局。这些测试可以使用命令cargotest运行。问题是C没有规定编译器应该如何布置结构中的字段。例如C的structtm以字段tm_sec开头,表示秒;但C不要求编译版本遵循此顺序。无论如何,Rust测试应该成功,并且Rust对库函数的调用应该按预期工作。设置第二种情况并开始运行。bindgen生成的代码不包含main函数,自然是一个模块。下面是初始化StructTM并调用asctime和mktime的主要函数:modmytime;使用我的时间::*;使用std::ffi::CStr;fnmain(){letmutsometime=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};不安全{letc_ptr=&mutsometime;//原始指针//调用、转换和拥有//返回的C字符串letchar_ptr=asctime(c_ptr);让c_str=CStr::from_ptr(char_ptr);println!("{:#?}",c_str.to_str());让utc=mktime(c_ptr);println!("{}",UTC);这段Rust代码可以编译(直接使用rustc或使用cargo)并运行。输出是:Ok("MonFeb101:01:011901\n",)2120218157对C函数asctime和mktime的调用必须再次放置在不安全区域,因为Rust编译器不能对这些外部函数的潜在内存安全风险。我在这里声明,asctime和mktime没有安全风险。调用的两个函数的参数是裸指针ptr,它指向某个时刻(在栈中)结构的地址。asctime是调用的两个函数中比较棘手的一个,因为这个函数返回一个指向Cchar的指针,如果函数返回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调用CRustFFI和工具bindgen都非常擅长协助Rust调用C库,包括标准库和第三方库。Rust可以轻松地与C对话,并通过C与其他语言对话。对于像sqrt这样简单的库函数调用,RustFFI的行为很直接,因为Rust的原始数据类型覆盖了它们的C对应类型。对于更复杂的通信——尤其是Rust调用涉及结构和指针的C库函数,如asctime和mktime——bindgen工具是极好的帮手。该工具生成支持代码和所需的测试。当然,Rust编译器不能假设C代码符合Rust的内存安全标准;因此,Rust必须在不安全区域调用C。