Unsafe in Rust

冰岩作坊 December 3, 2022

基本介绍##

Unsafe Rust是Safe Rust的一个超集。在Unsafe Rust中,不会禁用Safe Rust中的安全检查。例如,在unsafe块中同时对变量a进行不可变借用和可变借用,依然会违反借用规则,编译器会报错。Unsafe Rust是指在进行以下五种操作的时候,不会提供任何安全检查:

Unsafe语法

通过unsafe关键字和unsafe块就可以使用Unsafe Rust,他们的作用分别如下:

unsafe关键字

Rust标准库中包含了很多被unsafe关键字标记的函数、方法和trait。如String中实现的以下函数:

1
pub unsafe fn from_utf8_unchecked(bytes: Vec<u8>) -> String }

其函数签名中包含了unsafe关键字,该函数接受一个Vec类型的字节数组,返回一个String类型。虽然,该函数只是简单地返回一个String结构体实例,没有进行我们之前提到的Unsafe Rust中允许的五种操作中的任意一种,完全是正常的Safe Rust代码,而且也在编译器的安全检查之下。这里之所以使用unsafe关键字的原因在于,该函数并未对传入的参数进行合法性验证,如果传入的是一个非法的UTF-8字节序列,则会出现内存不安全的问题,也就是说使用该函数会引入一定风险。这就是此处使用unsafe关键字的含义,如果使用该函数的开发者传入的参数没有满足合法性要求,可以在unsafe标记的范围内排查问题,将风险限定在unsafe标记的范围内。

除了标记函数或方法外,unsafe也可以用于标记trait。标准库中包含的unsafe trait有Send和Sync。编译器依赖Rust内置的类型和内部严格的规则,为开发者自定义的类型自动实现这两个trait,这是Rust保证并发安全的基石。使用unsafe对Send和Sync进行标记,意味着开发者手动实现会有一定风险。在实现unsafe trait的时候,也必须相应的使用unsafe impl。

unsafe块

被unsafe关键字标记的不安全函数或方法只能在unsafe块中被调用。强制使用unsafe块,意味着强制让开发者将unsafe函数的调用和安全代码隔离起来,便于排查错误。

访问和修改可变静态变量

静态变量是全局可访问的,Rust允许定义可变的静态变量。可变的静态变量,需要在unsafe块中进行操作。在和C语言进行交互时,可变静态变量非常有用。

Union联合体

Rust同样提供了类似于C语言那样的Union联合体,Union和Enum相似,Enum属于Tagged Union,优点在于其存储的Tag可以保证内存安全,但是Tag要占用多余的内存空间。而Union并不需要多余的Tag,如果想访问其中的字段,就必须靠程序逻辑来保证其安全性,如果访问错误,就会引发未定义行为。

Union的内存布局和Enum相似,字段共用同一片内存空间,所以也被称为共用体。内存对齐方式也是按照字段中内存占用最大的类型为主。Rust里引入Union的主要问题还是方便了Rust和C语言打交道。

当使用Union联合体时,#[repr(C)]属性是必须的,该属性会告诉Rust编译器,此联合体应该使用和C语言一样的内存布局。如果不加#[repr(C)]属性,则有可能发生未定义行为。

解引用原生指针

Rust提供了const和mut两种指针类型,因为这两种指针和C语言中的指针十分相近,所以叫其原生指针。原生指针的特点如下:

FFI

编程语言之间通过FFI技术进行交互。FFI(Foreign Function Interface,外部函数接口)由Common Lisp语言规范中首次提出,用于规范语言间调用的语言特征。

FFI技术的主要功能就是将一种编程语言的语言和调用约定与另一种编程语言的语义和调用约定相匹配。不管是哪种编程语言,无论是编译执行还是解释执行,最终都会到达处理器指令这个环节。在这个环节所处的层面上,编程语言之间的语法、数据类型等语义差异均以消除,只需要匹配调用约定,这就给编程语言之间的相互调用带来了可能。

应用程序二进制接口

调用约定的匹配,与应用程序二进制接口(ABI)高度相关。

ABI是一个规范,主要涵盖以下内容:

不同的体系结构、操作系统、编程语言、每种编程语言的不同编译器实现基本都有自己规定或者遵循的ABI和调用规范。目前只能通过FFI技术遵循C语言ABI才可以做到编程语言的相互调用。C语言ABI是唯一通用的稳定的标准ABI。Rust提供了FFI技术,允许开发者通过稳定的C-ABI和其他语言进行交互。

在Rust中使用FFI非常简单,只需要通过extern关键字和extern块对FFI接口进行标注即可。在编译时会由LLVM默认生成C-ABI。

链接与Crate Type

有了统一的ABI之后,还需要经过链接才能实现最终的相互调用。链接是将编译单元产生的目标文件按照特定的约定组合在一起,最终生成可执行文件、静态库或动态库。C/C++语言中每个文件都是一个编译单元,编译之后就能产生一个目标文件。Rust和C/C++不同,它以包(crate)为编译单元。在一个Rust包中通过extern crate声明来引入其他包之后,编译器支持各种方法可以将包链接在一起,生成指定的可执行文件、动态库或静态库。

从概念上看,库可以分为两种:静态库和动态库。静态库,可以在链接时引用的代码和数据复制到引用该库的程序中;在格式上,他只是普通目标文件的集合,只是一种简单的拼接。静态库使用简单,原理上容易理解,但是它容易浪费空间。动态库和静态库差异比较大。动态库可以把链接这个过程延迟到运行时进行,比如重定位发生在运行时而非编译时。动态库更加节省空间。除可执行文件外,Rust一共支持四种库,如下图所示:

extern语法

Rust提供了extern语法使得FFI便于使用。