trait object
rust中支持了静态派发(static dispatch)和动态派发(dynamic dispatch)两种方式
静态派发指的是具体调的函数,在编译阶段就能确定下来,静态派发通过泛型来完成。对于不同的泛型类型参数,编译器通过单态化(monomorphization)生成不同的函数实现,在编译阶段就确定了应该调用哪个函数。动态派发指的是具体调用哪个函数,在执行阶段才能确定。它通过 trait object 来完成。trait object类似golang中的interface,本质上是个胖指针,它可以指向不同的类型,从而动态地选择调用的方法
下面这张图展示了trait object在rust内部是如何表示的,图片来自这里

trait object的底层其实就是一个由两个word组成的fat pointer。其中,一个指针指向数据本身,另一个则指向虚函数表(vtable)。vtable 是一张静态表,rust 在编译时会给使用了 trait object 的类型的 trait 实现生成一张表,放在可执行文件中(一般位于 RODATA 段)
vtable中所包含的信息有:
- destructor:当trait object被释放,用来释放其使用的所有资源
- size:类型的大小
- align:类型对齐方式
- methods:trait中方法在具体类型中的实现
lldb调试
待调试的代码如下
| 1 | trait Trait { | 
执行lldb ./target/debug/lldb 进入调试
| 1 | (lldb) target create "./target/debug/deps/lldb-2bf0ba3510ca81ff" | 
在第12行设置了断点之后可以用run命令启动程序,启动后它将在断点处停止
| 1 | (lldb) run | 
| 1 | (lldb) p t | 
如之前所说,trait object由一个指向实际数据的point和vtable组成。继续验证pointer的内容:
| 1 | (lldb) x/g t.pointer | 
可以看到这里打印的100正是在代码let t: &dyn Trait = &100中创建trait object时对应的值
接着对vtable进行验证:
| 1 | (lldb) x/4ag t.vtable | 
这四个地址中的内容分别表示:
| 内容 | 表示 | 
|---|---|
| 0x0000000100001640 | i32 drop方法地址 | 
| 0x0000000000000004 | i32大小,为4 | 
| 0x0000000000000004 | i32的对齐,为4 | 
| 0x00000001000014a0 | i32实现的foo方法地址 |