Rust - A CPP Programmer's Perspective

鉴于贵司大作tikv、tidb、tiflash在Rust、Go和C++之间横跳,因此学习Rust被提上了日程。

本文简称叫Rust: ACPPPP,它主要是用来讨论Rust在一些方面和C++的异同,而不是介绍这一门语言。所以文章是话题形式的,会有很多穿插,例如在讨论所有权时,会直接讲结构体。

rustup:Toolchain 管理工具

安装 nightly toolchain

1
rustup toolchain install nightly

然后激活

1
rustup default nightly

Override

Toolchain 的选择使用下面:

  1. 在命令行中指定,如cargo +beta
  2. RUSTUP_TOOLCHAIN环境变量
  3. rustup override set 覆盖当前目录以及子目录的设置
    rustup showrustup override unset可以查看和取消 override
  4. rust-toolchain.toml 或者 rust-toolchain
  5. 使用 default toolchain

Cargo:包管理工具

workspace、crate 和 mod

C++ 并没有什么包管理,如果我们想要引用什么东西,代码声明一下,然后确保链接器能够看到定义就行。并且因为模板的引入,很多都是头文件,直接include就行。

访问 mod

crate内

src/main.rs和src/lib.rs被称为crate

一个crate下有若干mod,每个mod的成员在对应mod文件夹的mod.rs中列出。
例如下面的声明,会查找当前目录下的hello.rs,或者hello目录下的mod.rs。

1
2
// mod.rs
mod hello;

可以通过#[path = "foo.rs"]来指定 mod 的位置。这种用法可以在函数中 inline。可以在这段代码中查看具体用法。

跨crate

跨crate访问,需要使用Cargo.toml中定义的crate别名。

rustc 和 crate

rustc只接受一个文件,并只生成一个crate。

1
rustc hello.rs --crate-type=lib

workspace

workspace不能嵌套。所以如果两个Cargo工程,并且工程A依赖于工程B,比较好的方案是平行摆放两个工程,并设置dependencies

virtual workspace

编译与链接

调试信息

可以通过 -C debug_info 来指定调试信息的等级,其中0(false)、1、2(true) 分别对应无/行信息以及全部信息。
另外,Cargo.toml 中的 [profile] 也可以修改。

条件编译

features

features用来支持条件编译可选依赖
在编译时,可以通过--features去enable某个feature。
例如在Cargo.toml中,webp是一个feature,并且它没有enable其他feature。而ico这个feature会enable两个feature即bmp和png。

1
2
3
4
[features]
# Defines a feature named `webp` that does not enable any other features.
webp = []
ico = ["bmp", "png"]

在代码中,可以用下面两种方式,让代码只对webp被enable的情况下生效,

1
2
3
4
5
6
// 1
#[cfg(feature = "webp")]
// 2
if cfg!(feature = "webp") {

}

默认情况下,所有的 feature 都是 disable 的,但可以把 feature 加入 default 中来默认 enable 它。

1
default = ["webp"]

在编译时,可以指定--no-default-features来disable default feature。

dependency features

在指定dependency时,也可以指定features。
例如下面的配置中,将flate2的default features disable掉,但额外enable了zlib这个feature。

1
2
[dependencies]
flate2 = { version = "1.0.3", default-features = false, features = ["zlib"] }

optional dependency

下面的语句表示gif默认不会作为依赖

1
2
[dependencies]
gif = { version = "0.11.1", optional = true }

它会隐式定义了如下的feature

1
2
[features]
gif = ["dep:gif"]

可以通过 cfg(feature = "gif") 来判断dependency是否被启用,通过 --features gif 来显式启用 dependency。

如下的代码表示 avif 会 enable ravif 和 rgb 这两个 feature,但因为显式使用了 dep:ravifdep:rgb,所以系统不会隐式生成 ravifrgb 这两个feature。

1
2
3
4
5
6
[dependencies]
ravif = { version = "0.6.3", optional = true }
rgb = { version = "0.8.25", optional = true }

[features]
avif = ["dep:ravif", "dep:rgb"]

Cargo.toml 解读

  1. [dependencies]
    依赖的第三方package

  2. [dev-dependencies]
    只有tests/examples/benchmarks依赖的第三方package

  3. [features]
    用来支持条件编译和可选依赖

  4. [lib]

  5. [[test]]
    两个中括号说明是表数组,我们可以这样写

    1
    2
    3
    4
    5
    6
    [[test]]
    path = ""
    name = ""
    [[test]]
    path = ""
    name = ""
  6. [package]

  7. [workspace]
    相对于package而言,workspace是一系列共享同样的Cargo.lock和输出目录的包。
    包含members数组

  8. [profile]

  9. [patch.crates-io]

Cargo.lock 解读

Cargo.lock 是记录每个 crate 对应版本的工具。例如下面的配置表示依赖一个0.1.0版本的 azure_core 库,可是这个版本具体对应哪个 rev 呢?github 打开发现 master 上已经是0.2.1的版本了,我们显然不可能是用的 master 啊。

1
azure_core = { version = "0.1.0", git = "https://github.com/Azure/azure-sdk-for-rust"}

此时查看 Cargo.lock 就能发现类似下面的配置,其中具体指出了0.1.0对应的 git commit

1
2
3
4
5
6
7
8
[[package]]
name = "azure_core"
version = "0.1.0"
source = "git+https://github.com/Azure/azure-sdk-for-rust#b3c53f4cec4a6b541e49388b51e696dc892f18a3"
dependencies = [
"async-trait",
...
]

一个 workspace 只在根目录有一个 Cargo.lock。这确保了所有的 crate 都使用完全相同版本的依赖。
如果在 Cargo.toml 和 add-one/Cargo.toml 中都增加 rand crate,则 Cargo 会将其都解析为同一版本并记录到唯一的 Cargo.lock 中。

Cargo 的常见问题

failed to authenticate when downloading repository这样的错误一般出现在和github交互的场景中。使用下面的办法可解决

1
2
3
eval `ssh-agent -s`
ssh-add
cargo build

Blocking waiting for file lock on the registry index 这样的错误一般删除rm $CARGO_HOME/.package-cache.

所有权、生命周期

为了检验是否初步理解Rust所有权,可以尝试自己实现一个双向链表。

绑定和可变性

let和let mut

let x = y表示把y这个值bound/assign到变量x上,因为let是immutable的,所以就不能修改变量x,也就是再次给它赋值(assign)了。如果需要能re-bound或者re-assign,就需要let mut x = y这种形式。

对结构体而言,如果它是immutable的,那么它的所有成员也都是immutable的。在C++中,可以声明类中的某个成员是mutable的,这样即使在const类中也可以修改它,但Rust不允许这样。

由此还派生出了&mut&两种引用。可以可变或者不可变地借用let mut绑定的值,但只能不可变地借用let绑定的值。

Pattern Matching

下面的语句都在尝试定义一个&mut {interger}类型的a,但第三条语句是编译不过的。原因是它触发了Rust里面的pattern matching。

1
2
3
4
5
let ref mut a = 5;
let a = &mut 5;
let &mut a = 5;
// 下面这个语句肯定编译不过,但可以从错误中得到a的实际类型,所以是个常见的白嫖编译器类型推导的办法
let _: () = a;

我们很熟悉对 enum 类型(诸如OptionResult)进行 Pattern Matching 的做法。下面介绍一些不一样的,例如可以 Pattern Match 一个 struct,有点类似 C++ 的 structual binding。

1
2
3
4
5
6
7
8
9
10
11
struct Point {
x: i32,
y: i32,
}

fn main() {
let p = Point { x: 0, y: 7 };
let Point { x: a, y: b } = p;
assert_eq!(0, a);
assert_eq!(7, b);
}

通过@,可以在 Pattern Matching 的时候同时指定期待的值,并将该值保存到局部变量中,有点类似于 Haskell 的用法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}

fn main() {
let p = Point {x: 1, y: 2};
match p {
Point { x: xv @ 1, y: yv @ 2} => println!("matched x {:?} y {:?}", xv, yv),
_ => println!("no match"),
}

match p {
pt @ Point { .. } => println!("matched pt {:?}", pt),
_ => println!("no match"),
}

// pattern bindings after an `@` are unstable
// https://github.com/rust-lang/rust/issues/65490
match p {
pt @ Point { x, y } => println!("matched pt {:?} x {:?} y {:?}", pt, x, y),
_ => println!("no match"),
}
}

如何在pattern matching的时候不move,而是borrow呢?如下所示,g是一个owned值,而不是一个mutable borrow的值。解决方案就是直接match v.intention_mut()

1
2
3
4
5
6
7
let intention = v.intention_mut();
match intention {
vehicle::Intention::Die => {
},
vehicle::Intention::Goto(g) => {
},
}

Variable shadow

在Rust中有如下称为Variable shadow的做法。一个问题油然而生,既然可以直接let mut,为什么还需要如下的做法呢?

1
2
let x = 1;
let x = 2;

其实shadow的含义是这个变量的生命周期没变,只是我们无法通过从前的名字访问它了,而let mut在重新assign之后,原来的value就会被析构掉。进一步举个例子,给出下面这个程序,它的输出是啥?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct S {
x: i32
}

impl Drop for S{
fn drop(&mut self) {
println!("drop {}", self.x)
}
}

fn main() {
{
let a = S { x: 1 };
let a = S { x: 2 };
}
{
let mut a = S { x: 1 };
a = S { x: 2 };
}
}

结论如下

1
2
3
4
drop 2
drop 1
drop 1
drop 2

为什么呢?对于第一种情况,a被rebound了,但是S {x: 1}只是被shadow了,并没有立即析构。但对于第二种情况,在rebound的时候,S { x: 1 }就被析构了。

移动和借用

