正式开始
为什么要阅读源码?
- 没有足够积累,很容易养成 StackOverflow driven 的写代码习惯
- 平时基础不牢靠,靠边写边学的进步是最慢的
- 周围能触达的那个最强工程师开发水平的上限,就是我们的上限
阅读源码的功用
- 知识的源头在你这里,你可以根据事实来分辨是非,而不是迷信权威
- 看别人的代码,积累了素材,开拓了思路,自己写代码时可以“文思如泉涌,下笔如有神”
- 可以打破天花板。累积素材是基础,被启发出来的思路将这些素材串成线,才形成了自己的知识
如何阅读源码呢?
- 从 crate 的大纲开始,先了解目标代码能干什么、怎么用
- 学习核心 trait,看看它支持哪些功能
- 掌握主要的数据结构,开始写一些示例代码
- 围绕自己感兴趣的情景深入阅读 以Bytes为例进行讲解
step1:从大纲开始
从 crate 的大纲开始,先了解目标代码能干什么、怎么用
- 阅读 crate 的文档,快速了解这个 crate 是做什么的
- 还可以看一下源码根目录下的 README.md
顺序一般是:trait → struct → 函数 / 方法
和写代码的思考方式相似
- 先从需求的流程中敲定系统的行为,需要定义什么接口 trait;
- 再考虑系统有什么状态,定义了哪些数据结构 struct;
- 最后到实现细节,包括如何为数据结构实现 trait、数据结构自身有什么算法、如何把整个流程串起来等等。
step2:熟悉核心 trait 的行为
学习核心 trait,看看它支持哪些功能
-
Required Methods:实现这个 trait 需要实现的方法
-
Provided Methods:缺省方法实现
-
Implementations on Foreign Types:为哪些 “foreign types” 实现了 trait
a. 切片 &[u8]、VecDeque 都实现了 Buf trait;
b. 如果 T 满足 Buf trait,那么 &mut T、Box 也实现了 Buf trait;
c. 如果 T 实现了 AsRef<[u8]>,那 Cursor 也实现了 Buf trait。
-
Implementors:当前模块有哪些 implementors
高手定义trait的一些思路
- 定义好 trait 后,可以考虑一下标准库的数据结构,哪些可以实现这个 trait
- 如果未来别人的某个类型 T ,实现了你的 trait,那他的 &T、&mut T、Box 等衍生类型,是否能够自动实现这个 trait
step3:掌握主要的 struct
掌握主要的数据结构,开始写一些示例代码
- 了解了数据结构的基本介绍后,继续看看它的内部结构
/// ```text
///
/// Arc ptrs +---------+
/// ________________________ / | Bytes 2 |
/// / +---------+
/// / +-----------+ | |
/// |_________/ | Bytes 1 | | |
/// | +-----------+ | |
/// | | | ___/ data | tail
/// | data | tail |/ |
/// v v v v
/// +-----+---------------------------------+-----+
/// | Arc | | | | |
/// +-----+---------------------------------+-----+
/// ```
pub struct Bytes {
ptr: *const u8,
len: usize,
// inlined "trait object"
data: AtomicPtr<()>,
vtable: &'static Vtable,
}
pub(crate) struct Vtable {
/// fn(data, ptr, len)
pub clone: unsafe fn(&AtomicPtr<()>, *const u8, usize) -> Bytes,
/// fn(data, ptr, len)
pub drop: unsafe fn(&mut AtomicPtr<()>, *const u8, usize),
}
- 它内部使用了裸指针和长度,模拟一个切片,指向内存中的一片连续地址;
- 使用了 AtomicPtr 和手工打造的 Vtable 来模拟了 trait object 的行为
- 看 Vtable 的样子,大概可以推断出 Bytes 的 clone() 和 drop() 的行为是动态的
我们自己的数据结构,也应该尽可能实现需要的标准 trait,包括但不限于:AsRef、Borrow、Clone、Debug、Default、Deref、Drop、PartialEq/Eq、From、Hash、IntoIterator(如果是个集合类型)、PartialOrd/Ord 等
- 如果你的数据结构里使用了不支持 Send / Sync 的类型,编译器默认这个数据结构不能跨线程安全使用,不会自动添加 Send / Sync trait 的实现。
- 但如果你能确保跨线程的安全性,可以手工通过 unsafe impl 实现它们
step4:深入研究实现逻辑
围绕自己感兴趣的情景深入阅读 推荐 “主题阅读”或者说“情境阅读”,就是围绕着一个特定的使用场景,以这个场景的主流程为脉络,搞明白实现原理
以 Bytes 如何实现自己的 vtable 为例,深入看 Bytes 是如何 clone 的?
impl Clone for Bytes {
#[inline]
fn clone(&self) -> Bytes {
// 它用了 vtable 的 clone 方法,传入了 data ,指向数据的指针以及长度
unsafe { (self.vtable.clone)(&self.data, self.ptr, self.len) }
}
}
包括四种表: STATIC_VTABLE、PROMOTABLE_EVEN_VTABLE、PROMOTABLE_ODD_VTABLE 和 SHARED_VTABLE 这四张表
- 后三张表是处理动态数据的,在使用时如果 Bytes 的来源是 Vec、Box<[u8]> 或者 String,它们统统被转换成 Box<[u8]>,并在第一次 clone() 时,生成类似 Arc 的 Shared 结构,维护引用计数
- 由于 Bytes 的 ptr 指向这个 Bytes 的起始地址,而 data 指向引用计数的地址,所以,你可以在这段内存上,生成任意多的、大小不同、起始位置不一样的 Bytes 结构,它们都用同一个引用计数
小结
注意阅读的顺序:
- 从大纲开始,先了解目标代码能干什么,怎么用;
- 然后学习它的主要 trait;
- 之后是数据结构,搞明白后再看看示例代码(examples)或者集成测试(tests),自己写一些示例代码;
- 最后,围绕着自己感兴趣的情景深入阅读
链接
- 高效网络数据处理库 Bytes
- Http库 hyper
- 解析器组合框架 nom
- Rust异步库 tokio
- gRPC 框架
- Rust docs系统
- crate.io中的DataStructure
- Rust文档地址
精选问答
- 在阅读 Bytes 的 clone() 场景时,对于 PROMOTABLE_EVEN_VTABLE、PROMOTABLE_ODD_VTABLE 这两张表比较迷惑,不明白为什么会根据 ptr & 0x1 是否等于 0 来提供不同的 vtable
impl From<Box<[u8]>> for Bytes {
fn from(slice: Box<[u8]>) -> Bytes {
// Box<[u8]> doesn't contain a heap allocation for empty slices,
// so the pointer isn't aligned enough for the KIND_VEC stashing to
// work.
if slice.is_empty() {
return Bytes::new();
}
let len = slice.len();
let ptr = Box::into_raw(slice) as *mut u8;
if ptr as usize & 0x1 == 0 {
let data = ptr as usize | KIND_VEC;
Bytes {
ptr,
len,
data: AtomicPtr::new(data as *mut _),
vtable: &PROMOTABLE_EVEN_VTABLE,
}
} else {
Bytes {
ptr,
len,
data: AtomicPtr::new(ptr as *mut _),
vtable: &PROMOTABLE_ODD_VTABLE,
}
}
}
}
- Box<[u8]> 是 1 字节对齐,所以 Box<[u8]> 指向的堆地址可能末尾是 0 或者 1
- data 这个 AtomicPtr 指针,在指向 Shared 结构时,这个结构的对齐是 2/4/8 字节(16/32/64 位 CPU 下),末尾一定为 0
struct Shared {
// holds vec for drop, but otherwise doesnt access it
_vec: Vec<u8>,
ref_cnt: AtomicUsize,
}
这里用了一个小技巧,以 data 指针末尾是否为 0x1 来区别,当前的 Bytes 是升级成共享,类似于 Arc 的结构(KIND_ARC),还是依旧停留在非共享的,类似 Vec 的结构(KIND_VEC)