Rust特征对象
引入
在看了《Rust圣经》Box堆对象分配 - Rust语言圣经(Rust Course)这一章后。
想对之前学到的 Box<dyn trait>这种表示特征对象的方式进行深入的探究。
我们知道,特征对象可以有各种类型,他们之间唯一的共同点是实现了同样的方法,因此特征对象是一种 DST
要同时使用一个变量访问这些 DST,就必须创建一个特别的引用,这就是特征对象。
但是你有没有好奇过到底是怎么实现的?
下面我们将会通过
rustfn print_data_of_t<T>(pt: &T) {
// 将a地址转换为*const String类型
let address_of_t: *const T = pt as *const _;
// 获取a占用的内存大小
let size_of_t: usize = std::mem::size_of_val(pt);
// 直接去a的地址看看a到底装的什么
let bytes: &[u8] = unsafe { std::slice::from_raw_parts(address_of_t as *const u8, size_of_t) };
// 打印bytes数组,16进制
for b in bytes {
print!("{:02x} ", b);
}
println!();
}
这个可以直接打印变量在内存中的值的函数对特征对象进行探究。
实验
代码示例
首先我使用的示例代码同样来自于《Rust圣经》。如下
rusttrait Draw {
fn draw(&self);
}
struct Button {
id: u32,
}
impl Draw for Button {
fn draw(&self) {
println!("这是屏幕上第{}号按钮", self.id)
}
}
struct Select {
id: u32,
}
impl Draw for Select {
fn draw(&self) {
println!("这个选择框贼难用{}", self.id)
}
}
有一个特征 Draw和两个实现了这个特征的结构体 Button和 Select
观察现象
首先我们先不使用特征对象,先看一下直接创建出来的结构体的引用是什么有样子的。
rustfn main() {
let drawable_one: Box<Button> = Box::new(Button { id: 1 });
drawable_one.draw();
print_data_of_t(&drawable_one);
}
plaintext这是屏幕上第1号按钮
10 0b 6b 83 12 56 00 00
可以看到可以正常使用结构体的方法。
然后我们也看到了 drawable_one这个引用的值为 10 0b 6b 83 12 56 00 00
这看起来是个指向堆上的地址。经过简单的推理,可以知道这个就是在堆栈上存储的Button对象的地址。
这非常合理。
但是倘若我们使用 Box <dyn Draw>特征对象来指向 Button对象呢?
rustfn main() {
let drawable_one: Box<dyn Draw> = Box::new(Button { id: 1 });
drawable_one.draw();
print_data_of_t(&drawable_one);
}
plaintext这是屏幕上第1号按钮
10 fb 17 01 23 56 00 00 68 45 40 f4 22 56 00 00
同样可以进行方法调用。
但是发现,此时 drawable_one这个引用这次在内存中占用了16个字节,并且似乎是两个指向堆上数据的地址。
我们知道一个地址是指向 Button对象的,那么另一个呢?
我们先来推理一下,一个特征对象可能是好几种不同的类型,因此它们各自实现的特征方法可能在结构体中的不同位置,那么怎样才能通过同样的引用调用他们都实现了的特征方法呢?
答案已经呼之欲出了,就是在引用中添加方法的地址。
揭秘
那真的是这样吗,我们验证一下。
//由于接下来的操作涉及到直接操作指针和内存,因此直接用 unsafe包裹
首先打印 Button中 draw方法的地址,再访问 drawable_one的内容
rustfn main() {
unsafe {
// 获取Button的draw方法的地址
let p_draw_fn: *const u64 = Button::draw as *const u64;
println!("Button::draw的地址是:{:p}", p_draw_fn);
// 创建一个Draw特征对象
let drawable_one: Box<dyn Draw> = Box::new(Button { id: 2 });
// 看一下drawable_one里面有什么
print!("drawable_one的内容是:");
print_data_of_t(&drawable_one);
}
}
plaintextButton::draw的地址是:0x557852f8f430
drawable_one的内容是:10 ab 84 6f 78 55 00 00 b8 11 fd 52 78 55 00 00
发现并不一样,这是怎么回事??
查阅文档后发现,原来特征对象的另一个地址并不直接是特征方法的地址,而是一个叫做 vtable的东西的地址
那么这个 vtable里面会有我们想要的方法的地址吗?
看一看就知道了,直接操作指针获取 vtable的地址
rust // 获取drawable_one的地址指针
let p_drawable_one: *const u64 = std::mem::transmute::<_, *const u64>(&drawable_one);
println!("drawable_one的地址是:{:p}", p_drawable_one);
// 偏移地址指针
let p_offset: *const u64 = p_drawable_one.add(1);
println!("p_drawable_one偏移1个地址指向:0x{:x}", *p_offset as u64);
// 获取vtable地址,转换为指针
let p_vtable = *p_offset as *const u64;
println!("vtable地址:{:p}", p_vtable);
然后就可以看里面有什么了
rust // 看一下vtable里面有什么
for i in 0..4 {
let p_fn = p_vtable.add(i);
println!("vtable[{}]:0x{:x}", i, *p_fn as u64)
}
最终结果如下
plaintextButton::draw的地址是:0x557852f8f430
drawable_one的内容是:10 ab 84 6f 78 55 00 00 b8 11 fd 52 78 55 00 00
drawable_one的地址是:0x7ffc82227d98
p_drawable_one偏移1个地址指向:0x557852fd11b8
vtable地址:0x557852fd11b8
vtable[0]:0x0
vtable[1]:0x4
vtable[2]:0x4
vtable[3]:0x557852f8f430
神奇的发现,我们终于在 vatable[3]处发现了我们想要找的特征方法 Button:draw的地址
大功告成,总算亲自体验了一下特征对象是如何实现的。
完整代码
rusttrait Draw {
fn draw(&self);
}
struct Button {
id: u32,
}
impl Draw for Button {
fn draw(&self) {
println!("这是屏幕上第{}号按钮", self.id)
}
}
struct Select {
id: u32,
}
impl Draw for Select {
fn draw(&self) {
println!("这个选择框贼难用{}", self.id)
}
}
fn main() {
unsafe {
// 获取Button的draw方法的地址
let p_draw_fn: *const u64 = Button::draw as *const u64;
println!("Button::draw的地址是:{:p}", p_draw_fn);
// 创建一个Draw特征对象
let drawable_one: Box<dyn Draw> = Box::new(Button { id: 2 });
// 看一下drawable_one里面有什么
print!("drawable_one的内容是:");
print_data_of_t(&drawable_one);
// 获取drawable_one的地址指针
let p_drawable_one: *const u64 = std::mem::transmute::<_, *const u64>(&drawable_one);
println!("drawable_one的地址是:{:p}", p_drawable_one);
// 偏移地址指针
let p_offset: *const u64 = p_drawable_one.add(1);
println!("p_drawable_one偏移1个地址指向:0x{:x}", *p_offset as u64);
// 获取vtable地址,转换为指针
let p_vtable = *p_offset as *const u64;
println!("vtable地址:{:p}", p_vtable);
// 看一下vtable里面有什么
for i in 0..4 {
let p_fn = p_vtable.add(i);
println!("vtable[{}]:0x{:x}", i, *p_fn as u64)
}
}
}
fn print_data_of_t<T>(pt: &T) {
// 将a地址转换为*const String类型
let address_of_t: *const T = pt as *const _;
// 获取a占用的内存大小
let size_of_t: usize = std::mem::size_of_val(pt);
// 直接去a的地址看看a到底装的什么
let bytes: &[u8] = unsafe { std::slice::from_raw_parts(address_of_t as *const u8, size_of_t) };
// 打印bytes数组,16进制
for b in bytes {
print!("{:02x} ", b);
}
println!();
}
💬 评论区
暂无评论,来发表第一条吧。