可以把所有对值的使用方式归纳为三种:复制、移动和引用(或者称为指针):

  1. 复制的缺点是浪费空间和时间。
  2. 移动的缺点是很多变量的地址会变,这个FFI带来很多麻烦,需要用Box/Pin将一些东西分配到堆上的固定地址,并且传出裸指针。
  3. 引用的缺点是存在NULL,为了避免NULL,又要引入生命周期注解等机制。此外,即使在有了移动语义后,多线程之间依然可以通过引用来访问同一个值,产生并发问题。

Rust中的移动可能伴随着内存地址的变化。很显然,一个对象从A方法通过调用被移动到B方法中,那么肯定出于不同的栈帧中,它的地址肯定会变化,所以要提防这个。而C++中移动更类似金蝉脱壳,将老对象中的东西拆出来用来构建新对象。

移动

1
2
3
4
5
6
7
8
9
// Can compile
let x = 1;
let y = x;
println!("Result: {}", x);

// Can not compile
let vx = vec![1];
let vy = vx;
println!("Result: {}", vx[0]);

引用

引用和借用是什么关系呢?创建一个引用的行为称为借用,在借用过程中,是不可以访问owned值的,否则出现use of borrowed xxx错误。

在C++中,引用必须在定义时就绑定,并且,无论它是可变引用T&还是不可变引用const T&,都不能重新绑定。这很难受,并且std::reference_wrapper也不是什么时候都可以用的。Rust中这些都不是问题,例如下面的代码就可以正常运行。

1
2
3
let mut a: &i32;
a = &1;
a = &2;

只能有一个可变借用,或多个不可变借用

考虑下面的Race Condition:

  1. 多个指针访问同一块数据
  2. 至少一个指针被用来修改数据
  3. 没有同步机制

Rust解决方案是只能同时有一个可变借用,或者多个不可变借用。问题来了,如果Owner在写,有一个可变引用在写,或者有一个不可变引用在读呢?
对于对象的成员函数的调用,这种情况是不存在的。如下所示,成员函数需要&self或者&mut self

1
2
3
4
5
6
let mut x = "123".to_string();
let y = &mut x;

x.push_str("456");

println!("y = {}", y);

那么对于primitive types呢?运行下面的代码,发现出现错误提示”use of borrowed aaa“,这也就是说在借用期间,是无法访问owned value的,毕竟被借走了嘛。

1
2
3
4
let mut aaa: i32 = 1;
let bbb = &mut aaa;
aaa += 1;
println!("bbb {:?}", *bbb);

注意,下面的代码给人一种”可以同时使用借用和owned的值的错觉“,但并不是这样。因为change_aaaaaa的借用在调用完成之后就结束了,后面aaa = 2的时候就没有其他借用情况了。

1
2
3
4
5
6
7
8
9
10
fn change_aaa(bbb: &mut i32){
*bbb = 2;
}

fn main() {
let mut aaa: i32 = 1;
// TODO 是否可以想个办法异步执行
change_aaa(&mut aaa)
aaa = 2;
}

demo

通过移动来实现析构

std::mem::drop函数用来析构T的对象,这是对移动的应用。在调用drop函数时,_x的所有权会被移入。当然,如果实现了Copy,那么drop就无效了

1
pub fn drop<T>(_x: T) { }

借用的demo

当一个函数接受引用作为参数时,需要显式借用,这一点和C++不一样。

1
2
3
4
5
6
7
fn fn_takes_ref(i: &int) {
println!("{}", i);
}
// Error
fn_takes_ref(1);
// Ok
fn_takes_ref(&1);

Clone和Copy

Copy

Rust有一个叫做std::marker::Copy的特殊trait,其中不带有任何方法,所以基本可以视作是给编译器提供的一个marker。如果一个类型实现了Copy trait,在赋值的时候使用复制语义而不是移动语义。

Rust不允许自身或其任何部分实现了Drop trait的类型使用Copy trait。这听起来很奇怪,但如果我说Copy trait的实现就是bitwise的Copy,就合理了。所以可以近似理解为Copy只适用于C++中的trivial的对象。

Clone

对于非trivial对象,又想复制怎么办呢?一个方法是实现Clone trait。可以理解为是C++中的拷贝构造函数。

容易想到,如果仅仅实现深复制,那么实际上就是递归调用所有field的.clone()而已,这其实等价于下面的代码

1
2
3
#[derive(Clone)]
struct S {
}

但注意,编译器在要求实现Copy后,Clone的含义也必须代表bitwise memcpy。因此我们通常会通过#[derive(Copy,Clone)]来支持自动生成Copy特性。

所有权相关设施

介绍Borrow(.borrow())/BorrowMut(.borrow_mut())/AsRef(.as_ref())/AsMut(.as_mut())/ToOwned(.to_owned())等基础的实现。

as_ref/as_mut 和借用

什么时候用 as_ref/as_mut 呢?如下代码所示,如果需要获得容器 Option 持有的对象的借用,那么我们不能先 unwrap 再 &mut 借用,而应该先 as_mut 再 unwrap。

1
2
3
4
5
6
7
8
9
10
11
12
13
struct S {
a: i32,
b: i32,
}

fn main() {
let mut a: Option<S> = Some(S {a: 1, b: 2});
let b = &mut a;
// Error
let c = &mut b.unwrap();
// Ok
let c = b.as_mut().unwrap();
}

Borrow和AsRef的区别是什么?

可以看到AsRef和Borrow两个trait的定义不能说非常相似,也可以说是一模一样了,那为什么会分成两个呢?

1
2
3
4
5
6
7
pub trait AsRef<T: ?Sized> {
fn as_ref(&self) -> &T;
}

pub trait Borrow<Borrowed: ?Sized> {
fn borrow(&self) -> &Borrowed;
}

显然这个疑问是普遍的,通常的说法是Borrow更严格,目的是借用;AsRef支持的类型更广,目的是类型转换。但说实话,还是一头雾水。这篇文章讲解了个例子,概括如下:

  1. HashMap存储(K, V)对,并且可以通过提供的&K查找对应的&mut V。因为按K写,按&K取,所以需要保证这两个的行为是一致的。
    【Q】为什么HashMap要按照&K取呢?
  2. 于此同时,我们可以实现一个CaseInsensitiveString结构,它可以看做是忽略大小写比较的一个String。
  3. 问题来了,我们有impl Borrow<str> for String,那么是否可以实现impl Borrow<str> for CaseInsensitiveString呢?
    答案是不可以的,这样会破坏HashMap的一致性。例如我两个只是大小写不同的字符串,按照s: CaseInsensitiveString比较是相等的,按照s.borrow()比较就不相等了。
    但这就够了么?难道CaseInsensitiveString不可以转换成&str么?当然可以,所以有AsRef。

cannot infer type for type parameter Borrowed declared on the trait BorrowMut

1
2
let a = Box::new(RefCell::new(1));
(*a.borrow_mut().get_mut()) = 2;

为什么不能从&mut调用Clone?

从下面的实现可以看到,标准库没有为&mut提供Clone,原因是会产生指向同一个位置的两个&mut

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
impl<T: ?Sized> Clone for *const T {
fn clone(&self) -> Self {
*self
}
}

impl<T: ?Sized> Clone for *mut T {
fn clone(&self) -> Self {
*self
}
}

impl<T: ?Sized> Clone for &T {
fn clone(&self) -> Self {
*self
}
}

impl<T: ?Sized> !Clone for &mut T {}

下面的代码中,如果clone了&mut MyStruct2,会出现多个指向同一个地址的&'a mut MyStruct

1
2
3
4
5
6
7
8
9
#[derive(Clone)]
struct MyStruct {
val: usize,
}

#[derive(Clone)]
struct MyStruct2<'a> {
struct_reference: &'a mut MyStruct
}

但需要注意,clone的目标不是&T而是T。上面例子为什么会失败,原因是在Clone MyStruct的时候递归地需要Clone &'a mut MyStruct导致的。但如果直接对一个&mut T调用Clone就不会出现编译问题,如下所示

1
2
3
4
5
6
7
#[derive(Clone)]
struct DoClone{
x: i32
}
let mut dc = DoClone{x:1};
let mdc = &mut dc;
mdc.clone();

ToOwned和Clone的区别是什么?

1
2
3
4
5
6
pub trait ToOwned {
type Owned: Borrow<Self>;
fn to_owned(&self) -> Self::Owned;

fn clone_into(&self, target: &mut Self::Owned) { ... }
}

下面这个例子很经典,"123"是一个&str类型,对它调用clone,还会得到一个&str类型。但调用to_owned则会得到一个String类型。

1
2
let become_str = "123".clone();
let become_String = "123".to_owned();

Wrapper

Option

如何处理Option呢?

  1. unwrap+if
  2. match,并处理Some(e)None
  3. unwrap_or
  4. map组合子,and_then组合子
  5. ?
    得到Result<T, NoneError>

Result

如何处理Result呢?

  1. try!
  2. ?

指针和智能指针

C++中,为了突破栈上分配的限制会在堆上分配对象,Rust中为了避免移动,有更进一步的往堆上创建对象的需求。C++不会对指针进行资源管理,后面标准库也只是断断续续支持了一些智能指针,但Rust希望做得更周到一点。

