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 的解决方式是对 &
、 &mut
与 fn
采用不同的退化策略。
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,要么直接调用 alloc
和 dealloc
,要么在确保 T:Copy
的情况下使用
Vec::set_len
;但其实大部分情况下,由于 Rust 存在 Read
和 Write
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,要求T
与U
大小相同。对于强制转换引用生命周期有奇效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))
}