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方法地址 |