在Rust中有下面的指针:

  1. *mut T/*const T
    这是C的裸指针
  2. Box
  3. Pin
  4. Rc
  5. Arc
    原子引用计数
  6. Ref
  7. RefCell
  8. Cow

Box

trait Deref/DerefMut

Deref是deref操作符*的 trait,比如*v。它的作用是:

  1. 对于实现了Copy对象,获得其拷贝
  2. 对于没有实现Copy的对象,获得其所有权

如下所示,一个智能指针对象U比如Box,如果它实现了U: Deref<Target=T>,那么Deref能够从它获得一个&T。实现上,我们从一个&Box<T>解两次引用,获得T,再返回&T。抽象一点来说,在实现了Deref后,能将&U变成&T,换种说法*x的效果就是*Deref::deref(&x)。这么做的好处是将所有奇怪的对智能指针的引用都转成&T

1
2
3
4
5
6
7
8
9
10
11
12
13
impl<T: ?Sized> Deref for &T {
type Target = T;
fn deref(&self) -> &T {
*self
}
}

impl<T: ?Sized, A: Allocator> Deref for Box<T, A> {
type Target = T;
fn deref(&self) -> &T {
&**self
}
}

而DerefMut如下所示,它允许我们从智能指针获取一个 &mut T。容易发现,如果一个智能指针没实现DerefMut,那么它实际上是 Immutable 的。

1
2
3
4
5
pub trait DerefMut: Deref {
/// Mutably dereferences the value.
#[stable(feature = "rust1", since = "1.0.0")]
fn deref_mut(&mut self) -> &mut Self::Target;
}

Rc/Arc

如下图所示,三个list中,bc共享 a 的所有权。我们可以用 Rc 来描述。

注意虽然Rc::clone(a)等价于a.clone(),但推荐使用Rc::clone,因为这显式表示它只增加引用计数。

1
2
3
4
5
6
7
8
9
10
11
12
13
enum List {
Cons(i32, Rc<List>),
Nil,
}

use crate::List::{Cons, Nil};
use std::rc::Rc;

fn main() {
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
let b = Cons(3, Rc::clone(&a));
let c = Cons(4, Rc::clone(&a));
}

只读和 &mut

需要注意的是,Rc 只允许各个所有者之间只读地进行共享。否则,如果各个所有者能修改,那么就有可能data race。也就是说,Rc/Arc 不实现 AsMut 和 DerefMut,从而做到禁止可变借用。事实上,Rc 会在编译期进行不可变借用的检查。

例如,如果 Rc 中持有 FnMut,则会导致 “cannot borrow data in an Rc as mutable” 报错

1
2
3
let mut a = 10;
let r = Rc::new(|| a += 1);
r();

尽管如此,Rc 还是通过 Rc::get_mut 提供一种获得 &mut T 的方法。它会在运行期用 Rc::is_unique 来判断是否为唯一引用,并返回 Some(&mut T) 或者 None

RefCell

RefCell是类似于Box的指针,但不同于引用和Box类型,RefCell在运行期检查借用。具体来说,RefCell在运行期检查:

  1. 在任意时刻只能获得一个&mut或任意个&
  2. 引用指向的对象是存在的

容易想到,RefCell的内部实现肯定会有unsafe块,才能绕过编译期的可变/不可变借用检查,而delay到运行期检查。但检查仍然是要求在任何时候只允许有多个不可变借用或一个可变借用。如果运行时检查出现问题,则会panic,如下所示。

1
2
3
// panic: already borrowed: BorrowMutError
let y1 = b.borrow_mut();
let y2 = b.borrow_mut();

RefCell在自己不可变的情况下,修改内部的值,这也就是内部可变性。可以类比为C++中一个const对象里面的mutable成员。那么RefCell也可以用在类似的场景下,例如一些需要存中间状态的状态机、Mocker等。

RefCell&&mut借用,分别对应了.borrow().borrow_mut()方法。

RefCell是Send/Sync的么?将在Sync/Send章节中介绍。

RefCell 的 borrow_mut 和 get_mut

上文介绍了,RefCell 访问对象需要通过 borrow 系列方法,但还有一个 get_mut 方法,它是做啥的呢?
根据文档可以发现,这个方法直接在编译器从 RefCell 中获取 &mut T。如下所示,这遵循编译期的检查,比如两次 &mut T 会在编译器挡掉而不是在运行期 panic,有点不把它当 RefCell 用的感觉。

1
2
3
4
5
6
// OK
let x1 = b.get_mut();
let x2 = b.get_mut();
// Compile Error
x1.store(false, std::sync::atomic::Ordering::SeqCst);
x2.store(true, std::sync::atomic::Ordering::SeqCst);

如果我们联用,会在编译期报错,不过报错内容比较有趣。它说b.get_mut()是个可变借用,而b.borrow_mut()是个不可变借用。为什么不是两次可变借用的冲突?原因很简单,RefCell 本来就是支持的内部可变性嘛,所以对于 Rust 来讲,这是个不可变借用没问题。

1
2
3
4
let x1 = b.get_mut();
let y1 = b.borrow_mut();
x1.store(false, std::sync::atomic::Ordering::SeqCst);
y1.store(false, std::sync::atomic::Ordering::SeqCst);

Mutex 可以和 RefCell 联用么?

Mutex 似乎自带了内部可变性。

1
2
3
4
5
6
let a = Mutex::new(1);
let mut lock = a.lock().unwrap();
*(lock.deref_mut()) = 2;

let a = Box::new(RefCell::new(1));
(*a.deref().borrow_mut().deref_mut()) = 2;

Rc+RefCell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn make_value(i: i32) -> Rc<RefCell<i32>> {
Rc::new(RefCell::new(i))
}

fn main() {
let value = make_value(5);
let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));
let b = Cons(make_value(1), Rc::clone(&a));
let c = Cons(make_value(2), Rc::clone(&a));
*value.borrow_mut() += 10;
let z = value.borrow_mut();
// *z = 11; // error
println!("a after = {:?}", a);
println!("b after = {:?}", b);
println!("c after = {:?}", c);
}

Pin

【建议在学习Pin之前,了解 Deref 和 DerefMut】
一个async fn会产生一个自引用结构AsyncFuture,因此它不能被移动。让一个对象不能被移动的第一步是将它分配到堆上,Box可以做到这一点。但这并不够,因为如下所示,std::mem::swap能够移动Box中的对象:

1
2
3
4
let mut rb = Box::new(TestNUnpin{b: "b".to_owned()});
let mut rb2 = Box::new(TestNUnpin{b: "a".to_owned()});
std::mem::swap(rb.as_mut(), rb2.as_mut());
println!("{} {}", rb.b, rb2.b); // Should be `a b`

另一方面,很多FFI会跨语言边界传递指针,这也需要保证地址是不变的。综上于是就有了Pin。Pin 中包裹了一个指针,如 Pin<&mut T> , Pin<&T> , Pin<Box<T>>,Pin 保证对应的 T 不会被移动。
其实在 C++ 中也会有自引用结构,并且也会造成相同的问题。

Pin分析了下,诸如std::mem::swap之流为什么能移动,原因是它们都能获得&mut T。所以只要限制可变借用,就可以在把对象Pin在堆上。限制获得可变引用简单啊,不实现AsMut就行。

Unpin 和 !Unpin 和 PhantomPinned

大部分的类型都被实现了 Unpin trait,表示能够随意被移动。

而一个可以被 Pin 住的值需要实现 !Unpin。因为 Rust 中带 ! 这样的称为 negative bounds,Rust 对它的支持还没有稳定下来。所以更一般的做法是让结构中持有一个 PhantomPinned 的 marker。

1
2
3
4
5
#[derive(Debug)]
struct StructCanBePinned {
a: String,
_marker: PhantomPinned,
}

std::marker::PhantomPinned中被实现了!Unpin,它会是持有它的结构体变成 !Unpin,从而无法被移动。

以上的容易理解,但为什么会有Unpin!Unpin呢?原因是需要给类型分类,讨论在Pin之前和之后类型的行为。这肯定难以理解,所以不妨先看看Pin是如何创建的,再回过来看。

Pin 对象的创建方式

在下面的代码中,对一个实现了 trait Unpin 的类型 Target,可以直接通过 Pin::new 产生一个 Pin<P> 对象。

1
2
3
4
5
6
7
8
impl<P: Deref<Target: Unpin>> Pin<P> {
pub const fn new(pointer: P) -> Pin<P> {
unsafe { Pin::new_unchecked(pointer) }
}
}
pub const unsafe fn new_unchecked(pointer: P) -> Pin<P> {
Pin { pointer }
}

但如果说是一个!Unpin的对象,Pin::new 会返回错误 “error[E0277]: PhantomPinned cannot be unpinned”;或者错误 “the trait Unpin is not implemented for TestNUnpin“,和”note: consider using Box::pin“。可以通过打开下面代码的注释来检查。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
use std::pin::Pin;

#[derive(Default, Debug)]
struct TestUnpin {
a: String,
}
#[derive(Default, Debug)]
struct TestNUnpin {
b: String,
}
impl !Unpin for TestNUnpin {}

fn main() {
let rp = Pin::new(&mut TestUnpin::default());
// let rnp = Pin::new(&mut TestNUnpin::default());
// let rnp2 = Pin::new(&TestUnpin::default()); // error[E0277]: `PhantomPinned` cannot be unpinned
let rnb = Box::pin(TestNUnpin::default());
}

使用不安全的 new_unchecked

我们可以通过 Pin::new_unchecked 来创建 !Unpin 的对象。但这是不安全的,因为我们不能保证传入的 pointer: P 指向的数据是被 pin 的。使用这个方法,需要保证 P::Deref/DerefMut 的实现中不能将 self 中的东西进行移动。这是因为 Pin 的 as_mutas_def 会调用 P 的 deref(_mut)

1
2
3
4
pub fn as_mut(&mut self) -> Pin<&mut P::Target> {
// SAFETY: see documentation on this function
unsafe { Pin::new_unchecked(&mut *self.pointer) }
}

我们可以构造出一个 evil 有问题的 case。在 DerefMut 中,我们将 b 的原值 move 了出来。结果打印出来已经有问题了。解决方案也很简单,我们始终 pin 住 &mut T 就行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#[derive(Default, Debug)]
struct EvilNUnpin {
b: String,
}
impl !Unpin for EvilNUnpin {}

impl Deref for EvilNUnpin {
type Target = String;

fn deref(&self) -> &Self::Target {
&self.b
}
}

impl DerefMut for EvilNUnpin {
fn deref_mut(&mut self) -> &mut Self::Target {
self.b = "3".to_owned();
&mut self.b
}
}

fn main() {
let mut x1 = EvilNUnpin { b: "1".to_owned() };
let mut x2 = EvilNUnpin { b: "2".to_owned() };
let ptr1 = &x1 as *const _ as isize;
let ptr2 = &x2 as *const _ as isize;
// Ok if we use &mut x1 and &mut x2.
let mut xp = unsafe { Pin::new_unchecked(x1) };
let mut xp2 = unsafe { Pin::new_unchecked(x2) };
std::mem::swap(&mut xp.as_mut(), &mut xp2.as_mut());
unsafe {
let n1 = &*(ptr1 as *const TestNUnpin);
let n2 = &*(ptr2 as *const TestNUnpin);
println!("{:?} {:?}", n1, n2);
}
}

此外,还需要保证这个 pointer 指向的对象不会再被移动,特别要注意不能以 &mut P::Target 这样的方式被移动,例如通过之前提的 mem::swap。
特别地,Pin 需要保证自己维护的指针不会再被移动了,**即使在自己销毁之后,也是不能被移动的**,但这个很难在编译期判定。如下代码所示,在两个 Pin 对象析构后,我们又可以移动对象 x1 和 x2 了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let mut x1 = TestNUnpin{ b: "1".to_owned() };
let mut x2 = TestNUnpin{ b: "2".to_owned() };
let ptr1 = &x1 as *const _ as isize;
let ptr2 = &x2 as *const _ as isize;
unsafe {
let _pin1 = Pin::new_unchecked(&x1);
let _pin2 = Pin::new_unchecked(&x1);
}
std::mem::swap(&mut x1, &mut x2);
unsafe {
let n1 = &*(ptr1 as *const TestNUnpin);
let n2 = &*(ptr2 as *const TestNUnpin);
// Shoule be 2 and 1.
assert_eq!(n1.b, "2");
assert_eq!(n2.b, "2");
}

对 Rc 使用 new_unchecked 也不安全

如下所示,我们可以获得 &mut T,从而又可以乱搞了。

1
2
3
4
5
6
7
8
9
10
11
use std::rc::Rc;
use std::pin::Pin;

let mut x = Rc::new(TestNUnpin{ b: "1".to_owned() });
let pinned = unsafe { Pin::new_unchecked(Rc::clone(&x)) };
{
let p = pinned.as_ref();
}
drop(pinned);
// We can get &mut T now.
assert_eq!(Rc::get_mut(&mut x).is_some());

使用安全的 Box::pin

使用 Box::pin 会产生一个 Pin<Box<T>>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let mut x1 = TestNUnpin{ b: "1".to_owned() };
let mut x2 = TestNUnpin{ b: "2".to_owned() };
let ptr1 = &x1 as *const _ as isize;
let ptr2 = &x2 as *const _ as isize;

let mut bx1 = Box::pin(x1);
let mut bx2 = Box::pin(x2);
std::mem::swap(&mut bx1, &mut bx2);

unsafe {
let n1 = &*(ptr1 as *const TestNUnpin);
let n2 = &*(ptr2 as *const TestNUnpin);
// Should still be 1 and 2.
assert_eq!(n1.b, "1");
assert_eq!(n2.b, "2");
}

为什么 Box::pin 可以 Pin 住 !Unpin
查看 Box::pin 的实现。它传入一个 T,然后创建一个Box<T>并立马 Pin 住它。

1
2
3
pub fn pin(x: T) -> Pin<Box<T>> {
(box x).into()
}

在Pin之前,无法移动T,这是因为只能同时有一个可变借用&mut T
在Pin之后,无法移动T,这是因为Box被实现为owned且unique的。可以参考下面的代码

1
2
3
4
5
6
let mut t = TestNUnpin{b: "b".to_owned()};
let mt = &mut t;
let b = Box::pin(&mut t);
let mut t2 = TestNUnpin{b: "a".to_owned()};
std::mem::swap(mt, &mut t2);
println!("{} {}", t.b, t2.b);

使用安全的 pin_utils

还可以使用 pin_utils::pin_mut!。对于下面的代码,我们考量上述 new_unchecked 安全性的几点保证:

  1. 控制 Deref(Mut)
    pin 的是 &mut T 而不是 T
  2. 不能取出 &mut T
    这很简单,因为开始的 $x 已经被 shadow 了。
  3. 不能再次移动 T
    同上。
1
2
3
4
5
6
7
8
9
10
11
12
13
#[macro_export]
macro_rules! pin_mut {
($($x:ident),* $(,)?) => { $(
// Move the value to ensure that it is owned
let mut $x = $x;
// Shadow the original binding so that it can't be directly accessed
// ever again.
#[allow(unused_mut)]
let mut $x = unsafe {
$crate::core_reexport::pin::Pin::new_unchecked(&mut $x)
};
)* }
}

这里的 shadow 非常重要,我们用下面的例子来说明。可以看到 xp 并没有 shadow 住 x,因此在它被 drop 后,x 又可以被 mutable borrow 了。所以 pin_mut 的实现中保证了

1
2
3
4
5
6
7
8
9
let mut x = TestNUnpin { b: "b".to_owned() };
// mutable borrow begins
let mut xp = unsafe { Pin::new_unchecked(&mut x) };
drop(xp);
// mutable borrow ends
assert_eq!(x.b, "b");
let mut x2 = TestNUnpin { b: "b2".to_owned() };
std::mem::swap(&mut x, &mut x2);
assert_eq!(x.b, "b2");

另外,这里的 let mut $x = $x 也很重要,它使得下面的代码可以编译

1
2
3
4
5
6
7
fn main() {
let mut x = 5;
let x_mut = &mut x;
pin_mut!(x_mut);
**x_mut = 10;
print!("{}", x);
}

同时它可以拒绝

1
2
3
4
5
6
7
8
9
10
let mut foo = Foo { ... };

{
pin_mut!(foo);
let _: Pin<&mut Foo> = foo;
}

// Woops we now have an unprotected Foo when its supposed to be pinned and
// thus can break the guarantees of Pin::new_unchecked
let foo_ref: &mut Foo = &mut foo;

总结

总结几个疑问:

  1. 为什么可以直接 Pin::new 一个 Unpin 对象?
    因为对于实现Unpin类型的对象,Pin不做任何保证。

  2. 为什么不能直接 Pin::new 一个 !Unpin 的对象?
    因为这是不安全的,所以要么 unsafe 地 Pin::new_unchecked 来创建,要么借助于诸如 Box::pin 等安全的方法。

  3. 如果 T 是 Unpin,能获得 Pin 里面的 &mut 么?
    可以通过Pin::get_mut获得

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    let mut p = TestUnpin{ "a".to_owned() };
    let mut p2 = TestUnpin{ "b".to_owned() };
    let mut rp = unsafe {
    Pin::new(&mut p)
    };
    let mut rp2 = unsafe {
    Pin::ne(&mut p2)
    };
    std::mem::swap(Pin::get_mut(rp), Pin::get_mut(rp2));
    println!("{} {}", p.a, p2.a); // Should be `a b`

    但如果类型是!Unpin,我们就不能调用 Pin::get_mut

现在回答为什么要有Unpin和!Unpin的问题。对于Unpin类型,它实际上是给Pin做了一个担保,告诉Pin即使我这个类型被移动了也没事,所以Pin对它的作用就是屏蔽了&mut的获取渠道。对于!Unpin和PhantomPinned类型,它们是真的不能被移动的,这不仅要借助Pin,这些类型自己也要提供一个合适的接口,从它们来创建Pin。

Pin 和内部可变性

是不是被 Pin 的对象就不可以有内部可变性呢?不妨考虑下面一个更简单的对象,我们修改 a 的值,并不会导致任何地址上的变化,所以这个对象是可以有内部可变性的。
Pin 使得下面的代码不可编译,并报错”trait DerefMut is required to modify through a dereference, but it is not implemented for Pin<&mut SimpleNUnPin>“。

1
2
3
4
5
6
7
8
9
10
11
12
struct SimpleNUnPin {
a: u64,
}

impl !Unpin for SimpleNUnPin {}

fn main()
{
let x = SimpleNUnPin { a: 1 };
pin_utils::pin_mut!(x);
x.as_mut().a = 2;
}

那么,我们能够通过 RefCell 获得内部可变性么?这其实不安全。

生命周期

生命周期(lifetime)是编译期中的 borrow checker 用来检查所有的借用都 valid 的结构。
当在结构体中持有一个引用时,需要指定生命周期,从而防止悬垂引用。

计算生命周期

下面的代码展示了 Lifetime 和 Scope 的区别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn main() {
let i = 3; // Lifetime for `i` starts. ────────────────┐
// │
{ // │
let borrow1 = &i; // `borrow1` lifetime starts. ──┐│
// ││
println!("borrow1: {}", borrow1); // ││
} // `borrow1 ends. ──────────────────────────────────┘│
// │
// │
{ // │
let borrow2 = &i; // `borrow2` lifetime starts. ──┐│
// ││
println!("borrow2: {}", borrow2); // ││
} // `borrow2` ends. ─────────────────────────────────┘│
// │
} // Lifetime ends. ─────────────────────────────────────┘

如下所示,发生了移动,只有一次析构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct S {
a: u64,
}

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

fn main() {
let s = {
let s = S {
a: 1
};
s
};
}

但对于下面的代码,则会返回错误”error[E0597]: s.a does not live long enough”。这应该是 Rust 对自引用结构支持不够的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
use std::cell::RefCell;

struct S<'a> {
a: u64,
ra: RefCell<Option<&'a u64>>,
}

// impl<'a> Drop for S<'a> {
// fn drop(&mut self) {
// println!("drop!");
// }
// }

fn main() {
let s = {
let s = S {
a: 1,
ra: RefCell::new(None),
};
*s.ra.borrow_mut() = Some(&s.a);
s
};
let b = Box::into_raw(Box::new(s));
println!("{}", (*b).a);
}

声明周期注解

下面表示 foo 具有生命周期参数 ‘a 和 ‘b,并且 foo 的 lifetime 不会超过 ‘a 和 ‘b 的 lifetime。

1
2
foo<'a, 'b>
// `foo` has lifetime parameters `'a` and `'b`

函数

不考虑省略:

  1. 所有引用参数都需要带一个生命周期参数
  2. 返回的引用要么是’static,要么是和输入一样的生命周期

下面编译出错。这里,'a must live longer than the function,也就是说函数运行完之后,’a 应该还在 lifetime 中。而这里的 &String 在函数返回前就析构了,所以它肯定不满足 ‘a 的约束。

1
fn invalid_output<'a>() -> &'a String { &String::from("foo") }

方法

下面两个代码实际是等价的,所以如果希望 self 的生命周期就是 impl 的,那么应该加上 ‘a。

1
2
3
4
5
6
7
impl<'a> Foo<'a> {
fn foo(&'a self, path: &str) -> Boo<'a> { /* */ }
}

