2024/10 更新:本文为初学 Rust 阶段所作,仅作为个人学习记录,不应作为参考。

Rust 指针

Rust 有其裸指针 *mut T*const T,分别意味着指向的数据是可变/不可变的(实用中唯一的区别就是 Variance,实际通过 unsafe dereference 可以自由修改数据)。

它有以下限制

  • 解引用裸指针是 unsafe 的
  • 对象的所有权管理要手动实现
  • 默认 !Send + !Sync

转换规则

指针和引用之间的转换规则如下:

   &T        &mut T
   ^            ^  
   |            |  
   v            v  
*const T <-> *mut T

引用或指针转换到指针用 as 关键词,而指针转换为引用需要 &(*ptr)&mut(*ptr)。 由于解引用是 unsafe 的,此操作需要包装在 unsafe 内。

我们似乎可以构造这么个超模的函数

fn upgrade<T: ?Sized>(v: &T) -> &'static mut T{
    unsafe{&mut*(v as *const T as *mut T)}
}

当当!把不可变引用变成可变性引用了!这就是 unsafe 的威力啊哈哈!

Rust 手动写了一项静态检查,要求 &T 不能转换为 &mut T。因此上面的代码并不能通过编译, 尽管理论上他不违背 Rust 语法,我们也可以通过一些奇妙操作绕过这项检查。(见附 #2)

如果真的需要强制转换可变性,应直接用指针而非引用,因为它自带 unsafe, 这样的标注要求程序员更加注意,也更加 Rusty。这正是 UnsafeCell<T> 的设计来源。

返回值的生命周期

指针造出的引用是没有生命周期的。我们需要手动声明生命周期。

通常的做法时让返回的引用与 &self 具有相同的生命周期(默认)。如果要分割可变性,且可接受拷贝开销,可以用 Vec<XXXCell<T>>

另一种情况时,指针并不 unique 且,我们可以标注为 'static,并在文档中明确指出 错误使会造成内存泄漏。

当然,如果需要返回完整所有权的东西,请尽情用 ptr::read

Subtyping and Variance

Subtyping 意在于解决泛型类型自带生命周期的情况。

Subtyping 意味着生命周期的隐式转换,例如以下的情景:

& &T

例如官方例

fn foo<'a>(a: &'a str, b: &'a str)->&'a str {""}
fn main() {
    let hello: &'static str = "hello";
    {
        let world = String::from("world");
        let world = &world; // 'world has a shorter lifetime than 'static
        let out=foo(hello, world); // 'world
    }
}

这里的 hello 在传入时会自动退化到 'world,这种退化与参数顺序无关,所有 参数会自动退化到最短生命周期的参数。因此输出也具有 'world 生命周期。

&mut &T

然而有时候我们又需要拒绝无序退化。例如官方例子

fn assign<T>(input: &mut T, val: T) {
    *input = val;
}

fn main() {
    let mut hello: &'static str = "hello";
    {
        let world = String::from("world");
        assign(&mut hello, &world);
    }
    println!("{hello}"); // use after free 😿
}

这里的问题本质上是把 &'world str 赋值给了 &'static str,这是不允许的; 但是如果沿用上面的无序退化,则似乎 'static 应该自动退化到 'world

问题的关键在于“赋值”:由于 input 可赋值,因此才会发生问题;因此我们要拒绝 可赋值时的生命周期退化。

fn(&T)

还有一种情形:我们要使用多个函数作为参数。

fn foo<T>(f1: fn(T), f2: fn(T), i: T){
    f1(i);
    f2(i);
}

fn main() {
    fn f1(x: &'static str){}
    fn f2(x: &str){} // unbounded
    foo(f1, f2, "hi"); // T: &'static str
}

这里的 f2 的 lifetime 就应当升级到 'static,才能与 f1 并列工作。 这大概是唯一一个生命周期升级的例子了吧。

Variance

Rust 的解决方式是对 &&mutfn 采用不同的退化策略。

Rust 提供了三种退化策略:

  • 可变:Invariant,拒绝退化
  • 不可变:Covariant,允许退化
  • 函数参数:Contravariant,允许升级(变成更长的生命周期)

更详细地(其中 T 都包含某个生命周期,这里以不可变引用 &'b T 为例):

‘b
& &'b T covariant
&mut &'b T invariant
Box<&'b T> covariant
Vec<&'b T> covariant
UnsafeCell<&'b T> invariant
Cell<&'b T> invariant
fn(&'b T) -> U contravariant
*const &'b T covariant
*mut &'b T invariant

以及基础的 &'a&'a mut'a 都是 covariant,即允许退化。

我们在自定义函数的时候可以通过此列表预估并设计泛型 variance 特性。 尤其是,用 *const T 来允许 T 为引用时的生命退化,*mut T 来拒绝。

UnsafeCell

UnsafeCell<T>T 的透明包装,通过指针提供了改变可变性的可能。 用源码理解起来更简单一点。

#[repr(transparent)] // allow direct transform from *UnsafeCell<T> to *T
struct UnsafeCell<T: ?Sized> {
    value: T,
}

impl<T: ?Sized> UnsafeCell<T> {
    pub const fn get(&self) -> *mut T {
        self as *const UnsafeCell<T> as *const T as *mut T
    }
}

由于 const fn 的声明和 #[repr(transparent)] 的标注,这段代码完全零开销, 唯一作用就是满足编译器。

NonNull

TLDR: Option<NonNull<T>>const *T 的 Rusty 包装,从而利用 enum 语法约束 我们处理空、悬空和普通三种情况。

在通常情况下,我们能够保证指针非空,并希望编译器为 Option<*T> 做出非零优化, 使得产出结构为近似 #[repr(transparent)] 的效果(但是不保证)。

Rust 提供了非零标注 #[rustc_nonnull_optimization_guaranteed]NonNull 是它的官方用例。

另一方面,它提供了 NonNull::dangling() 来描述悬空指针状态。;

如果允许空指针,可以用 Option<NonNull<T>>

不过都 unsafe 了,我选裸指针 XD

内存排列 (Layout) 和对齐 (Alignment)

对于一个基本类型,内存地址必须为 alignment 的整数倍。例如 *u32 的地址必须为 4 的整数倍, 否则会 panic。

对于一个结构体,内存的排列对齐方式是不确定的,而不像 C 那样就如结构体定义的顺序, 为了访问性能和对齐,Rust 会打乱定义顺序。

若工作需要依赖结构体的二进制结构和 field 顺序,请使用 #[repr(C)],并仔细注意 alignment。例如如下代码

#[repr(C)]
struct S{a:u8, b:u16, c:u8}

由于 alignment 要求,实际的内存布局为

┌────┬─┬─┬─┬─┬─┬─┐
│addr│0│1│2│3│4│5│
├────┼─┼─┼─┴─┼─┼─┤
│var │a│ │ b │c│ │
└────┴─┴─┴───┴─┴─┘

强制初始化

Rust 要求我们强制初始化结构体的所有部分。然而有时候我们不需要初始化。

Rust 提供了 std::mem::MaybeUninit

其大致原理如下:

// Union field must impl Copy or be ManuallyDrop
// as union doesn't have ownership of its fields.
// 
// Because it is validated by `#[lang = "manually_drop"]`,
// which must be unique throughout code, we cannot implement
// it ourselves unless we reject core crate.
use std::mem::ManuallyDrop; 
// with same size as T
union MaybeUninit<T>{
    uninit: (),
    value: ManuallyDrop<T>,
}
/// SAFETY: MUST initialize the fields to be read
unsafe const fn uninitialized<T>()->T{
    ManuallyDrop::into_inner(
        MaybeUninit::<T>{uninit: ()}.value
    )
}

可见利用了 union 的部分初始化特性。

实践中通常是在 FFI 调用中使用,范例如下:

fn get_num_cpus()->i32 {
    extern "C" { fn num_cpus(n: *mut i32); }
    let mut x = std::mem::MaybeUninit::uninit();
    unsafe {
        // FFI call
        num_cpus(x.as_mut_ptr());
    }
    // safety: Now inited
    unsafe{ x.assume_init() }
}

其他情况下,例如,你需要一个 Buffer,要么直接调用 allocdealloc,要么在确保 T:Copy 的情况下使用 Vec::set_len;但其实大部分情况下,由于 Rust 存在 ReadWrite trait,你需要的或许只是 Read::read_to_end 或是一个栈上分配的数 k 数组。 不过在此之前用 unsafe 裸指针避免 index check 开销岂不是更有用

PhantomData

在结构体内声明一个 PhantomData<T> 类型的 field 可以占用一个泛型变量,包括类型和生命周期, 以解决 parameter x is never used 的问题1。这在实现一些抽象类型,例如没有 T 类型或是 ‘a 生命周期的 field,但是需要使用到该信息的时候很有用。(相当于编译期变量)

它还有以下用途:

  • 曾经用作解决 Drop Checker 问题
  • 有人习惯用它来声明“内含某个类型元素”

一些好用的函数

均在 std crate 下。 内容操作:

  • mem::transmute<T, U>:强制 reinterpret,要求 TU 大小相同。 对于强制转换引用生命周期有奇效
  • ptr::read(self) -> T: 把 src 指针处的 T 类型物直接复制到临时变量中,并赋予完整的所有权,可以返回。
  • ptr::add(self, count: usize) -> Self: 等效于 C 中的 p+count。
  • mem::size_of::<T>(): 等效于 C 中的 sizeof(T),是 const fn,有编译期分支优化! 内存分配:
  • alloc::Layout::new<T>(): 用于 alloc::alloc 单个对象,需要 unwrap。 alloc::Layout::array<T>(): 用于 alloc::alloc 一组对象,需要 unwrap。
  • alloc::alloc(layout: Layout) alloc::dealloc(ptr: *mut u8, layout: Layout) alloc::realloc(ptr: *mut u8, layout: Layout, new_size: usize)

References

附录——一些代码

1. C里C气

use std::alloc::{alloc, dealloc, Layout};

pub struct Node<T: Copy> {
    value: T,
    next: *mut Self,
}

impl<T: Copy> Node<T>{
    pub fn new(value: T) -> &'static mut Self{
        unsafe{
            let ptr = alloc(Layout::new::<Self>()) as *mut Self;
            (*ptr).value=value;
            (*ptr).next=0 as *mut Self;
            &mut(*ptr)
        }
    }
}

impl<T: Copy> Drop for Node<T> {
    pub fn drop(&self){
        let mut p = self as *const Self;
        let mut next;
        let layout = Layout::new::<Self>();
        unsafe{
        while p as usize !=0{
            next=(*p).next;
            drop((*p).value);
            dealloc(p as *mut u8, layout);
            p=next;
        }}
    }
}

2. 可变性强制转换

/// # Unsafe
const fn raw<T: ?Sized>(v: &T) -> *mut T {
    v as *const T as *mut T
}

unsafe fn as_mut<T: ?Sized>(v: *mut T) -> &'static mut T{
    unsafe{&mut(*v)}
}

/// # Extremely Unsafe
unsafe fn upgrade<T: ?Sized>(v: &T) -> &'static mut T{
    as_mut(raw(v))
}