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

crate 内部组织形式

  1. mod.rs
  2. 一个 common.rs 用作公共依赖,mod.rs 注明只对该 mod 服务
  3. 一个或多个 xxx_engine.rs 用来定义 mod 对外暴露的主要功能
    在 mod 中 pub mod 和 pub use。
    不 pub use。
  4. 一个或多个 yyy_impls.rs,用来辅助 xxx_engine.rs 的实现
    可能需要 use xxx_engine.rs 中的一些东西。
    反过来,xxx_engine.rs 中会 use yyy_impls::*
    不 pub use。

编译与链接

调试信息

可以通过 -C debug_info 来指定调试信息的等级,其中0(false)、1、2(true) 分别对应无/行信息以及全部信息。如果设置为0,那么很可能部分代码的行号是打印不出来的。
另外,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"]

feature 的传递

假设一个 Cargo 工程的 default-member 是 x,其中有个 feature f。如果使用 edition 2018,那么指定 –features f 是不行的,需要在 workspace 下面传递一下 feature f,写成 f = [“x/f”] 才行

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();

异常和错误处理

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. ?

常见组合子

  1. filter_map
    F 如果返回 None,则跳过处理这个元素

panic

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

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

Panic 和 thread

1
2
3
4
5
let thread_join_handle = std::thread::spawn(move || {
panic!("Pan");
});
let res = thread_join_handle.join();
assert!(res.is_err());

可以看到,如果线程 panic 了,那么 join 会得到一个 Err。

1
2
3
4
5
6
7
8
9
thread '<unnamed>' panicked at 'Pan', src\mod_thread\mod.rs:4:9
stack backtrace:
0: std::panicking::begin_panic
at /rustc/f83e0266cf7aaa4b41505c49a5fd9c2363166522\library\std\src/panicking.rs:588:12
1: learn::mod_thread::test_join_panic::{{closure}}
at .\src\mod_thread\mod.rs:4:9
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

Process finished with exit code 0

捕获 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

另外一种捕获 panic 的方法,是 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());
}
}
}
}

double panic

避免 double panic,可以使用 safe_panic。double panic 的情况可能出现在:

  1. 如果在某个线程中已经 panic 了,并且后续在某个对象的 Drop 中也会有 panic 逻辑,那么可能 panic while panicking
    例如这里Drop->stop->cancel_applying_snap->check_applying_snapcheck_snap_status->check_applying_snap

指针和智能指针

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
  9. Cell
  10. NotNull

raw pointer

Rust book 中介绍了一些 raw pointer 相对于其它 rust 指针的特点:

  1. 它们不保证能够指向有效内存。它们有可能是 null。
  2. 它们不会像 Box 一样会自动 gc,需要手动内存管理。
    Box::from_raw 可以用来获得 raw pointer 的所有权,并进行自动的内存管理。
    也可以通过 ptr::drop_in_place 来销毁。它可以处理 trait object 的情况,因为这个时候我们无法通过 ptr::read 去读出这个对象,然后销毁。drop_in_place 是一个编译器内置的实现。
  3. 它们是 POD,也就是说它们不具备 ownership。不像 Box,Rust 编译器不会检查诸如 use-after-free 的场景。
  4. 不同于 &,raw pointer 不具备任何 lifetime,所以编译器不能推断出 dangling pointer。
  5. no guarantees about aliasing or mutability other than mutation not being allowed directly through a *const T

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

内部可变性(interior mutability)引用

总所周知,Rust 要求一个对象可以:

  1. 有多个不可变引用(aliasing)
  2. 有一个可变引用(mutability)

通过 Cell 和 RefCell 可以允许“多个可变引用”。其主要做法是可以通过&来 mutate 对象。

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);

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);
}

Cell

不同于 RefCell,Cell 实现可变性的办法是将持有的对象移出或者移进。所以它不是操作对象的可变引用 &mut,而是操作对象本身。这也意味着我们只能动态

  1. 对于实现了 Copy 的类型,get 方法可以获取当前值。
  2. 对于实现了 Default 的类型,take 可以取出当前值,并用 Default::default() 代替。
  3. 对于所有的类型
    1. replace 可以替换并返回旧值。
    2. into_inner 可以消费 Cell,并返回内部值。
    3. set 类似于 replace,但直接 drop 旧值。