impl<'a> Foo<'a> {
fn foo<'b>(&'b self, path: &str) -> Boo<'b> { /* */ }
}

当然,未必要给 impl 加 lifetime 参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct Owner(i32);

impl Owner {
// Annotate lifetimes as in a standalone function.
fn add_one<'a>(&'a mut self) { self.0 += 1; }
fn print<'a>(&'a self) {
println!("`print`: {}", self.0);
}
}

fn main() {
let mut owner = Owner(18);

owner.add_one();
owner.print();
}

Elision

闭包和函数

Fn/FnMut/FnOnce

Rust对a(b,c,d)这样的调用搞了个有点像Haskell中的$的东西,目的是为了重载“对函数的调用”

1
2
3
Fn::call(&a, (b, c, d))
FnMut::call_mut(&mut a, (b, c, d))
FnOnce::call_once(a, (b, c, d))

FnOnce会获取自由变量的所有权,并且只能调用一次,调用完会把自己释放掉。
FnMut会可变借用自由变量。
Fn会不可变借用自由变量。
FnMutFn都可以调用多次。

可以用下面的代码确定某个函数具体实现了哪个trait,实现了的trait能够通过编译。

1
2
3
4
fn is_fn <A, R>(_x: fn(A) -> R) {}
fn is_Fn <A, R, F: Fn(A) -> R> (_x: &F) {}
fn is_FnMut <A, R, F: FnMut(A) -> R> (_x: &F) {}
fn is_FnOnce <A, R, F: FnOnce(A) -> R> (_x: &F) {}

