这篇文章简单介绍下unsafe rust的几个要点
1. 解引用裸指针
裸指针其实就是C++或者说C的指针,与C的指针不同的是,Rust的裸指针还是要分为可变和不可变,*const T
和 *mut T
:
基于引用创建裸指针
rust
let mut num = 5;
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;
或者不想用类型转换也可以这么写(书上认为这是一种隐式转换,我觉得就是一种类型声明)
rust
let r3: *const i32 = #
let r4: *mut i32 = &mut num;
创建裸指针是安全的行为,而解引用裸指针才是不安全的行为
rust
fn main() {
let mut num = 5;
let r1 = &num as *const i32;
unsafe {
println!("r1 is: {}", *r1);
}
}
基于内存地址创建裸指针
基于内存地址创建裸指针相当于直接给指针赋值为某个内存地址:
rust
use std::{slice::from_raw_parts, str::from_utf8_unchecked};
fn main() {
let string = "bluebonnet27";
//as_ptr: Converts a string slice to a raw pointer.
let pointer_num = string.as_ptr() as usize;
let length = string.len();
unsafe {
//from_raw_parts: Forms a slice from a pointer and a length.
//from_utf8_unchecked: Converts a slice of bytes to a string slice without checking that the string contains valid UTF-8
let res = from_utf8_unchecked(from_raw_parts(pointer_num as *const u8, length));
println!(
"The {} bytes at 0x{:X} stored: {}",
length, pointer_num, res
)
}
}
我们可以尝试将pointer_num
和length
改成其他值
基于智能指针创建裸指针
还有一种创建裸指针的方式,那就是基于智能指针来创建:
rust
let a: Box<i32> = Box::new(10);
// 需要先解引用a
let b: *const i32 = &*a;
// 使用 into_raw 来创建
let c: *const i32 = Box::into_raw(a);
在C++中也可以通过智能指针创建裸指针,并且这种做法也存在一些问题。比如如下的代码:
cpp
auto p = make_shared<int>(42);
int* iPtr = p.get();
{
shared_ptr<int>(iPtr);
}
int value = *p; // Error! 内存已经被释放
p与iPtr指向了相同的内存,然而通过get方法后,将内存管理权转移给了普通指针。iPtr传递给里面程序块的临时智能指针后,引用计数为1,随后出了作用域,减少为0,释放内存。
2. 调用 unsafe 函数或方法
很简单,加上unsafe的声明就行:
rust
unsafe fn dangerous() {}
fn main() {
dangerous();
}
这样是编译不过的,因为dangerous
是个unsafe
函数。加上unsafe
调用即可:
rust
unsafe fn dangerous() {}
fn main() {
unsafe {
dangerous();
}
}
借用官方文档的一句话,"在整个代码库(code base,指构建一个软件系统所使用的全部代码)中,要尽可能减少不安全代码的量",比如我们上面的这个例子:
rust
fn main() {
let string = "bluebonnet27";
//as_ptr: Converts a string slice to a raw pointer.
let pointer_num = string.as_ptr() as usize;
let length = string.len();
unsafe {
//from_raw_parts: Forms a slice from a pointer and a length.
//from_utf8_unchecked: Converts a slice of bytes to a string slice without checking that the string contains valid UTF-8
let res = from_utf8_unchecked(from_raw_parts(pointer_num as *const u8, length));
println!(
"The {} bytes at 0x{:X} stored: {}",
length, pointer_num, res
)
}
}
printlin!
是个安全函数,将它放在unsafe唯一的原因是,我们需要在res
的生命周期内打印它。所以我们可以改成这样:
rust
fn get_str(pointer_num: usize, length: usize) -> String {
unsafe {
//from_raw_parts: Forms a slice from a pointer and a length.
//from_utf8_unchecked: Converts a slice of bytes to a string slice without checking that the string contains valid UTF-8
String::from(from_utf8_unchecked(from_raw_parts(
pointer_num as *const u8,
length,
)))
}
}
fn main() {
let string = "bluebonnet27";
//as_ptr: Converts a string slice to a raw pointer.
let pointer_num = string.as_ptr() as usize;
let length = string.len();
let res = get_str(pointer_num, length);
println!(
"The {} bytes at 0x{:X} stored: {}",
length, pointer_num, res
)
}
我们将unsafe的部分单独抽成了一个函数。这里的返回值,不想用String
交出所有权,也可以用'static
的&str
或者更简单地,可以直接将res右侧全部用unsafe包裹:
rust
let res = unsafe{ from_utf8_unchecked(from_raw_parts(pointer_num as *const u8, length));}
3. FFI
FFI(Foreign Function Interface)可以用来与其它语言进行交互,将 C/C++ 的代码重构为 Rust 时,先将相关代码引入到 Rust 项目中,然后逐步重构,也是不错的。
当然,除了 FFI 还有一个办法可以解决跨语言调用的问题,那就是将其作为一个独立的服务,然后使用网络调用的方式去访问,HTTP,gRPC 都可以。
言归正传,之前我们提到 unsafe 的另一个重要目的就是对 FFI 提供支持,它的全称是 Foreign Function Interface,顾名思义,通过 FFI , 我们的 Rust 代码可以跟其它语言的外部代码进行交互。
在Rust中调用其他语言的函数
下面的例子演示了如何调用 C 标准库中的 abs
函数(Rust 目前无法直接调用 C++ 库):
rust
extern "C" {
fn abs(input: i32) -> i32;
}
fn main() {
unsafe {
println!("Absolute value of -3 according to C: {}", abs(-3));
}
}
事实上,不指定 ABI 字符串的默认情况下,外部块会假定使用指定平台上的标准 C ABI 约定来调用当前的库。所以上面的代码这么写也是ok的:
rust
extern {
fn abs(input: i32) -> i32;
}
当然大括号不能去掉。在 extern "C" 代码块中,我们列出了想要调用的外部函数的签名。其中 "C" 定义了外部函数所使用的应用二进制接口ABI (Application Binary Interface):ABI 定义了如何在汇编层面来调用该函数。
有三个 ABI 字符串是跨平台的,并且保证所有编译器都支持它们:
extern "Rust"
-- 在任何 Rust 语言中编写的普通函数 fn foo() 默认使用的 ABI。extern "C"
-- 这等价于 extern fn foo();无论您的 C编译器支持什么默认 ABI。extern "system"
-- 在 Win32 平台之外,中通常等价于extern "C"
。在 Win32 平台上,应该使用"stdcall"
,或者其他应该使用的 ABI 字符串来链接它们自身的 Windows API。
4. 访问或修改一个可变的静态变量
静态变量
静态变量允许声明一个全局的变量,常用于全局数据统计,例如我们希望用一个变量来统计程序当前的总请求数
rust
static mut REQUEST_RECV: usize = 0;
fn main() {
unsafe {
REQUEST_RECV += 1;
assert_eq!(REQUEST_RECV, 1);
}
}
Rust 要求必须使用unsafe语句块才能访问和修改static变量,因为这种使用方式往往并不安全,其实编译器是对的,当在多线程中同时去修改时,会不可避免的遇到脏数据。
只有在同一线程内或者不在乎数据的准确性时,才应该使用全局静态变量。
和常量相同,定义静态变量的时候必须赋值为在编译期就可以计算出的值(常量表达式/数学表达式),不能是运行时才能计算出的值(如函数)
5. 实现 unsafe 特征
unsafe特征的意义是,特征中存在unsafe的方法,有时候就得需要unsafe的特征:
rust
unsafe trait Foo {
// 方法列表
}
unsafe impl Foo for i32 {
// 实现相应的方法
}
fn main() {}
但是在调用 unsafe trait 时,直接直接调用,不需要在 unsafe 块中调用,因为这里的安全已经被实现者保证了,毕竟如果实现者没保证,调用者也做不了什么来保证安全.
Rust 中的 Send
/ Sync
,这两个 trait 都是 unsafe trait,定义如下
rust
pub unsafe auto trait Send {}
pub unsafe auto trait Sync {}
6. 访问 union 中的字段
访问
这个从C中继承而来的数据结构,在Rust中也大多用于和C进行交互,下面就是一个union
的例子:
rust
union MyUnion {
f1: u32,
f2: f32,
}
union的关键属性是其所有字段共享公共存储。 因此,对union的一个字段的写入可以覆盖其他字段,并且 union的大小由其最大字段的大小决定。
rust
fn main() {
//初始化一个union,语法和struct类似
let u = MyUnion { f1: 1 };
//读取union的值
let f = unsafe { u.f1 };
println!("u.f1 = {f}");
}
读取值的操作是unsafe的,这也很好理解,编译器并不知道你读取的东西有没有初始化。反正大家都用相同的内存,我说这段数据就是f32
也行,就算它存进去的时候其实是u32
。
rust
let f = unsafe { u.f1 };
let tmp = unsafe { u.f2 };
println!("u.f1 = {f}");
println!("u.f2 = {tmp}");
结果如下:
也可以用模式匹配,当然,这种操作和直接读取没什么区别,所以也必须是unsafe的:
rust
unsafe {
match u {
MyUnion { f1: 1 } => {
println!("one");
}
MyUnion { f2 } => {
println!("{}", f2);
}
}
}
引用
引用操作也是unsafe
的,而且,由于union
各个成员是共享内存的,对一名成员的引用会视为对其他所有成员的引用:
rust
// 错误: 不能同时对 `u` (通过 `u.f2`)拥有多于一次的可变借用
fn test() {
let mut u = MyUnion { f1: 1 };
unsafe {
let b1 = &mut u.f1;
// ---- 首次可变借用发生在这里 (通过 `u.f1`)
let b2 = &mut u.f2;
// ^^^^ 二次可变借用发生在这里 (通过 `u.f2`)
*b1 = 5;
}
// - 首次借用在这里结束
assert_eq!(unsafe { u.f1 }, 5);
}
Rust-Analysis也给出了提示:
C++的改进
union存在很多问题,因此C++17设计了一个新的variant
替代原来的union
。
variant的用法如下:
cpp
using namespace std;
int main()
{
variant<int, string, float> myVar;
myVar = "Hello variant";
}
union
访问的时候,由于每个成员变量都有自己的变量名,因此直接就可以访问。但是variant
不太行,而且还要更麻烦一点。
最简单的就是用get
cpp
cout << get<string>(myVar) << endl;
但是这里存在一个问题,如果类型对了那皆大欢喜;类型错了,还要处理抛出的std::bad_variant_access
异常:
我们可以使用get_if
,先判断类型再进行访问。get_if
判断类型成功会返回指向数据的指针,判断失败会返回空指针。
cpp
if(auto ptr = get_if<string>(&myVar))
{
cout << *ptr << endl;
}
7. 内联汇编
Rust中的内联汇编
Rust 提供了 asm!
宏,可以让大家在 Rust 代码中嵌入汇编代码,对于一些极致高性能或者底层的场景还是非常有用的,例如操作系统内核开发。
rust
use std::arch::asm;
unsafe {
asm!("nop");
}
上面代码将插入一个 NOP 指令( 空操作 ) 到编译器生成的汇编代码中,其中指令作为 asm!
的第一个参数传入。
总结
C++中其实没有unsafe这个东西,像类似裸指针这种,在C++中甚至是一种比较常用的用法。毕竟智能指针,比如shared_ptr
,unique_ptr
,用法更为复杂。
所以我个人认为,Rust的unsafe的意义是,将这些不安全的操作变得复杂,变得难写,进而引导程序员选择更加简单,更加好写的安全用法。这和C++如今的处境刚好相反,C++中按照安全原则写出来的代码都比较复杂,这也是历史原因,毕竟不能动现成的代码。
另外,unsafe也是一种承诺,不再由编译器保证代码的安全性,而是由程序员自己来保证。一旦代码出问题,责任全在程序员自己。