Mutex 可以和 RefCell/Cell 联用么?

Mutex 自带了内部可变性。完全可以理解,如果一个函数不可变,那么为何需要用 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;

同时,RefCell 和 Cell 都没有实现 Sync,也就是说它们不是线程安全的。

GhostCell

GhostCell

Pin

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

1
2
3
4
5
// Note we don't use &TestNUnpin
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++ 中也会有自引用结构,并且也会造成相同的问题。可以参考付老板的文章

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct X {
int data;
int & ref;
explicit X(int v) : data(v), ref(data) {}
};

void test() {
std::vector<X> vec;
for (int i = 0; i < 10; ++i) {
vec.emplace_back(i);
}
for (const auto & x: vec) {
std::cout << x.ref << std::endl; // boom!
}
}

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 了出来。然后我们将 EvilNUnpin 作为一个 String 的指针传进去。结果打印出来已经有问题了。解决方案也很简单,如果想要 T 不被移动,那么始终 pin 住 &mut T 就行。

此外,还需要保证这个 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
17
18
fn test_new_unchecked() {
// We can even swap !Unpin objects, with Pin::new_unchecked
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);
assert_eq!(n1.b, "2");
assert_eq!(n2.b, "1");
}
}

对 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
10
fn test_shadow() {
// How pin_mut! takes effect.
let mut x = TestNUnpin { b: "b".to_owned() };
let mut xp = unsafe { Pin::new_unchecked(&mut x) };
drop(xp);
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 获得内部可变性么?这其实不安全。

NonNull

生命周期

生命周期(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

生命周期的协变和逆变

'a'b的子类,这个在生命周期注解中的含义是'a'b长,也就是说子类的生命周期大于等于父类。

1
'a : 'b

根据里氏替换原则,可以得到定义:

  1. 协变
    子类可以替换父类,也就是生命周期短的参数可以接受生命周期长的参数。大部分指针都是协变或者不变的。
  2. 逆变

讨论变性:

  1. &'a T
    'a协变,对 T 协变
  2. &'a mut T
    'a协变,对 T 不变
  3. fn(T) -> U
    对 T 逆变,也就是它可以接受生命周期更短的参数。
    对 U 协变。

讨论上面几个问题。
为什么 &'a mut T 对 T 不变?不妨考虑 T 分别是 &'static str&'a str 的情况,显然前者是后者的子类。那么如果 T 协变,则前者就能代替后者。现在我们考虑下面的 overwrite 函数。如果协变成立,就可以事实上将 string 这个生命周期更短的赋值给 forever_str 这个生命周期更长的了。

1
2
3
4
5
6
7
8
9
10
11
12
13
fn overwrite<T: Copy>(input: &mut T, new: &mut T) {
*input = *new;
}

fn test_varaint_mut() {
let mut forever_str: &'static str = "hello";
{
let string = String::from("world");
// *string is str
// &*string is &'a str
overwrite(&mut forever_str, &mut &*string);
}
}

闭包和函数

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 的,另一些则不可以:

  1. Rc<T> 只能在同一个线程内部使用,它就不能被实现为 Send 的。
  2. 裸指针也不是 Send 的。

由 Send 类型组成的新类型也是 Send 的。

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

切片(Slice)的方法

Sized、!Sized、?Sized和DST

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

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

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

胖指针

胖指针指的是指向 DST 的引用或者指针
一个 Slice 是 DST,那么指向 Slice 的指针就是胖指针

1
2
3
4
5
6
7
assert_eq!(size_of::<u32>(), 4);
assert_eq!(size_of::<usize>(), 8);
assert_eq!(size_of::<[u32; 2]>(), 8);
assert_eq!(size_of::<&u32>(), 8);
assert_eq!(size_of::<&[u32; 2]>(), 8);
assert_eq!(size_of::<&[u32]>(), 16);
assert_eq!(size_of::<&mut[u32]>(), 16);

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

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

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

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

1
2
assert_eq!(size_of::<*mut[u32]>(), 16);
assert_eq!(size_of::<*const[u32]>(), 16);

除了 Slice,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) // 1

assert(&ZST() != &ZST())

在 Rust 中

1
2
struct A;
println!("{}", std::mem::size_of::<A>());

在 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)