查看代码,发现三者具有继承关系Fn : FnMut : FnOnce

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
pub trait FnOnce<Args> {
/// The returned type after the call operator is used.
#[lang = "fn_once_output"]
#[stable(feature = "fn_once_output", since = "1.12.0")]
type Output;

/// Performs the call operation.
#[unstable(feature = "fn_traits", issue = "29625")]
extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
}

pub trait FnMut<Args>: FnOnce<Args> {
/// Performs the call operation.
#[unstable(feature = "fn_traits", issue = "29625")]
extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output;
}

pub trait Fn<Args>: FnMut<Args> {
/// Performs the call operation.
#[unstable(feature = "fn_traits", issue = "29625")]
extern "rust-call" fn call(&self, args: Args) -> Self::Output;
}

为什么是这样的继承关系呢这篇回答给出了解释。

确实可以让FnOnce、FnMut和FnOnce做7种自由组合,但其中只有三种traits是有意义的:

  1. Fn/FnMut/FnOnce
  2. FnMut/FnOnce
  3. FnOnce

这是因为,如果传入&self可以解决的问题,传入&mut self也可以解决。传入&mut self可以解决的问题,传入self也可以解决。但反之就不一定成立。

所以 self 是大哥级的人物,动用了伤害很大,它能够解决一切的问题,所以他是最 base 的 trait,而不是最 derive 的 trait。

闭包对三个trait的实现

  1. 所有的闭包都实现了FnOnce
  2. 如果闭包只移出了所有权,则只实现FnOnce
  3. 如果闭包没移出所捕获变量的所有权,并修改了变量,则实现FnMut
  4. 如果闭包没移出所捕获变量的所有权,且没有修改变量,则实现Fn

move关键字不会改变闭包具体实现的trait,而只影响变量的捕获方式,我们将在下节讨论。

捕获

上面的章节中介绍了闭包可能实现的三个trait,这个章节说明闭包如何捕获环境中的变量。

C++中捕获的问题

在C++中,返回一个捕获了Local变量的闭包,是有安全问题的,见get_f()
对于类中的方法,如果捕获了this指针(即使是[=])并传出,在对象析构之后也是有问题的,见print_this_proxy()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <iostream>
#include <functional>
struct S {
int x_ = 0;
S(int x) : x_(x) {}
~S() {
printf("Bye\n");
}
std::function<void()> print_this_proxy() {
return [=](){
// Capture this->x_
printf("x_ %d", x_);
};
}
};

std::function<void()> get_f() {
S s(1);
auto f = [&](){
printf("S %d\n", s.x_);
};
return f;
}

int main(){
auto f = get_f();
f(); // Not safe

std::function<void()> proxy;
{
S s(2);
proxy = s.print_this_proxy();
}
proxy(); // Not safe
}

Rust的捕获

Rust的捕获相比C++使人比较困惑。首先它没有地方指定捕获哪些变量;另外,还有个move关键字;最后还会加上复制和移动语义。

1
2
3
4
5
|| 42;
|x| x + 1;
|x:i32| x + 1;
|x:i32| -> i32 { x + 1 };
move |x:i32| -> i32 { x + 1 };

闭包按照什么方式捕获,取决于我们打算如何使用捕获后的变量。

我们不妨看一个例子,首先定义下面的结构。get_numberinc_numberdestructor分别需要传入不可变引用,可变引用以及值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct MyStruct {
text: &'static str,
number: u32,
}
impl MyStruct {
fn new (text: &'static str, number: u32) -> MyStruct {
MyStruct {
text: text,
number: number,
}
}
fn get_number (&self) -> u32 {
self.number
}
fn inc_number (&mut self) {
self.number += 1;
}
fn destructor (self) {
println!("Destructing {}", self.text);
}
}

下面代码展示了类似fn的情况,这里fn并没有捕获任何自由变量,因此下面的代码可以正常编译和运行。

1
2
3
4
5
6
7
8
9
10
let obj1 = MyStruct::new("Hello", 15);
let obj2 = MyStruct::new("More Text", 10);
let closure1 = |x: &MyStruct| x.get_number() + 3;
assert_eq!(closure1(&obj1), 18);
assert_eq!(closure1(&obj2), 13);

is_fn(closure1);
is_Fn(&closure1);
is_FnMut(&closure1);
is_FnOnce(&closure1);

下面的代码展示了Fn的情况,这里closure2捕获了obj1的引用。后面的代码进行验证,仍然可以obj1.get_number()来不可变借用,但需要可变引用的obj1.inc_number()就不能通过编译了。

1
2
3
4
5
6
7
8
9
let obj1 = MyStruct::new("Hello", 15);
let obj2 = MyStruct::new("More Text", 10);
// obj1 is borrowed by the closure immutably.
let closure2 = |x: &MyStruct| x.get_number() + obj1.get_number();
assert_eq!(closure2(&obj2), 25);
// We can borrow obj1 again immutably...
assert_eq!(obj1.get_number(), 15);
// But we can't borrow it mutably.
// obj1.inc_number(); // ERROR

事实上,闭包类似语法糖,相当于把需要捕获的上下文封装到一个Context里面传给真正的执行单元。例如我们可以改写closure2,得到一个自由函数func2。它接受一个Context对象,里面封装了一个不可变引用,并且其生命周期等于Context的生命周期。

1
2
3
4
5
6
7
8
9
10
11
12
struct Context<'a>(&'a MyStruct);
let obj1 = MyStruct::new("Hello", 15);
let obj2 = MyStruct::new("More Text", 10);
let ctx = Context(&obj1);
fn func2 (context: &Context, x: &MyStruct) -> u32 {
x.get_number() + context.0.get_number()
}
assert_eq!(func2(&ctx, &obj2), 25);
// We can borrow obj1 again immutably...
assert_eq!(obj1.get_number(), 15);
// But we can't borrow it mutably.
// obj1.inc_number(); // ERROR

其实细心观察,就可以提出反对意见。上面的case中不能obj1.inc_number()原因是我们没有let mut obj1 = ...,如果加上去就能正常编译了。这不就是同时Immutable和Mutable Borrow了么?其实我们在最后加一行,再调用一次func2就能报错了。看起来Rust还蛮智能的,func2虽然可变借用,但后续没有用到了,所以就不影响obj1.get_number()

1
2
3
4
...
assert_eq!(func2(&ctx, &obj2), 25);
assert_eq!(obj1.get_number(), 15);
assert_eq!(func2(&ctx, &obj2), 26);

下面的代码展示了FnMut的情况。现在闭包里就直接是可变借用了。在闭包之外,我们既不能可变借用,也不能不变借用,否则都无法编译。

1
2
3
4
5
6
7
8
9
10
11
12
13
let mut obj1 = MyStruct::new("Hello", 15);
let obj2 = MyStruct::new("More Text", 10);
// obj1 is borrowed by the closure mutably.
let mut closure3 = |x: &MyStruct| {
obj1.inc_number();
x.get_number() + obj1.get_number()
};
assert_eq!(closure3(&obj2), 26);
assert_eq!(closure3(&obj2), 27);
assert_eq!(closure3(&obj2), 28);
// We can't borrow obj1 mutably or immutably
// assert_eq!(obj1.get_number(), 18); // ERROR
// obj1.inc_number(); // ERROR

