不安全 Rust
因为底层计算机硬件固有的不安全性。如果 rust 不允许进行不安全的操作,那么某些底层任务可能根本就完成不了,rust 拥有”不安全超能力“来与操作系统进行操作,本人的理解 rust 本身是比 C、C++等自由度高的语言多了一层安全的封装,但是呢安全是有条件限制的,很多功能无法实现,此时就需要摆脱这层限制,用“不安全的 rust”来实现。
解引用裸指针
不安全 Rust 的世界里拥有两种类似于引用的新指针类型,它们都被叫作裸指针(raw pointer,有的地方也叫做原生指针),与引用类似,裸指针同样分为两种:
// 不可变裸指针
*const T
// 可变裸指针
*mut T
复制代码
注意裸指针开头的星号是类型名的一部分而不是解引用操作
裸指针与引用、智能指针的区别:
创建不可变裸指针
可以使用类型声明和类型转换的方式创建裸指针:
let num = 5;
// 类型声明不可变裸指针
let r1: *const i32 = #
// 使用as操作符将引用转换为裸指针:
let r2 = &num as *const i32;
println!("r1内存地址: {:?}", r1); // r1内存地址: 0x7ffee6b7323c
println!("r2内存地址: {:?}", r2); // r2内存地址: 0x7ffee6b7323c
复制代码
因为r1``和
r2都是通过
num```创建的,所以内存地址是一样的。
创建可变裸指针
可变裸指针同上面不可变裸指针差不多,只是把const
换成mut
:
let r1: *mut i32 = &mut num;
// 等价:
let r2 = &mut num as *mut i32;
println!("r1内存地址: {:?}", r1); // r1内存地址: 0x7ffee6b7323c
println!("r2内存地址: {:?}", r2); // r2内存地址: 0x7ffee6b7323c
复制代码
创建指向任意内存地址的裸指针
我们可以直接将一个数字转为裸指针:
// 0x12345是16进制数,当然也可以写成十进制74565
let address = 0x12345usize;
let r1 = &address as *const usize;
println!("r1内存地址{:?}", r1); // r1内存地址: 0x7ffee6b73380
// 当然可变引用裸引用也是可以的,
// 这里使用十进制i32类型,这里只是想体现用什么数字类型都可以
let mut address2 = 74565i32;
let r2 = &mut address2 as *mut i32;
println!("r2内存地址{:?}", r2); // r2内存地址: 0x7ffee6b73380
复制代码
裸指针解引用
裸指针支持解引用,用于获取指针对应的真实数据,但是需要使用unsafe
块:
let mut num = 5;
let r = &num as *const i32;
// 查看地址
println!("{:?}", r); // 0x7ffee6b733dc
// 解引用获取数据
println!("{}", *r); // 报错,解引用裸指针需要使用unsafe块
// 在unsafe块中解引用裸指针
unsafe {
println!("{:?}", *r); // 5
}
复制代码
调用不安全函数或方法
不安全函数定义时前面也需要加unsafe
关键字,意味着函数中有不安全的操作:
unsafe fn dangerous() -> i32 {
let mut num = 5;
let r = &num as *const i32;
return *r
}
dangerous(); // 报错,不允许unsafe块外部调用不安全函数
// 在unsafe块中调用
unsafe {
let num = dangerous();
println!("{}", num); // 5
}
复制代码
创建不安全代码的安全抽象
函数中包含不安全代码并不意味着我们需要将整个函数都标记为不安全的。实际上,将不安全代码封装在安全函数中是一种十分常见的抽象,例如下面使用安全的split_at_mut
函数:
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]);
复制代码
上面代码中使用split_at_mut
函数将一个数组引用按照索引分割成了数组的两个可变引用,如果在安全的 rust 中我们是无法实现这个函数的:
fn split_at_mut(slice: &mut [i32], index: usize) -> (&mut [i32], &mut [i32]) {
let a = &mut slice[..index];
let b = &mut slice[index..]; // 报错,不能多次利用可变引用借用slice
return (a, b)
}
复制代码
上面代码中因为 rust 不允许我们同时对一个数据创建两个可变引用,所以报错了,此时我们就可以利用unsafe
块实现:
fn split_at_mut(slice: &mut [i32], index: usize) -> (&mut [i32], &mut [i32]) {
// 切片引用由开始位置和长度组成
// 获取切片的长度
let len = slice.len();
// 获取切片开始位置的裸指针
let ptr = slice.as_mut_ptr();
// 确保分片位置不能大于slice长度
assert!(index <= len);
// slice在内存中的开始位置地址
println!("内存地址: {:?}", ptr); // 内存地址: 0x7ff765c05b20
unsafe {
use std::slice;
// 对slice的开始位置解引用,可以获得第一个元素
println!("第一个元素: {}", *ptr); // 第一个元素: 6
// 第一个片段,通过裸指针位置,直接在内存中获取指定长度的切片
let a = slice::from_raw_parts_mut(ptr, index);
println!("a: {:?}", a); // a: [6, 5, 4]
// 第二个片段的开始位置,需要将裸指针指向的内存地址向后移动index位,即:第一个片段的长度
let move_to_index = ptr.offset(index as isize);
// 通过len - index获取完整切片的剩余长度,作为第二个切片的长度
let b = slice::from_raw_parts_mut(move_to_index, len - index);
println!("b: {:?}", b); // b: [3, 2, 1]
(a, b)
}
}
let mut v = vec![6, 5, 4, 3, 2, 1];
let r = &mut v[..];
let (a, b) = split_at_mut(r,3);
assert_eq!(a, &mut [6, 5, 4]);
assert_eq!(b, &mut [3, 2, 1]);
复制代码
上面代码没有将split_at_mut
函数标记为unsafe
,所以可以在安全 Rust 中调用该函数。我们创建了一个对不安全代码的安全抽象,并在实现时以安全的方式使用了unsafe
代码,因为它仅仅创建了指向访问数据的有效指针,但是有时候from_raw_parts_mut
函数有可能导致崩溃:
use std::slice;
let address = 0x12345usize;
let ptr = address as *const i32;
unsafe {
let data: &[i32] = slice::from_raw_parts(ptr, 10000 as usize); // 报错,无法保证这段代码的切片中一直包含有效的i32值
println!("data: {:?}", data);
}
复制代码
因为我们只是拥有内存地址,而不拥有内存数据,所以无法保证其他的变量会使用到这块内存,然后修改了里面的值,导致里边的值不是有效的i32
类型,所以编译失败了。
使用 extern 函数调用外部代码
另外,rust 为了与其他语言相互调用,专门提供了extern
关键字来简化创建和使用外部函数接口(Foreign Function Interface,FFI)的过程。FFI 是编程语言定义函数的一种方式,它允许其他(外部的)编程语言来调用这些函数:
// 声明外部外部函数签名
extern "C" {
fn abs(input: i32) -> i32;
}
// 需要在unsafe模块中调用
unsafe {
abs(-10);
}
// 对外向其他语言暴露方法
// 用来注解来避免Rust在编译时改变它的名称
#[no_mangle]
pub extern "C" fn call_from_c () {
println!("C语言调用了这个方法");
}
复制代码
访问或修改一个可变静态变量
在 rust 中,全局变量也被称为静态(static
)变量,定义并使用一个不可变静态变量:
static HELLO_WORLD: &str = "hello world";
println!("{}", HELLO_WORLD); // hello world
复制代码
常量和不可变静态变量看起来可能非常相似,但它们之间存在一个非常微妙的区别:静态变量的值在内存中拥有固定的地址,使用它的值总是会访问到同样的数据。与之相反的是,常量则允许在任何被使用到的时候复制其数据。
静态变量允许是可变的
静态变量也可以使用mut
来将其标注为可变,但是在修改的时候也只能在unsaf
e 块中:
static mut COUNTER: u32 = 0;
// COUNTER = 1; // 报错,使用或修改都需要unsafe块
unsafe {
COUNTER = 1;
println!("{}", COUNTER); // 1
}
复制代码
实现不安全 trait
还有trait
,当某个trait
中存在至少一个方法拥有编译器无法校验的不安全因素时,我们就称这个trait
是不安全的,同样需要使用unsafe
来标识这个trait
:
// 定义和实现一个不安全trait
unsafe trait Foo {
fn foo() {}
}
// 为i32实现Foo trait
unsafe impl Foo for i32 {
fn foo() {}
}
复制代码
封面图:跟着Tina画美国
关注「码生笔谈」公众号,阅读更多最新章节
评论