
19.1 摆脱安全性限制的unsafe Rust19.1.1 什么是 unsafe Rust到目前为止我们讨论过的所有代码都在编译时强制执行 Rust 的内存安全保证。然而Rust 内部还隐藏着第二种语言它并不强制执行这些内存安全保证。它被称为unsafe Rust。它和普通 Rust 一样但给了我们额外的“超能力”。unsafe Rust之所以存在是因为- 静态分析非常保守。编译器在判断一段代码是否安全时宁可拒绝一个实际能正确运行的程序也不会放过任何潜在不安全的代码。- 计算机硬件本身就是不安全的如果 Rust 想达到和 C 一样的底层能力就需要unsafe Rust。换句话说unsafe Rust允许进行底层系统编程。使用unsafe Rust就是在告诉编译器“我知道自己在做什么并接受相应风险。”19.1.2 Unsafe Rust 的超能力使用unsafe关键字切换到unsafe Rust。它会开启一个代码块写在这个块里的就是不安全代码。unsafe Rust可以做五件事也就是它的超能力- 解引用原始指针- 调用不安全的函数或方法- 访问或修改可变静态变量- 实现不安全的 trait- 访问union的字段注意-unsafe Rust并不会关闭借用检查器也不会停用其它安全检查。如果你在代码里使用引用这些引用仍然会被检查。unsafe关键字只是允许你执行上面那五个编译器不会替你做内存检查的操作。所以即便在unsafe块中你仍然保留一部分安全保证。- 任何与内存安全相关的错误都必须留在unsafe块里。- 尽可能隔离不安全代码。理想情况下把它封装在安全的抽象中并提供安全的 API。标准库中有些代码在内部使用了unsafe块但在其上提供了安全抽象。这可以有效防止不安全代码泄漏到调用方因为使用安全抽象是安全的无论内部是否使用了unsafe Rust。特性 1解引用原始指针unsafe Rust提供了两种与引用类似的指针类型称为原始指针raw pointers。只有在解引用原始指针时才需要unsafe块因为这时可能出问题。创建原始指针本身不会造成问题因此不必放在unsafe块中。和引用一样原始指针可以是可变的或不可变的- 可变*mut T- 不可变*const T*const T表示这个指针可以被解引用但不能通过该指针给指向的值赋值。注意这里的*是类型的一部分不是解引用运算符。*const T这三个记号合在一起才是一个类型例如*const String。*const T和*mut T的差别很小可以彼此自由转换。Rust 引用mut T和T在编译期间会被编译器转换成原始指针这意味着无需进入unsafe块就能获得原始指针的性能。引用和原始指针的区别是- 原始指针允许你忽略借用规则可以同时拥有不可变指针和可变指针或多个指向同一位置的可变指针。- 原始指针不能保证指向有效内存而引用可以。- 原始指针可以为null。- 原始指针不实现任何自动清理。如果放弃安全保证就可以换取更好的性能以及与其它语言或硬件接口的互操作性。看一个例子fn main() { let mut num 5; let r1 num as *const i32; let r2 mut num as *mut i32; }这是一个创建原始指针的例子。在main中我们同时创建了一个不可变原始指针和一个可变原始指针。这段代码不在unsafe块中但仍然可以编译。所以我们可以在不安全代码之外创建原始指针但解引用它们只能在unsafe代码中进行。这段代码在同一个作用域里同时存在指向同一块内存区域的可变指针和不可变指针而 Rust 允许这样做。这意味着我们可以通过可变引用修改值但必须非常小心。创建原始指针时我们先用引用语法写出它们再用as *const和as *mut转换成对应的原始指针。因为这两个原始指针来自有效引用所以我们知道它们是有效的但它们未必会一直有效。接下来我们创建一个无法保证有效性的原始指针fn main() { let address 0x012345usize; let r address as *const i32; }我们直接根据内存地址写出一个指针。那个地址上可能有数据也可能没有但我们仍然可以创建原始指针。编译器不会报错。现在尝试解引用这些原始指针fn main() { let mut num 5; let r1 num as *const i32; let r2 mut num as *mut i32; println!(r1 is: {}, *r1); println!(r2 is: {}, *r2); }这会产生错误dereference of raw pointer is unsafe and requires unsafe function or block意思是原始指针的解引用只能在不安全函数或不安全块中进行。把原始指针的解引用放进unsafe块就可以了fn main() { let mut num 5; let r1 num as *const i32; let r2 mut num as *mut i32; unsafe { println!(r1 is: {}, *r1); println!(r2 is: {}, *r2); } }那对直接根据内存地址创建原始指针的例子这样做也行吗fn main() { let address 0x012345usize; let r address as *const i32; unsafe { println!(r {}, *r); } }输出$ cargo run Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example) Finished dev profile [unoptimized debuginfo] target(s) in 0.08s Running target/debug/unsafe-example thread main (483665) panicked at src/main.rs:5:9: misaligned pointer dereference: address must be a multiple of 0x4 but is 0x12345 note: run with RUST_BACKTRACE1 environment variable to display a backtrace thread caused non-unwinding panic. aborting.创建原始指针本身没问题但解引用一个任意地址属于未定义行为。那个地址上可能有有效数据也可能没有编译器可能把这次访问优化掉程序也可能崩溃——例如上面本地运行中的未对齐指针 panic 并 abort退出代码 134或段错误退出代码 139 /SIGSEGV。具体表现会随系统、编译器和构建选项而变化。你可以在自己的电脑上试试。既然原始指针这么危险为什么还要用它们原因是- 与 C 进行接口交互- 构建借用检查器无法理解的安全抽象特性 2调用不安全的函数和方法不安全函数和方法是用unsafe关键字声明的函数或方法。除此之外它们与普通函数或方法没有太大区别。在调用这类函数或方法之前你必须手动满足一些条件通常要靠阅读文档因为 Rust 无法替你验证这些条件。此外调用不安全函数或方法必须发生在unsafe块中。看一个例子unsafe fn dangerous() {} fn main() { unsafe { dangerous(); } }我们用unsafe关键字声明了一个dangerous函数所以它是不安全函数。这意味着main必须在unsafe块中调用它。函数内部包含不安全代码并不意味着整个函数都必须标记为不安全。事实上把不安全代码封装在安全函数中是一种常见的抽象。例如fn split_at_mut(values: mut [i32], mid: usize) - (mut [i32], mut [i32]) { let len values.len(); assert!(mid len); (mut values[..mid], mut values[mid..]) } fn main() { let mut v vec![1, 2, 3, 4, 5, 6]; let r mut v[..]; let (a, b) r.split_at_mut(3); assert_eq!(a, mut [1, 2, 3]); assert_eq!(b, mut [4, 5, 6]); }在main中有一个名为v的Vec。r是它的完整可变切片然后对r调用了split_at_mut。split_at_mut接收一个元素类型为i32的切片self和一个usize值。它把这个usize当作把self切成两个可变切片的索引。在函数体内它先检查传入的usize是否在有效范围内不大于self的长度然后返回前半段和后半段。输出$ cargo run Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example) error[E0499]: cannot borrow *values as mutable more than once at a time -- src/main.rs:6:31 | 1 | fn split_at_mut(values: mut [i32], mid: usize) - (mut [i32], mut [i32]) { | - lets call the lifetime of this reference 1 ... 6 | (mut values[..mid], mut values[mid..]) | --------------------------^^^^^^-------- | | | | | | | second mutable borrow occurs here | | first mutable borrow occurs here | returning this value requires that *values is borrowed for 1 | help: use .split_at_mut(position) to obtain two mutable non-overlapping sub-slices For more information about this error, try rustc --explain E0499. error: could not compile unsafe-example (bin unsafe-example) due to 1 previous errorRust 的借用检查器无法理解我们借用的是切片中两个不同且互不重叠的部分。它只知道我们从同一个切片借用了两次。所以我们需要使用unsafe块同时保持外层函数是安全的use std::slice; fn split_at_mut(values: mut [i32], mid: usize) - (mut [i32], mut [i32]) { let len values.len(); let ptr values.as_mut_ptr(); assert!(mid len); unsafe { ( slice::from_raw_parts_mut(ptr, mid), slice::from_raw_parts_mut(ptr.add(mid), len - mid), ) } }as_mut_ptr返回一个原始指针具体是*mut i32。元组返回值使用了unsafe块、原始指针和指针运算。slice模块中的slice::from_raw_parts_mut接收一个原始指针ptr和一个长度mid来创建切片slice::from_raw_parts_mut(ptr, mid)创建一个从ptr开始、包含mid个元素的切片。slice::from_raw_parts_mut(ptr.add(mid), len - mid)创建一个从ptr.add(mid)开始、包含len - mid个元素的切片——也就是从ptr往后偏移mid个元素的位置正好是第一个切片的结尾。这个函数使用了unsafe块但它本身并没有标记为unsafe。这就是对不安全代码的安全抽象。如果我们不使用安全抽象呢use std::slice; fn main() { let address 0x01234usize; let r address as *mut i32; let values: mut [i32] unsafe { slice::from_raw_parts_mut(r, 10000) }; }我们不一定拥有这个任意地址上的内存也无法保证这段代码创建的切片包含有效的i32值。试图把values当作有效切片使用可能导致未定义行为。使用extern调用外部代码或被外部代码调用extern关键字简化了定义和使用外部函数接口Foreign Function InterfaceFFI的过程。FFI 允许一种编程语言定义函数并让其它编程语言调用这些函数。看一个例子extern C { fn abs(input: i32) - i32; } fn main() { unsafe { println!(Absolute value of -3 according to C: {}, abs(-3)); } }在extern块中声明的任何函数都是不安全的因为其它语言不会强制执行 Rust 的规则而 Rust 也无法检查它们。所以调用外部函数被隐式标记为不安全安全责任落在开发者身上。在extern C块中我们列出想要调用的另一种语言中外部函数的名称和签名。C部分定义了外部函数使用的应用程序二进制接口Application Binary InterfaceABI。ABI 定义了在汇编层面如何调用该函数。CABI 最常见它遵循 C 编程语言的 ABI。既然 Rust 可以调用其它编程语言的函数那其它编程语言能否调用 Rust 代码答案是可以。我们可以使用extern创建一个可供其它语言调用的接口。为此在fn前添加extern关键字并指定 ABI。你还需要#[no_mangle]属性这样 Rust 就不会在编译期间改变函数名。mangle指的是编译中的一个阶段编译器会修改函数名使其包含更多供后续编译阶段使用的信息。这些改名后的名字通常很难阅读所以如果你希望其它语言能正常使用该函数就必须阻止 Rust 改名。看一个例子#[no_mangle] pub extern C fn call_from_c() { println!(Just called a Rust function from C!); }特性 3访问或修改可变静态变量Rust 支持全局变量但所有权规则可能带来一些问题例如数据竞争。Rust 中的全局变量叫做静态变量。它们用static关键字声明遵循 UPPER_SNAKE_CASE 命名约定并且在声明时必须标注类型。它们的生命周期是且只能是static表示在整个程序运行期间都有效。你不必显式写出这一点Rust 会自行推断。访问不可变静态变量是安全的。例如static HELLO_WORLD: str Hello, world!; fn main() { println!(name is: {HELLO_WORLD}); }HELLO_WORLD是声明的全局变量值为Hello, world!类型是字符串切片str。main打印了这个全局变量。常量const和可变静态变量static mut的区别是- 静态变量有固定的内存地址因此使用它们的值时总会访问同一份数据。- 常量在使用时会被复制。- 静态变量可以是可变的而访问或修改可变静态变量是不安全的所以这些操作必须发生在unsafe块中。例如static mut COUNTER: u32 0; fn add_to_count(inc: u32) { unsafe { COUNTER inc; } } fn main() { add_to_count(3); unsafe { println!(COUNTER: {COUNTER}); } }访问和修改都是不安全操作所以两者都被放在unsafe块中。这里的输出显然是 3。但如果涉及多个线程就很容易引入数据竞争。在多线程代码中最好使用我们之前讨论过的并发技术或像ArcT这样的线程安全智能指针这样编译器就能安全地检查跨线程的数据访问。特性 4实现不安全的 trait当一个 trait 中至少有一个方法包含编译器无法验证的不安全因素时这个 trait 就被认为是不安全的。声明不安全 trait 的方式是在trait定义前加上unsafe关键字。这样的 trait 只能在unsafe块中实现。例如unsafe trait Foo { // methods go here } unsafe impl Foo for i32 { // method implementations go here } fn main() {}unsafe trait Foo声明了一个名为Foo的不安全 trait。为i32实现Foo必须发生在unsafe块中因此需要unsafe impl。特性 5访问union字段union类似于struct但在给定实例中一次只使用一个已声明的字段。union主要用于与 C 代码中的union互操作。访问union字段是不安全的因为 Rust 无法保证当前存储在union实例中的数据类型。详情见 Rust Reference。19.1.3 何时使用unsafe代码确保unsafe代码正确是棘手的因为编译器无法帮助维护内存安全而开发者自己也很难保证正确性。当你有充分理由时再使用unsafe代码。显式的unsafe标注会让问题发生时更容易追踪根源。