注意,孤儿规则是对 trait 而言的。而 impl 和 struct 就不能出现在不同的 crate 中,否则会报错 “cannot define inherent impl for a type outside of the crate where the type is defined”。

泛型约束

例如我们实现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 的常见问题

在 impl 中调用 trait 的默认实现

实现 dynamic cast

简单来说就是 PSElementWriteBatch 实现了 trait ElementWriteBatch。一个 PSElementWriteBatch 知道怎么 merge 一个 PSElementWriteBatch,但一个 PSElementWriteBatch 似乎很难知道如何 merge 一个 Box<dyn ElementWriteBatch>,除非 Box<dyn ElementWriteBatch> 提供一个 as_any(),然后在 merge 里面把这个 Any 手动 downcast 成 PSElementWriteBatch。

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 中包含了对象的析构函数、大小、对齐、方法(也就是虚函数指针)等信息。

Rust 中有个常见的 E0035 错误,和 trait 的对象安全有关。具体来说,只有对象安全的 trait 才可以作为 trait object 来使用,否则只能作为一般的 trait 来使用。
具体指下面几点:

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

E0038 错误中,列出了一些破坏上述原则的情况。
比如 T 中定义了带有 type parameter 的接口函数 cb,那么 T 就不能以 trait object 的形式使用。这是因为 trait object T 的虚表无法穷尽 F 取不同类型的值的时候的情况。

1
2
3
4
5
6
7
8
pub trait T {
fn get_value_cf<F>(&self, cf: &str, key: &[u8], cb: F)
where
F: FnOnce(Result<Option<&[u8]>, String>),
}

// Error like the following
// for a trait to be "object safe" it needs to allow building a vtable to allow the call to be resolvable dynamically

现在只能将 cb 改为动态分发形式

1
2
fn get_value_cf(&self, cf: &str, key: &[u8], cb: Box<dyn FnOnce(Result<Option<&[u8]>, String>)>)
fn get_value_cf(&self, cf: &str, key: &[u8], cb: &mut dyn FnOnce(Result<Option<&[u8]>, String>));

但这样会出现另一个问题。一个 S: impl T 可以通过 &S 的方式传给 &dyn T 的参数。但反过来,一个 S: Box<dyn T> 就没法传给 impl T 的参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
trait T {}

struct S {}

impl T for S {}

fn call_x(t: impl T) {}

fn call_y(t: &dyn T) {}

fn main() {
let s = S {};
let t : &dyn T = &s;
call_x(t);
call_y(&s);
}

在实践中,还观察到出现有 “Cast requires that variable is borrowed for ‘static” 这样的报错。在 demo 中如果删掉 impl RaftStoreProxyimpl RaftStoreProxyEngineTrait for RaftStoreProxyEngine 里面的 + '_,就能复现报错。解决方案就是加上 + '_。原因是 dyn Trait 会被默认为是 dyn Trait + 'static。我们需要显式指定另一个生命周期。

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 交替出现。

Fat pointer 和 FFI

Rust 中的 Fat pointer 包含以下几类,在传给 FFI 接口后,会丢失除了地址之外的信息:

  1. 切片
    切片中包含了长度信息,但传递给 FFI 只会保留起始地址,而丢失长度信息。
  2. trait object
    这个在下面会专门讲到

trait object 和 FFI

在处理 Fat pointer 和 FFI 的过程中,trait object 非常头疼。
考虑下面的场景,RaftStoreProxyPtr 位于 FFI 边界,负责向 C++ 端传递上下文 RaftStoreProxy。C++ 端持有 RaftStoreProxyFFIHelper 对象,并通过里面的 fn_handle_get_proxy_status 接口来调用 Rust。在调用时会传入 RaftStoreProxyPtr 作为上下文。Rust 端在收到调用时,将传入的 RaftStoreProxyPtr 从指针转回为 RaftStoreProxy,并调用 RaftStoreProxy 中的方法。

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
37
38
39
40
41
42
43
44
45
46
47
pub trait RaftStoreProxyFFI: Sync {
...
}

struct RaftStoreProxy {
...
}

impl RaftStoreProxyFFI for RaftStoreProxy {
...
}

#[repr(C)]
#[derive(Debug)]
pub struct RaftStoreProxyPtr {
pub inner: *const ::std::os::raw::c_void,
}

