20|4 Steps :如何更好地阅读Rust源码?

477 阅读5分钟

正式开始

为什么要阅读源码?

  1. 没有足够积累,很容易养成 StackOverflow driven 的写代码习惯
  2. 平时基础不牢靠,靠边写边学的进步是最慢的
  3. 周围能触达的那个最强工程师开发水平的上限,就是我们的上限

阅读源码的功用

  1. 知识的源头在你这里,你可以根据事实来分辨是非,而不是迷信权威
  2. 看别人的代码,积累了素材,开拓了思路,自己写代码时可以“文思如泉涌,下笔如有神”
  3. 可以打破天花板。累积素材是基础,被启发出来的思路将这些素材串成线,才形成了自己的知识

如何阅读源码呢?

  1. 从 crate 的大纲开始,先了解目标代码能干什么、怎么用
  2. 学习核心 trait,看看它支持哪些功能
  3. 掌握主要的数据结构,开始写一些示例代码
  4. 围绕自己感兴趣的情景深入阅读 以Bytes为例进行讲解

step1:从大纲开始

从 crate 的大纲开始,先了解目标代码能干什么、怎么用

  1. 阅读 crate 的文档,快速了解这个 crate 是做什么的
  2. 还可以看一下源码根目录下的 README.md

顺序一般是:trait → struct → 函数 / 方法

和写代码的思考方式相似

  1. 先从需求的流程中敲定系统的行为,需要定义什么接口 trait;
  2. 再考虑系统有什么状态,定义了哪些数据结构 struct;
  3. 最后到实现细节,包括如何为数据结构实现 trait、数据结构自身有什么算法、如何把整个流程串起来等等。

step2:熟悉核心 trait 的行为

学习核心 trait,看看它支持哪些功能

  1. Required Methods:实现这个 trait 需要实现的方法

  2. Provided Methods:缺省方法实现

  3. 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。

  4. Implementors:当前模块有哪些 implementors

高手定义trait的一些思路

  1. 定义好 trait 后,可以考虑一下标准库的数据结构,哪些可以实现这个 trait
  2. 如果未来别人的某个类型 T ,实现了你的 trait,那他的 &T、&mut T、Box 等衍生类型,是否能够自动实现这个 trait

step3:掌握主要的 struct

掌握主要的数据结构,开始写一些示例代码

  1. 了解了数据结构的基本介绍后,继续看看它的内部结构

/// ```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),
}
  1. 它内部使用了裸指针和长度,模拟一个切片,指向内存中的一片连续地址;
  2. 使用了 AtomicPtr 和手工打造的 Vtable 来模拟了 trait object 的行为
  3. 看 Vtable 的样子,大概可以推断出 Bytes 的 clone() 和 drop() 的行为是动态的

我们自己的数据结构,也应该尽可能实现需要的标准 trait,包括但不限于:AsRef、Borrow、Clone、Debug、Default、Deref、Drop、PartialEq/Eq、From、Hash、IntoIterator(如果是个集合类型)、PartialOrd/Ord 等

  1. 如果你的数据结构里使用了不支持 Send / Sync 的类型,编译器默认这个数据结构不能跨线程安全使用,不会自动添加 Send / Sync trait 的实现。
  2. 但如果你能确保跨线程的安全性,可以手工通过 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 这四张表

  1. 后三张表是处理动态数据的,在使用时如果 Bytes 的来源是 Vec、Box<[u8]> 或者 String,它们统统被转换成 Box<[u8]>,并在第一次 clone() 时,生成类似 Arc 的 Shared 结构,维护引用计数
  2. 由于 Bytes 的 ptr 指向这个 Bytes 的起始地址,而 data 指向引用计数的地址,所以,你可以在这段内存上,生成任意多的、大小不同、起始位置不一样的 Bytes 结构,它们都用同一个引用计数

image.png

小结

注意阅读的顺序:

  1. 从大纲开始,先了解目标代码能干什么,怎么用;
  2. 然后学习它的主要 trait;
  3. 之后是数据结构,搞明白后再看看示例代码(examples)或者集成测试(tests),自己写一些示例代码;
  4. 最后,围绕着自己感兴趣的情景深入阅读

链接

  1. 高效网络数据处理库 Bytes
  2. Http库 hyper
  3. 解析器组合框架 nom
  4. Rust异步库 tokio
  5. gRPC 框架
  6. Rust docs系统
  7. crate.io中的DataStructure
  8. Rust文档地址

精选问答

  1. 在阅读 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,
            }
        }
    }
}
  1. Box<[u8]> 是 1 字节对齐,所以 Box<[u8]> 指向的堆地址可能末尾是 0 或者 1
  2. 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)