Rust裸指针

概念

Rust开发中直接使用裸指针并不常见,裸指针更多的用于标准库,和一些基础设施,数据结构等。

分类

Rust裸指针分两种

  • * mut T 可变裸指针
  • * const T 不可变裸指针
    这两者可以互相转换:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    #[test]
    fn f1() {
    let a = 100;
    let b = &a as *const i32;
    let c = b as *mut i32;
    let d = c as *mut i32;

    println!("b = {}", unsafe { *b });
    println!("c = {}", unsafe { *c });
    println!("d = {}", unsafe { *d });
    }

    -------
    b = 100
    c = 100
    d = 100
    这里似乎Rust并不介意我们对a存在多个可变裸指针。

如何获取一个裸指针

1. 由引用转换而来

1
2
3
4
5
#[test]
fn f1() {
let my_num: i32 = 10;
let ptr: *const i32 = &my_num;
}

也可以这样转:

1
2
3
4
5
#[test]
fn f1() {
let my_num: i32 = 10;
let ptr = &my_num as *const i32;
}

侧面印证,引用本质就是指针

2. 由Box转换而来

这里会消费一个Box指针生成一个裸指针。

1
2
3
struct A;
let a = A;
let a: *const A = Box::into_raw(Box::new(a));

功能

read

将裸指针转为裸指针类型对应的对象。

1
2
3
4
5
6
fn main() {
let a = 3;
let x = &a as *const i32;
let mm = unsafe { x.read() };
println!("{}",mm);
}

write

null/is_null

构建一个空指针/判断是否为空指针

1
2
3
4
fn main() {
let p = std::ptr::null::<usize>();
println!("p is null: {}", p.is_null());
}

特性

1. Drop裸指针不会释放其指向的元素

1
2
3
4
5
6
7
8
9
10
11
12
struct A;

impl Drop for A {
fn drop(&mut self) {
println!("drop A...");
}
}

fn main() {
let a = A;
let b = Box::into_raw(Box::new(a));
}

执行这段代码并不会打印drop A。换句话说,释放裸指针并不会释放它所指向的对象。裸指针b的释放不会同时释放其指向的内存结构。

2. 构建裸指针safe,解引用裸指针是unsafe

比如我们从引用转裸指针,引用总是合法的指针,然而将其转为裸指针后。原本跟随引用的生命周期检查则失效了:

1
2
3
4
5
6
7
8
9
10
fn main() {
let p = u();
println!("{}", unsafe { *p });
}

fn u() -> *mut i32 {
let mut x = 2;
let y = &mut x as *mut i32;
y
}

上例中,函数u执行完成后,裸指针y指向的元素已经回收。此时解引用y将会产出ub,这里使用debug可能仍能打印正确结果2 ,使用release build会看到奇怪的问题。

3. 不能将value move出来

和引用一样不能从解引用(裸指针)move,只能读写解引用。

1
2
3
4
5
6
7
struct A;
fn main() {
let a = A;
let x = &a as *const A;
// 这里报错
let mm :A = unsafe { *x };
}

那么read可以move吗?

1
2
3
4
5
6
7
struct A;
fn main() {
let a = A;
let x = &a as *const A;
// let mm:A = unsafe { *x };
let mm:A = unsafe { x.read() };
}

看起来没报错。我们看read这个函数的注释:

1
2
3
/// Reads the value from `src` without moving it. This leaves the

/// memory in `src` unchanged.

实际上 read 也不会move原数据。所以从语义上来看,将裸指针转为了裸指针对应的对象。src must be [valid] for reads.

1
2
3
4
5
///
/// `read` creates a bitwise copy of `T`, regardless of whether `T` is [`Copy`].
/// If `T` is not [`Copy`], using both the returned value and the value at
/// `*src` can violate memory safety. Note that assigning to `*src` counts as a
/// use because it will attempt to drop the value at `*src`.

rust read的语义这里Rust官方论坛有些讨论:
https://users.rust-lang.org/t/why-does-reading-a-raw-pointer-cause-a-drop/66411/2

语义上讲如果T是非Copy,则执行read就应该将原数据从旧地址move到新地址,实际上实现上根据文档来看是bit级浅拷贝,原地址内存并没有动。

实现

应该说Rust裸指针和C的指针仍有非常大的不同。C指针只是一个地址。Rust裸指针兼顾部分安全性和效率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//从下面结构定义可以看到,裸指针本质就是PtrComponents<T>
pub(crate) union PtrRepr<T: ?Sized> {
pub(crate) const_ptr: *const T,
pub(crate) mut_ptr: *mut T,
pub(crate) components: PtrComponents<T>,
}

pub(crate) struct PtrComponents<T: ?Sized> {
//*const ()保证元数据部分是空
pub(crate) data_address: *const (),
//不同类型指针的元数据
pub(crate) metadata: <T as Pointee>::Metadata,
}

//从下面Pointee的定义可以看到一个RUST的编程技巧,即Trait可以只用来实现对关联类型的指定,Pointee这一Trait即只用来指定Metadata的类型。
pub trait Pointee {
/// The type for metadata in pointers and references to `Self`.
type Metadata: Copy + Send + Sync + Ord + Hash + Unpin;
}
//廋指针元数据是单元类型,即是空
pub trait Thin = Pointee<Metadata = ()>;

Thin 瘦指针Metadata是空的。此外就是metadata不为空的胖指针。str,切片,trait object是目前的三种常见的胖指针。

裸指针const T/ mut T将内存和类型系统相连接,*const T代表了一个内存块,指示了内存块首地址,大小,对齐等属性,以及后文提到的元数据,但不保证这个内存块的有效性和安全性。