下面的代码展示了FnOnce的情况

1
2
3
4
5
6
7
let obj1 = MyStruct::new("Hello", 15);
let obj2 = MyStruct::new("More Text", 10);
// obj1 is owned by the closure
let closure4 = |x: &MyStruct| {
obj1.destructor();
x.get_number()
};

尝试用四个函数检查下,发现上面三个trait的检查都无法通过编译,也就说明closure4没有实现上面三个trait。

1
2
3
4
5
6
// Does not compile:
// is_fn(closure4);
// is_Fn(&closure4);
// is_FnMut(&closure4);
// Compiles successfully:
is_FnOnce(&closure4);

可以发现,闭包捕获变量按照&T -> &mut T -> T的顺序,和Fn -> FnMut -> FnOnce的继承关系如出一辙。也就是先派小弟尝试捕获,小弟解决不了,再请老大出山的思路。

当然,可以通过move关键字,强行请老大出山。

对于move/move async捕获,如果闭包中需要使用某个变量例如p,并且在闭包调用完之后,还需要继续访问,则需要在调用闭包前进行clone,例如得到pp。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#[derive(Debug,Clone)]
struct Point {
x: i32,
y: i32,
}

fn foo() {
let p = Point {x: 1, y: 2};
let pp = p.clone();
let total_price = move | price: i32| {
p.x * p.y * price
};
let price = total_price(10);
println!("p {:?} price {}", pp, price);
}

闭包能否被多个线程使用?

https://stackoverflow.com/questions/36211389/can-a-rust-closure-be-used-by-multiple-threads

并发与异步

Send和Sync

trait Send表示该类型的实例可以在线程之间移动。大多数的Rust类型都是Send的,另一些则不可以。比如引用计数智能指针Rc<T>只能在线程内部使用,它就不能被实现为Send的;此外裸指针也不是Send的。由Send类型组成的新类型也是Send的。

trait Sync表示多个线程中拥有该类型实例的引用。换句话说,对于任意类型T,如果&T是Send的,那么T就是Sync的。

一些常见类型对Send和Sync的支持

Rc

Rc并不是Send的。原因是Rc共享同一个引用计数块,并且更新引用计数并不是原子的。如果两个线程同时尝试clone,那么它们可能同时更新引用计数,从而可能会UB。

Arc

如果T是Send和Sync的,那么Arc<T>是Send和Sync的

1
2
unsafe impl<T: ?Sized + Sync + Send> Send for Arc<T> {}
unsafe impl<T: ?Sized + Sync + Send> Sync for Arc<T> {}

初看这很奇怪,难道不是为了并发安全才用的Arc么?为什么反过来Arc还需要一个并发安全的类型呢?其实和C++一样,智能指针的线程安全包含两个层面,即智能指针本身实现,特别是引用计数的线程安全;以及智能指针保护的数据的线程安全:

  1. Arc相对Rc只是保证了引用计数这一块功能是并发安全的
  2. 如果类型不是并发安全的,通常需要配合RwLock和Mutex等使用。

RefCell

定义看,RefCell是Send的,但不是Sync的。很容易理解,一个具有内部可变性的对象的引用被各个线程持有,那岂不是可以瞎改了?

1
2
3
4
#[stable(feature = "rust1", since = "1.0.0")]
unsafe impl<T: ?Sized> Send for RefCell<T> where T: Send {}
#[stable(feature = "rust1", since = "1.0.0")]
impl<T: ?Sized> !Sync for RefCell<T> {}

引用

&T需要T是Sync的,这个对应了上面的定义。
&mut T需要T是Send的

1
2
unsafe impl<T: Sync + ?Sized> Send for &T {}
unsafe impl<T: Send + ?Sized> Send for &mut T {}

裸指针

各类指针都不是Send的

1
2
impl<T: ?Sized> !Send for *const T {}
impl<T: ?Sized> !Send for *mut T {}

当然可以简单包一层,从而间接得到可以Send或Sync的裸指针

1
2
3
4
struct MyBox(*mut u8);

unsafe impl Send for MyBox {}
unsafe impl Sync for MyBox {}

当然,也可以用同样的办法,通过negative_impls,取消某些已经被Send/Sync的类型的特性。

1
2
3
4
5
6
7
#![feature(negative_impls)]

// I have some magic semantics for some synchronization primitive!
struct SpecialThreadToken(u8);

impl !Send for SpecialThreadToken {}
impl !Sync for SpecialThreadToken {}

Mutex和RwLock

Mutex需要Send,但不需要Sync。

1
2
3
4
#[stable(feature = "rust1", since = "1.0.0")]
unsafe impl<T: ?Sized + Send> Send for Mutex<T> {}
#[stable(feature = "rust1", since = "1.0.0")]
unsafe impl<T: ?Sized + Send> Sync for Mutex<T> {}

RwLock不仅需要Send,还需要Sync,从而保证能被Reader们共享读取。

1
2
3
4
#[stable(feature = "rust1", since = "1.0.0")]
unsafe impl<T: ?Sized + Send> Send for RwLock<T> {}
#[stable(feature = "rust1", since = "1.0.0")]
unsafe impl<T: ?Sized + Send + Sync> Sync for RwLock<T> {}

Future

Future的所有权可能在各个线程之间移动,那为什么Future不是Send的呢

多线程

因为Rust目前不支持可变参数包,所以只能通过spawn闭包的形式创建线程

如果子线程panic了,其他线程是没影响的,除非:

  1. 某个线程join了panic的线程,此时会得到一个包含Err的Result,如果直接unwrap则会panic
  2. 如果线程在获得锁后panic,这种现象称为poison
    此时,再次尝试mutex.lock()会得到PoisonError,并且mutex.is_poisoned()会返回true。

线程间同步

线程间通信

可以使用类似Go的Channel的方式来通信,也就是所谓的Do not communicate by sharing memory; instead, share memory by communicating。
这里mpsc是multiple producer, single consumer的意思。
send方法返回一个Result<T, E>类型,所以如果接收端已经被丢弃了,将没有发送值的目标,所以发送操作会返回错误。

1
2
3
4
5
6
7
8
9
10
11
use std::sync::mpsc;
use std::thread;

fn main() {
let (tx, rx) = mpsc::channel();

thread::spawn(move || {
let val = String::from("hi");
tx.send(val).unwrap();
});
}

因为生产者是可以有多个的,所以tx.clone()可以产生另一个生产之。

异步

async和await

下面展示了两种async的写法,一种是async函数plus_one,另一种是async块plus_two。对async函数,编译器在实现时也会最终转成async块的形式,并且会改写函数签名。
所以,可以理解为async函数最终是返回了一个impl Future<Output = ...>类型,impl Future表示这个类型实现了trait Future,这应该就是impl Trait这个特性。Output是我们期望这个Future在Ready后实际返回的类型,比如在这里就是i32。

1
2
3
4
5
6
7
8
9
10
11
async fn initial() -> i32 {
1
}
fn plus_one() -> impl Future<Output = i32> {
async {
initial().await + 1
}
}
async fn plus_two() -> i32 {
initial().await + 2
}

plus_one_resplus_two_res都是Future,可以通过block_on获取结果。

1
2
3
4
5
6
fn main() {
let plus_one_res = plus_one();
let plus_two_res = plus_two();
println!("{}", futures::executor::block_on(plus_one_res));
println!("{}", futures::executor::block_on(plus_two_res));
}

我们也可以通过join同时await多个Future。

1
2
3
4
5
futures::executor::block_on(async {
let j = futures::future::join(plus_one_res, plus_two_res).await;
println!("{}", j.0);
println!("{}", j.1);
});

trait Future

这里指的是std::future::Future,因为在早前还有futures::future::Future,它是一个“社区版”的时候,后来trait Future被整合到了标准库中,剩余部分整合到了pub trait FutureExt: Future,其中包含了map/then等操作。当然对于trait Future还有其他的扩展,例如async-std。但回过头,先来看看最基础的trait Future

1
2
3
4
pub trait Future {
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}

poll函数返回的Poll是个enum,包含Ready(T)Pending两个状态。但它并不只有忙等,如果在一次poll后返回的是Pending,那就会注册cx.waker这个回调,在Future后调用进行通知。

Pin<&mut Self>实际是个指针,它是为了解决自引用结构的问题。

impl Future

impl了trait Future的类型有很多,例如f.map生成的Map,f.then生成的Then这些组合子都是Future。

例如,下面代码为Map类型impl Future。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
pub struct Map<I, F> {
// Used for `SplitWhitespace` and `SplitAsciiWhitespace` `as_str` methods
pub(crate) iter: I,
f: F,
}

// in futures-0.1.31, src/future/map.rs
// 注意,这是一个较老的版本,所以future.poll的签名也不一样。在futures-0.3.15中该实现被挪到了futures-util中
impl<U, A, F> Future for Map<A, F>
where A: Future,
F: FnOnce(A::Item) -> U,
{
type Item = U;
type Error = A::Error;

fn poll(&mut self) -> Poll<U, A::Error> {
let e = match self.future.poll() {
Ok(Async::NotReady) => return Ok(Async::NotReady),
Ok(Async::Ready(e)) => Ok(e),
Err(e) => Err(e),
};
e.map(self.f.take().expect("cannot poll Map twice"))
.map(Async::Ready)
}
}

这里e.map().map()比较独特,前一个map是把self.f应用到e里面的东西,并且清空self.f,让它成为一次性的调用。后一个是把将map的结果包在Async::Ready里面。

async的生命周期

async实现

我们知道,因为在async实现中会产生自引用结构,所以需要用Pin,那什么是自引用结构?为什么async中会存在这种结构呢?
首先得从async的实现讲起

普通情况

看下面代码,f1和f2两个await之间是串行的,那么编译器如何生成f.await的代码呢?