impl RaftStoreProxyPtr {
pub unsafe fn as_ref(&self) -> &RaftStoreProxy {
&*(self.inner as *const RaftStoreProxy)
}
...
}

#[repr(C)]
#[derive(Debug)]
pub struct RaftStoreProxyFFIHelper {
pub proxy_ptr: RaftStoreProxyPtr,
pub fn_handle_get_proxy_status: ::std::option::Option<
unsafe extern "C" fn(
arg1: RaftStoreProxyPtr,
) -> RaftProxyStatus,
>,
...
}

impl RaftStoreProxyFFIHelper {
// forward fn_handle_get_proxy_status to ffi_handle_get_proxy_status
}

pub extern "C" fn ffi_handle_get_proxy_status(proxy_ptr: RaftStoreProxyPtr) -> RaftProxyStatus {
unsafe {
let r = proxy_ptr.as_ref().status().load(Ordering::SeqCst);
std::mem::transmute(r)
}
}

现在我们想通过 RaftStoreProxyPtr 调用 RaftStoreProxyFFI 的方法,然后由 RaftStoreProxyFFI 动态 dispatch 到 RaftStoreProxy 上。这样的目的是因为 RaftStoreProxy 里面涉及了很多细节实现,我们想解耦掉,提出一个较为干净的 FFI 层。但很快遇到了问题:

  1. 没法为 RaftStoreProxyPtr 实现到 RaftStoreProxyFFI 的解引用
    显然,这是因为没办法从 *const c_void 转成 trait object。

    1
    2
    3
    error[E0277]: the trait bound `c_void: RaftStoreProxyFFI` is not satisfied
    the trait `RaftStoreProxyFFI` is implemented for `RaftStoreProxy`
    required for the cast from `c_void` to the object type `dyn RaftStoreProxyFFI`
  2. 没法为 RaftStoreProxyPtr 直接实现 RaftStoreProxyFFI
    这相当于是为 *const c_void 实现一些 trait。不论编译器的报错,单从这个指针我们都无法获得任何的可供后续转发的上下文。

  3. 也许可以尝试将 RaftStoreProxyPtr 直接定义为 TraitObject,可是它并不是 FFI 安全的。有一些比较 hack 的做法,比如可以再定义一个 FFI 安全的平行于 trait object 的对象。

最后的做法是避免在 FFI 边界使用 trait object,而是用一个简单的对象,如 RaftStoreProxy。然后让 RaftStoreProxy 持有一个 trait object 即 RaftStoreProxyEngineTrait。

异常

C++ 中异常不能穿透语言边界,并且会产生一个 SIGABRT。
在 v1.cpp 中有

1
2
3
4
5
6
7
8
#include <exception>
#include <stdexcept>

extern "C" {
void c_func() {
throw std::runtime_error("error!!!");
}
}

在 Rust 的 main.rs 中有

1
2
3
4
5
6
7
8
9
10
#[link(name = "v1", kind = "dylib")]
extern {
fn c_func();
}

fn main() {
unsafe {
c_func();
}
}

经过测试发现,C++ 异常在语言边界会产生如下的错误

1
fatal runtime error: Rust cannot catch foreign exceptions

Layout

FFI 中保持 C++ 和 Rust 端 struct 的 Layout 一致是重要的。我们的实践是使用一个代码生成工具,而不是手动管理。
在下面的代码中并没有使用代码生成工具,结果造成 SSTReaderPtr 和 ProxyFFI.h 中的声明不符合。这导致 C++ 中使用的 struct 比 Rust 中多一个字段。实际我们观察到的现象是在 x86 架构下代码能够照常运行,但是在 arm 架构下就有问题。在 C++ 端传的参数是对的,但是到了 Rust 端解析出来就错了。这里的原因可能是 x86 编译会带上一些 padding,导致错误被掩盖了。

FFI 和库

考虑 Rust 动态库会链接到 C++ 上,但他们可能“菱形依赖”同一个库。一般来说,方案是:

  1. Rust 动态库 rename 掉自己的依赖,或者静态打包成 local
  2. Rust 动态库动态加载这些库

但对于诸如 jemalloc 这些库,只能用第二种办法。

测试

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

cargo test 打印

1
test_snap_append_restart-0 2022/11/15 16:02:38.526 thread.rs:396: [DEBG] tid 42023 thread name is region-worker::test_snap_append_restart-0

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