Skip to content

Commit b4e33e8

Browse files
committed
modify ch8
1 parent 7625ca5 commit b4e33e8

24 files changed

+2786
-7
lines changed

projects/doc_attr/Cargo.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[package]
2+
name = "doc_attr"
3+
version = "0.1.0"
4+
edition = "2018"
5+
6+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7+
8+
[dependencies]

projects/doc_attr/src/main.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
macro_rules! make_function {
2+
($name:ident, $value:expr) => {
3+
// 这里使用 concat! 和 stringify! 构建文档注释
4+
#[doc = concat!("The `", stringify!($name), "` example.")]
5+
///
6+
/// # Example
7+
///
8+
/// ```
9+
#[doc = concat!(
10+
"assert_eq!(", module_path!(), "::", stringify!($name), "(), ",
11+
stringify!($value), ");")
12+
]
13+
/// ```
14+
pub fn $name() -> i32 {
15+
$value
16+
}
17+
};
18+
}
19+
20+
21+
make_function! {func_name, 123}
22+
23+
fn main() {
24+
func_name();
25+
}

src/SUMMARY.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,8 @@
237237
- [Rust 技巧篇](./chapter_8/rust-tips.rs)
238238
- [真实世界的设计模式 | 单例模式 与 Sealed](./chapter_8/singleton_and_sealed.md)
239239
- [为 reqwest 增加中间件支持](./chapter_8/reqwest-middleware.md)
240-
- [Rust 编写 GUI 框架?](./chapter_8/rust-gui-framwork.md)
240+
- [想用 Rust 编写 GUI 框架吗?](./chapter_8/gui-framework-ingredients.md)
241+
- [Trait Upcasting 系列 | 如何把子 trait 转成父 trait ?](./chapter_8/what-is-trait-upcasting.md)
241242
- [Trait Upcasting 系列 | Part II](./chapter_8/trait-upcasting-part2.md)
242243
- [GitHub 趋势榜](./chapter_8/github_trending.md)
243244
- [推荐项目 | 基础工具库](./chapter_8/tool_libs.md)