1
2
3
4
5
let f = async move {
f1.await;
f2.await;
}
f.await;

如果是用Then的方式,那么就通过回调实现,但这里Rust使用了状态机的方式,即编译器会生成类似下面的代码AsyncFuture实际上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
struct AsyncFuture {
fut_one: FutOne,
fut_two: FutTwo,
state: State,
}

enum State {
AwaitingFutOne,
AwaitingFutTwo,
Done,
}

impl Future for AsyncFuture {
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
loop {
match self.state {
State::AwaitingFutOne => match self.fut_one.poll(..) {
Poll::Ready(()) => self.state = State::AwaitingFutTwo,
Poll::Pending => return Poll::Pending,
}
State::AwaitingFutTwo => match self.fut_two.poll(..) {
Poll::Ready(()) => self.state = State::Done,
Poll::Pending => return Poll::Pending,
}
State::Done => return Poll::Ready(()),
}
}
}
}

为什么async实现会涉及自引用结构?

在之前,已经讨论过编译async的普通情况。考虑下面的代码,应该如何编译呢?

1
2
3
4
5
6
async {
let mut x = [0; 128];
let read_into_buf_fut = read_into_buf(&mut x);
read_into_buf_fut.await;
println!("{:?}", x);
}

因为在await时可能发生线程切换,所以我们需要将x也转移到生成的AsyncFuture中,那么read_into_buf就会产生一个指向x的引用。如果在一个结构中,某个字段是指向另一个字段的引用,这就是一个自引用结构。

1
2
3
4
5
6
7
8
struct ReadIntoBuf<'a> {
buf: &'a mut [u8], // points to `x` below
}

struct AsyncFuture {
x: [u8; 128],
read_into_buf_fut: ReadIntoBuf<'what_lifetime?>,
}

自引用结构

自引用结构如下,b是一个指向a的引用

1
2
3
4
struct Test<'a> {
a: String,
b: &'a String,
}

但很遗憾,Rust现在不支持自引用结构,导致下面的代码会报错(之前在 lifetime 部分也提过)

1
2
3
4
fn main() {
let a = String::from("Hello");
let _test = Test { a, b: &a };
}

作为workaround,又得用裸指针。但这玩意有个问题,它的地址是绝对的。当Test被移动了,b指向的地址并不会变化。这就好比反过来的刻舟求剑,我们希望b是一个刻在船(Test)上的地址,但实际上它是个GPS坐标。

1
2
3
4
struct Test {
a: String,
b: *const String,
}

then用法

在没有async和await时,可以使用then系列的用法。

类型系统

数组和切片

数组的签名是[T;N],和C++一样,数组类型中包含了它的大小,是编译期常量。数组是否是Copy/Clone取决于其内部的类型,但如果使用[x, N]创建数组,则x对应的类型必须是Copy的。数组引用&[T;N]可以转换为切片引用&[T]

与之对应的是切片&[T]&mut [T]

切片的方法

Sized、!Sized、?Sized和DST

Dynamically sized type(DST),即动态大小类型,表示在编译阶段无法确定大小的类型

如果一个类型在编译期是已知Size,并且Size固定不变的,那么它会自动实现trait Sized。
但有些类型是无法在编译期确定大小的,例如str的大小是未知的(所以我们一般通过&str来访问它),一个trait的大小也是未知的。

如果一个类型的大小是未知的,那么它的使用会有限制,例如我们不能Vec<T>,而只能将T放到Box里面,做成Vec<Box<T>>

胖指针

胖指针指的是指向DST的引用或者指针

1
2
3
4
5
size_of::<&u32>()      = 8
size_of::<&[u32; 2]>() = 8
size_of::<&[u32]>() = 16
size_of::<&[u32]>() = 16
size_of::<&mut [u32]>() = 16

特别强调,指针也是胖的。考虑到C++允许直接使用delete析构POD数组,这也是和C++部分一致的。

1
2
size_of::<*const [u32]>() = 16
size_of::<*mut [u32]>() = 16

可以看到,&[u32]具有两倍大小,原因是其中还储存了一份长度,如下所示

1
2
3
4
struct SliceRef { 
ptr: *const u32,
len: usize,
}

不能直接把变量绑定到一个DST上,因为编译器无法计算出如何分配内存。例如我们经常见到&str,但基本见不到str

除了切片,trait object也是DST,它还包含了一个vptr,将在后面讨论。

1
2
3
4
struct TraitObjectRef {
data_ptr: *const (),
vptr: *const (),
}

Rust中的字符串

Rust中的字符串是很好的比较数组和切片的工具。和C++一样,Rust有两种字符串:

  1. str
    str是Rust的原生字符串类型。因为是DST,所以通常以&str出现。
  2. String
    String类型可以随时修改其长度和内容。

str

&str相关方法实现在str.rs的impl str中。通过.as_ptr()将其转换为一个*const u8指针,通过.len()获得其长度。

字符串字面量的类型是&'static str

&str&[u8]可以互相转换。

String

struct

tuple struct

Int 是一个别名,Interger 是一个新的类型。这种形式称为 tuple struct。

1
2
type Int = i32
struct Interger(u32)

ZST

在C++中,会接触到这样的类型

1
2
3
4
5
6
struct ZST {
};

sizeof(ZST)

&ZST() != &ZST()

在Rust中

ZST实例的地址是什么呢?

never类型和!

诸如returnbreakcontinuepanic!()loop 没有返回值的,或者说返回值类型是never!,对应到类型理论中就是Bottom类型
never类型可以转换为其他任何类型,所以在诸如match中才能下入如下代码而不会产生类型错误

1
None => panic!

如下的发散函数也没有返回值,因此也具有never类型

1
2
3
4
5
6
fn foo() -> u32
{
let x: ! = {
return 123;
}
}

类型推导

通过turbofish可以辅助推导,下面列出一些例子

1
2
3
4
5
6
7
8
9
10
x.parse::<i32>()
[
AdminCmdType::CompactLog,
AdminCmdType::ComputeHash,
AdminCmdType::VerifyHash,
]
.iter()
.cloned()
.collect::<std::collections::HashSet<AdminCmdType>>()
// can also use std::collections::HashSet<_>

trait

trait类似于Haskell中的typeclass。

trait和adhoc多态

见笔记

关联类型

关联类型(associated types)是一个将类型占位符(也就是下面的type Output)与trait相关联的方式。

考虑如果某个类型impl了trait Add,那么它可以接受一个RHS类型的右操作数,并返回Output类型的结果。

1
2
3
4
5
6
7
8
pub trait Add<RHS, Output> {
fn my_add(self, rhs: RHS) -> Output
}
impl Add<u32, u32> for u32 {
fn my_add(self, ths: u32) -> u32 {
self + rhs
}
}

但考虑到trait Add可以接受的RHS可能是多种(例如对String而言可以接受String&str),但返回的Output类型是确定的,所以可以将Output类型从由用户指定改为由实现方指定。此时就可以定义一个关联类型type Output

1
2
3
4
pub trait Add<RHS = Self> {
type Output;
fn add(self, rhs: RHS) -> Self::Output
}

trait的继承

struct不能继承,但是trait可以继承。

这里涉及到泛型约束的问题,例如我们impl的是两个Father的交集还是并集呢?

1
2
3
trait Son: Father1 + Father2 {
}
impl <T: Father1 + Father2> Son for T {}

在这里Father1和Father2是取的交集,也就是说对所有实现了Father1和Father2的T实现Son。

孤儿规则(Orphan Rule)

泛型约束

例如我们实现sum函数,它只能接受泛型参数T是实现了trait Add的。可以这样写

1
fn sum<T: Add<T, Output=T>>

因为使用了关联参数,所以还可以简写成这样

1
fn sum<T: Add<Output=T>>

如果要写的比较多,可以把里面的东西拿出来,用where来写

静态分发和动态分发

静态分发和动态分发是对trait而言的。
下面是静态分发,为fly_static::<Pig>fly_static::<Duck>生成独立的代码。这类似于C++里面的模板实例化。

1
2
3
fn fly_static<T: Fly>(S: T) -> bool {

}

下面是动态分发,在运行期查找fly_dyn(&Fly)对应类型的方法,例如实际传入的是&Duck还是&Pig,是不一样的。这类似C++里面的动态绑定,是有运行时开销的。

1
2
3
fn fly_dyn(S: &Fly) -> bool {

}

问题来了,这里的&Fly是啥呢?实际上这是后面讨论的trait对象。

trait作为存在类型(Existential Type)

存在类型,又被称为无法直接实例化,它的每个实例是具体类型的实例。
对于存在类型,编译期无法知道其功能和Size,目前Rust使用trait object和impl Trait处理存在类型。

trait object

如下所示,fly_dyn中的&Fly参数就是一个trait object。

1
2
3
fn fly_dyn(S: &Fly) -> bool {

}

TraitObject 可以看成具有下面的组织结构

1
2
3
4
5
6
7
#[repr(C)]
#[derive(Copy, Clone)]
#[allow(missing_debug_implementations)]
pub struct TraitObject {
pub data: *mut (),
pub vtable: *mut (),
}

vtable中包含了对象的析构函数、大小、对齐、方法(也就是虚函数指针)等信息。

只有对象安全的trait才可以作为trait object来使用:

  1. 该trait的Self不能被限定为Sized
  2. 该trait的所有方法必须是对象安全的
    1. 方法受Self: Sized约束
    2. 不包含任何泛型参数
    3. 第一个参数必须为Self类型,或者可以解引用为Self的类型
    4. Self不能出现在出第一个参数之外的地方,包括返回值中

impl Trait

在目前的版本中,不能在trait中返回impl Trait,也就是下面的代码无法编译。只能使用Trait Object。

1
2
3
4
5
pub trait Vehicle {
fn next(&self) -> impl Vehicle;
}