src/chapter_8/ant-futures-compat.md

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,217 @@
11
# 蚂蚁集团 | Trait Object 还是 Virtual Method Table
2+
3+
4+
> Trait object 是 Rust 动态分发的实现方式。在 2021 年 4 月发刊的 Rust Magazine 中,Jiacai Liu 同学在《Trait 使用及实现分析》文章中介绍了 Rust 中 Ad-hoc 多态的使用方式,包括静态分发与动态分发,并且对 trait object 中的对象安全问题以及原因做出了详细解释。
5+
> 那么,使用 trait object 就是 Rust 中动态分发的终点吗?事实上我们发现,在很多 Rust 代码中使用了原始的虚表而不是 trait object,这其中的原因又是什么呢?
6+
> 在本文中,会先简单介绍一下 trait object 与虚表,然后结合笔者挑选出的几个具有代表性的代码片段,讨论手动构造虚表而不使用 trait object 的优缺点。
7+
8+
## 简介
9+
10+
在 Rust 中使用 trait 实现多态有两种方式,静态分发或者动态分发。静态分发使用 trait bound 或者 impl trait 方式实现编译期单态化,根据类型参数生成对应的结构或者函数。动态分发使用 trait object 的方式实现,而由于 trait object 是动态大小类型,无法在编译期确定类型大小,所以一般会使用指向 trait object 的引用或者指针来操作 trait object。而指向 trait object 的引用或者指针本质上是一个胖指针,其中包含了指向擦除了具体类型的对象指针与虚函数表。所以每次调用 trait object 的方法时,需要解引用该胖指针,所以部分观点认为动态分发比静态分发开销更大,而相反的观点认为使用静态分发会导致编译时间变长,编译后二进制文件膨胀以及增加缓存失效概率等问题,所以具体使用哪种方式就见仁见智了。
11+
12+
![trait object](./image/ant/1.jpg)
13+
14+
然而,标准的 `Trait` 结构也有着一些缺陷,比如由于对象安全的要求,一些 trait 无法通过 trait object 的方式使用。所以我们在使用或者阅读一些 Rust crate 的时候会发现,这些库实现了自己的 trait object 结构,比如标准库的 `RawWaker` 结构,`tokio``RawTask` 结构,`bytes``Bytes` 结构,`anyhow``ErrorImpl` 结构,以及关于类型擦除的内存分配器 [^1] 的讨论。在接下来的内容里,我对其中几个实现进行了一些粗浅的分析,并结合一些已有的讨论 [^2],尝试总结它们的共同点。笔者水平有限,如有错漏,烦请指出。
15+
16+
## Examples
17+
18+
<!-- 在这里,我挑选了几个在 Rust 生态系统中广泛使用的 crate,甚至是 Rust 标准库的一些内容。它们的共同点在于,使用了手动实现的虚表来实现动态分发。 -->
19+
20+
### `std` 中的 `RawWaker` {#RawWaker}
21+
22+
Rust 异步编程的核心是 Executor 与 Reactor,其中 Reactor 部分主要是 `Waker` 结构。查看源代码发现 `Waker` 结构仅仅包装了 `RawWaker` 结构。而 `RawWaker` 的结构与指向 trait object 的胖指针十分相似,包含了一个指向任意类型的数据指针 `data` 与自定义此 `Waker` 行为的虚函数指针表 `vtable`。当调用 `Waker` 的相关方法时,实际上会调用虚表中对应的函数,并将 `data` 作为函数的第一个参数传入。
23+
24+
```Rust
25+
/// A `RawWaker` allows the implementor of a task executor to create a [`Waker`]
26+
/// which provides customized wakeup behavior.
27+
///
28+
/// [vtable]: https://en.wikipedia.org/wiki/Virtual_method_table
29+
///
30+
/// It consists of a data pointer and a [virtual function pointer table (vtable)][vtable]
31+
/// that customizes the behavior of the `RawWaker`.
32+
#[derive(PartialEq, Debug)]
33+
#[stable(feature = "futures_api", since = "1.36.0")]
34+
pub struct RawWaker {
35+
/// A data pointer, which can be used to store arbitrary data as required
36+
/// by the executor. This could be e.g. a type-erased pointer to an `Arc`
37+
/// that is associated with the task.
38+
/// The value of this field gets passed to all functions that are part of
39+
/// the vtable as the first parameter.
40+
data: *const (),
41+
/// Virtual function pointer table that customizes the behavior of this waker.
42+
vtable: &'static RawWakerVTable,
43+
}
44+
45+
#[stable(feature = "futures_api", since = "1.36.0")]
46+
#[derive(PartialEq, Copy, Clone, Debug)]
47+
pub struct RawWakerVTable {
48+
clone: unsafe fn(*const ()) -> RawWaker,
49+
wake: unsafe fn(*const ()),
50+
wake_by_ref: unsafe fn(*const ()),
51+
drop: unsafe fn(*const ()),
52+
}
53+
```
54+
55+
在学习这部分代码的时候,我产生了一个疑问,为什么不使用 Rust 提供的 `Trait` 作为 `Waker` 的抽象,而是要手动实现一个类似 trait object 胖指针的复杂,危险且容易出错的 `RawWaker`。为了解开这个疑问,我尝试使用 `Trait` 来模拟 `RawWaker` 的功能。
56+
57+
```Rust
58+
pub trait RawWaker: Send + Sync {
59+
fn clone(&self) -> Box<dyn RawWaker>;
60+
61+
fn wake(&self);
62+
fn wake_by_ref(&self);
63+
}
64+
65+
impl Clone for Box<dyn RawWaker> {
66+
fn clone(&self) -> Self {
67+
RawWaker::clone(&**self)
68+
}
69+
}
70+
71+
pub struct Waker {
72+
waker: Box<dyn RawWaker>,
73+
}
74+
```
75+
76+
根据虚表 `RawWakerVTable` 中要求的方法,我们可以写出一个简单的 `RawWaker` trait。这里遇到了几个问题,首先,`RawWaker` 要求实现 `Clone`,这样做的原因在 Saoirse Shipwreckt [^3] 的博客文章中有过简单的总结:
77+
78+
> 当事件源注册某个 `future` 将等待一个事件时,它必须存储 `Waker`,以便稍后调用 `wake` 方法。为了引入并发,能够同时等待多个事件是非常重要的,因此 `Waker` 不可能由单个事件源唯一拥有,所以 `Waker` 类型需要是可克隆的。
79+
80+
然而,`Clone` trait 本身不是对象安全的,因为它有着 `Sized` supertrait 限定。也就是说,如果我们使用 `pub trait RawWaker: Clone` 的写法,则该 `trait` 将无法作为 trait object 使用。所以在使用 `trait` 模拟的 `RawWaker` 中,我退而求其次的为 `Box<dyn RawWaker>` 实现了 `Clone`,并将具体的细节转移到了 `RawWaker::clone` 内,这样一来,每次调用 `clone` 方法都会构造一个新的 trait object,并且这些 trait object 会共享同一些数据。
81+
82+
其次,为了能够在多线程环境中使用,我要求 `RawWaker` 的 supertrait 为 `Send + Sync`,这样我们可以将其在多个线程间共享或者发送到某个线程中。
83+
84+
最后,为了通过指针使用 trait object,我们需要通过将该对象装箱在堆上。那么我们应该选用哪种智能指针呢?在上面的代码中,我使用了 `Box` 作为具体的指针类型,不使用 `Arc` 的原因是唤醒器中公共数据的共享方式应该由具体的实现决定。比如 `RawWaker` 的实现可以使用引用计数的方式跟踪另一个堆上的对象,或者全部指向静态全局的某个对象,比如:
85+
86+
```Rust
87+
use std::sync::Arc;
88+
89+
#[derive(Debug, Default)]
90+
struct RcWakerInner {}
91+
92+
#[derive(Debug, Default)]
93+
pub struct RcWaker {
94+
inner: Arc<RcWakerInner>,
95+
}
96+
97+
impl RawWaker for RcWaker {
98+
fn clone(&self) -> Box<dyn RawWaker> {
99+
Box::new(RcWaker {
100+
inner: self.inner.clone(),
101+
})
102+
}
103+
104+
fn wake(&self) {
105+
todo!()
106+
}
107+
108+
fn wake_by_ref(&self) {
109+
todo!()
110+
}
111+
}
112+
113+
static GLOBAL_RAW_WAKER: StaticWakerInner = StaticWakerInner {};
114+
115+
#[derive(Debug, Default)]
116+
struct StaticWakerInner {}
117+
118+
#[derive(Debug)]
119+
pub struct StaticWaker {
120+
global_raw_waker: &'static StaticWakerInner,
121+
}
122+
123+
impl Default for StaticWaker {
124+
fn default() -> Self {
125+
Self {
126+
global_raw_waker: &GLOBAL_RAW_WAKER,
127+
}
128+
}
129+
}
130+
131+
impl RawWaker for StaticWaker {
132+
fn clone(&self) -> Box<dyn RawWaker> {
133+
Box::new(StaticWaker {
134+
global_raw_waker: self.global_raw_waker,
135+
})
136+
}
137+
138+
fn wake(&self) {
139+
todo!()
140+
}
141+
142+
fn wake_by_ref(&self) {
143+
todo!()
144+
}
145+
}
146+
```
147+
148+
接下来我们将标准库的 `RawWaker` 与上述实现方式进行一些对比可以发现:
149+
150+
- 由于我们发现 `Box<dyn RawWaker>` 首先经过了一层指针的包装,用来实现 trait object,而具体的 `RawWaker` 实现也很可能会使用指针来共享同一个对象。这样的不仅会在堆内存中占用额外的存储空间,产生许多小对象,也会由于解引用多级指针而带来额外的时间开销。
151+
- 标准库的 `Waker` 位于 `core::task` 模块下,而 `Box``Arc` 等结构都位于 `alloc` 模块下,它们都是 `std` 的子集。在通常的 `std` 应用程序中,我们确实可以使用 `Arc` 等智能指针。但 Rust 不想在 `no_std` 的 futures 上妥协,所以我们必须使用特别的技巧来实现这种能力。
152+
153+
考虑到上面的这些原因,Rust 选择了使用数据指针与虚表的方式来实现高性能的动态分发。`std::task::RawWaker` 中的 `data` 指针既提供了类型擦除的能力,也实现了对象的共享,非常的灵活。
154+
155+
### `tokio``async-task` 中的 `RawTask` {#RawTask}
156+
157+
`tokio``async-task` 的代码中,都将 `RawTask` 作为 `Task` 结构的具体实现。与刚才提到的 `RawWaker` 相似,`RawTask` 也通过虚表提供了类似于 trait object 的功能,然而,它们在内存布局上却有着不同的选择。下面以 `tokio::runtime::raw::RawTask` 为例。
158+
159+
```Rust
160+
#[repr(C)]
161+
pub(crate) struct Header {
162+
pub(super) state: State,
163+
pub(super) owned: UnsafeCell<linked_list::Pointers<Header>>,
164+
pub(super) queue_next: UnsafeCell<Option<NonNull<Header>>>,
165+
166+
/// Table of function pointers for executing actions on the task.
167+
pub(super) vtable: &'static Vtable,
168+
169+
pub(super) owner_id: UnsafeCell<u64>,
170+
#[cfg(all(tokio_unstable, feature = "tracing"))]
171+
pub(super) id: Option<tracing::Id>,
172+
}
173+
174+
/// Raw task handle
175+
pub(super) struct RawTask {
176+
ptr: NonNull<Header>,
177+
}
178+
```
179+
180+
通过与 `RawWaker` 的结构相对比,我们发现 `RawTask` 将虚表部分移动到了数据指针 `ptr` 内。这么做的好处显而易见,`RawTask` 的内存结构更为紧凑,只需要占用一个指针的大小,缺点是多了一层解引用的开销。
181+
所以,通过自定义类似 trait object 胖指针的结构,我们可以控制内存布局,使指针更瘦,或者更胖(比如 `async_task::raw::RawTask`)。
182+
183+
### `bytes` 中的 `Bytes` {#Bytes}
184+
185+
`bytes` crate 提供用于处理字节的抽象,它包含了一种高效的字节缓冲结构与相关的 traits。其中 `bytes::bytes::Bytes` 是用于存储和操作连续内存切片的高效容器,这是通过允许多个 `Bytes` 对象指向相同的底层内存来实现的。`Bytes` 结构充当了接口的功能,本身占用四个 `usize` 的大小,主要包含了内联的 trait object。
186+
187+
```Rust
188+
pub struct Bytes {
189+
ptr: *const u8,
190+
len: usize,
191+
// inlined "trait object"
192+
data: AtomicPtr<()>,
193+
vtable: &'static Vtable,
194+
}
195+
```
196+
197+
它的虚表主要是 `clone` 方法,这允许 `Bytes` 的具体实现来定义具体的克隆或者共享策略。`Bytes` 的文档中举了两个例子
198+
199+
- 对于 `Bytes` 引用常量内存(例如通过 `Bytes::from_static()` 创建)的实现,`clone` 实现将是空操作。
200+
- 对于 `Bytes` 指向引用计数共享存储(例如 `Arc<[u8]>`)的实现,将通过增加引用计数来实现共享。
201+
202+
可以看到,与 `std::task::RawWaker` 相似,`Bytes` 需要使用 `clone` 方法,并且具体的实现完全交给了实现方。如果选择 `Trait` 接口的方式,由于公共的数据部分已经是指针的形式,会引入额外的内存分配与解引用开销,感兴趣的同学可以尝试使用 `Trait` 的方式实现一下这两个例子,最终效果和上文中的 `RawWaker` 类似。而在内联了 trait object 之后,整个设计非常优雅,`data` 部分指向共享的内存,`vtable` 定义了如何进行 `clone`,其余字段作为独占的数据。
203+
204+
## 总结
205+
206+
Rust 提供了安全的抽象以避免产生安全问题或者错误。比如我们使用 `RC` 而不直接管理引用计数,使用 `Box` 而不是 `malloc/free` 直接管理内存分配。同样,`dyn Trait` 隐藏了复杂而又为危险的虚表实现,为我们提供了简单而又安全的动态分发。我们看到,上述手动实现虚表的代码中充斥着大量的 `unsafe`,稍有不慎,就会引入 bug。如果你的设计不能使用标准的 `dyn Trait` 结构来表达,那么你首先应该尝试重构你的程序,并参考以下理由来决定是否使用自定义的虚表。
207+
208+
- 你想要为一类指针对象实现多态,并且无法忍受多级指针解引用造成的性能开销,参考 [RawWaker](#RawWaker)[Bytes](#Bytes)
209+
- 你想要自定义内存布局,比如像 C++ 中虚表一样紧凑的内存结构(虚表指针位于对象内),参考 [RawTask](#RawTask)
210+
- 你的 crate 需要在 `no_std` 环境中使用动态分发,参考 [RawWaker](#RawWaker)
211+
- 或者,标准的 trait object 确实无法实现你的需求。
212+
213+
## 相关链接
214+
215+
[^1]: https://github.com/rust-lang/wg-allocators/issues/33
216+
[^2]: https://users.rust-lang.org/t/dyn-trait-vs-data-vtable/36127/3
217+
[^3]: https://boats.gitlab.io/blog/post/wakers-i/

0 commit comments

Comments
 (0)