// `impl Trait` not allowed outside of function and inherent method return types

标准库

连接

默认情况下Rust编译时会link标准库,通过添加no_std属性可以关闭这个行为。

字符串

字符串相关的结构之间的转换

包括strString&[u8]Vec<u8>

macro 宏

macro 的 import 和 export

macro 有两种 scope,textual scope 和 path-based scope。这里的 path 有专门的定义,可以理解为类似crate::a::b或者super::a::b这样的东西。

1
2
3
4
5
6
7
8
use lazy_static::lazy_static; // Path-based import.

macro_rules! lazy_static { // Textual definition.
(lazy) => {};
}

lazy_static!{lazy} // Textual lookup finds our macro first.
self::lazy_static!{} // Path-based lookup ignores our macro, finds imported one.

在通过 macro_rules! 定义了 macro 之后,进入 textual scope,直到退出外层的 scope。这就类似于通过 let 定义变量一样。如果定义多次,那么老的 macro 会被 shadow 掉。

如代码所示,在 mod.rs 中声明了 m 后,pub mod a,于是在 a 中也能使用 m 了。这是因为这里是 textual scope,a 也在 mod_macro 这个 scope 下面。也就是说 textual scope 可以进入子 mod,甚至穿越多个文件

使用#[macro_use]可以将 mod inner 中的 macro 暴露给外部。#[macro_use]甚至可以从另一个 crate import 指定的或者所有的 macro,如下所示:

1
2
3
4
5
#[macro_use(lazy_static)] // Or #[macro_use] to import all macros.
extern crate lazy_static;

lazy_static!{}
// self::lazy_static!{} // Error: lazy_static is not defined in `self`

#[macro_use]需要和#[macro_export]配合使用。#[macro_export]的作用是将 macro 的声明放到 crate root 中,这样就可以通过 crate::macro_name 来访问。
下面的代码中,helped 是定义在 mod mod_macro 中的,但它被 export 到了 crate root。所以我们可以通过 crate::helped 来访问。

macro 语法

基本定义

查看定义,MacroRule 就是一个被 match 的 pattern,它支持三种括号。

1
2
3
4
5
6
7
8
9
10
MacroRules :
MacroRule ( ; MacroRule )* ;?

MacroRule :
MacroMatcher => MacroTranscriber

MacroMatcher :
( MacroMatch* )
| [ MacroMatch* ]
| { MacroMatch* }

所以可以写如下所示的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
macro_rules! add {
{$a:expr,$b:expr,$c:expr} => {
$a+$b
};
[$a:expr,$b:expr] => {
$a+$b
};
($a:expr) => {
$a
}
}

pub fn main() {
println!("{}", add!{1, 2, 3});
println!("{}", add![1, 2]);
println!("{}", add!(1));
}

重复

如下的代码是两种对列表求和的方案。
在 MacroTranscriber 中有个结构$(+$a)*,表示给列表的每个元素前面都加上一个+

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
macro_rules! add_list {
($($a:expr),*) => {
0
$(+$a)*
}
}

macro_rules! add_list2 {
($a:expr) => {
$a
};
($a:expr,$($b:expr),+) => {
$a
$(+$b)*
};
}

pub fn main() {
println!("{}", add_list2!(1,2,3));
}

TT munchers

TT munchers指的是$($tail:tt)*这样的结构,它永远可以捕获到还没有被 macro 处理的部分。通过该结构可以“递归”调用 macro。
所以可以得到第三种求和方案。

1
2
3
4
5
6
7
8
macro_rules! add_list3 {
($a:expr) => {
$a
};
($a:expr,$($tail:tt)*) => {
$a+add_list3!($($tail)*)
};
}

对 MacroMatch 的详细说明

1
2
3
4
5
MacroMatch :
Tokenexcept $ and delimiters
| MacroMatcher
| $ ( IDENTIFIER_OR_KEYWORD except crate | RAW_IDENTIFIER | _ ) : MacroFragSpec
| $ ( MacroMatch+ ) MacroRepSep? MacroRepOp

MacroRepSep 能取什么呢?定义如下

1
2
MacroRepSep :
Token except delimiters and MacroRepOp

delimiter 是三个括号,Token 基本上啥都可以是了。但我们可以写出($($a:expr)>>*)或者($($a:expr)%*)么?并不能,原因在”Follow-set Ambiguity Restrictions”中有讲到。

metavariable

  1. item
    诸如 mod、extern crate、fn、struct、enum、union、trait、macro 这些结构都是 item
  2. expr
  3. block
  4. pat(Pattern)
  5. path
    类似crate::a::b这样的东西
  6. tt(TokenTree)

unsafe

unsafe 操作

Rust哪些操作是需要unsafe包裹的呢?

  1. *mut T解引用
    注意,取引用是safe的
  2. 访问全局的static对象
  3. 访问union

这也对应了Rust的两个机制,所有权(禁止裸指针)和并发安全。

FFI

常见报错

Pure virtual function called。通常是因为对象提前被析构了,导致虚表也被释放了。常常和 invalid memory reference 交替出现。

panic

1
2
3
macro_rules! panic {
($($arg:tt)*) => { ... };
}

一个 panic 操作会使得当前线程 panic。
诸如 Option 和 Result 的 unwrap 方法,如果结果是 None 或者 Err,则会导致 panic。

对 panic 的处理,可以是直接 abort,也可以是 unwind。通过std::panic::catch_unwind可以捕获 unwind 形式的 panic。在 Rust 1.0 中,panic 只能被父线程捕获,所以如果我们需要捕获 panic,就必须为可能 panic 的代码启动一个新的线程,而 catch_unwind 可以缓解这问题。

catch_unwind 的用法通常是在 FFI 的边界中用来捕获所有的 panic,但我们无法获取和 panic 有关的信息,例如 backtrace。此时可以使用panic::set_hook

Exception Safety

考虑下面的代码,在 unsafe 中,clone 可能 panic。一旦它 panic,因为已经 set_len 了,所以我们可能读到一些未初始化的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
impl<T: Clone> Vec<T> {
fn push_all(&mut self, to_push: &[T]) {
self.reserve(to_push.len());
unsafe {
// can't overflow because we just reserved this
self.set_len(self.len() + to_push.len());

for (i, x) in to_push.iter().enumerate() {
self.ptr().add(i).write(x.clone());
}
}
}
}

测试

cargo test

相比C++的各种测试库,Cargo直接整合了cargo test。测试一般分为两种:

  1. 单测
    一般是某个 mod 下面的 #[cfg(test)] 的 mod。
  2. 集成测试
    一般是单独的 crate,名字叫做 tests。

说到 test feature,有一个坑点。考虑集成测试的情况,我们创建两个 crate:tests 和 raftstore。在集成测试的 tests crate 中开启的 #[cfg(test)],或者 cargo test 自己带上的 test feature,都不会传递到 raftstore 中。如果有需要,得通过自定义一个 testexport 来传递:

  1. 如果 raftstore 需要感知 test 环境,就定义一个 testexport 在自己的 Cargo.toml
  2. tests 的 Cargo.toml 去 enable raftstore/testexport

当然,如果是 raftstore 自己内部的单测,就不需要 testexport 了,所以我们常常看到代码

1
#[cfg(any(test, feature = "testexport"))]

具体进行什么测试,会经过:

package selection

--package表示只测试某个package下面的测试,--workspace测试workspace中的所有测试。
如果不给定任何选项,则会根据--manifest-path。如果在没有给定,则使用当前的工作目录。
如果工作目录是某个workspace的根,则运行所有的default成员的测试。即[default-members]中列出的项目。如果没有列出,对于virtual workspace会运行所有workspace成员的测试;对于非virtual,则只运行root crate的测试。这里其实有点反直觉,按理说virtual workspace一个都不运行比较好。因为比如我哪天将workspace改成了virtual,那么原来的cargo test脚本可能就会运行很多的测试。

target selection

如果没有指定 target selection,则TODO

Demo

线程安全的双向链表

Reference

  1. Rust编程之道 by 张汉东
  2. https://course.rs/
    Rust 语言圣经
  3. https://learnku.com/docs/rust-async-std/translation-notes/7132
    异步rust学习
  4. https://huangjj27.github.io/async-book/01_getting_started/03_state_of_async_rust.html
    同样是异步教程
  5. https://huangjj27.github.io/async-book/02_execution/02_future.html
    对Future实现的讲解
  6. https://kangxiaoning.github.io/post/2021/04/writing-an-os-in-rust-01/
    这个是用Rust写操作系统的教程,这一节讲的是如何移除标准库
  7. https://www.cnblogs.com/praying/p/14179397.html
    future的实现,不关注async相关,包含各种组合子
  8. https://cloud.tencent.com/developer/article/1628311
    对pin的讲解
  9. https://folyd.com/blog/rust-pin-unpin/
    对pin的讲解
  10. https://doc.rust-lang.org/std/pin/
    pin的官方文档
  11. https://www.zhihu.com/question/470049587
    AsRef/Borrow/Deref的讲解
  12. https://dengjianping.github.io/2019/03/05/%E8%B0%88%E4%B8%80%E8%B0%88Fn,-FnMut,-FnOnce%E7%9A%84%E5%8C%BA%E5%88%AB.html
    Fn FnOnce FnMut的区别
  13. https://zhuanlan.zhihu.com/p/341815515
    对闭包的论述
  14. https://medium.com/swlh/understanding-closures-in-rust-21f286ed1759
    对闭包的说明
  15. https://stackoverflow.com/questions/59593989/what-will-happen-in-rust-if-create-mutable-variable-and-mutable-reference-and-ch
    Owner和&mut是否可以同时修改?
  16. https://doc.rust-lang.org/cargo/reference/features.html
    对features的论述
  17. https://danielkeep.github.io/tlborm/book/pat-incremental-tt-munchers.html
    TT munchers