本来这一节是要继续实现虚方法的,但我们遇到了问题。我们要先将问题解决才能够继续。
之前我们将数据成员从类中拆分出来成为纯数据类,这样做的好处是:

  1. 每个类都可以方便的管理自己的虚表;
  2. 责任清晰:数据类负责数据,类自身负责虚表,数据类的构造函数负责初始化数据,类自身的构造函数负责初始化虚表,由数据类负责数据相关方法,类自身负责导出虚方法;

然而,但是很多情况下,责任没有办法划分的这样清楚,如:虚函数可以调用非虚函数,非虚函数也可以调用虚函数,而非虚函数定义于数据类,是没有虚表的。
一般情况下,我们可以用数据类的对象地址负偏移一个指针得到虚表指针地址,但是,如果类指定了对齐属性,想要获得虚表指针的地址就比较麻烦了。而且如果基类的对齐和派生类的对齐属性不一样,问题就更大了,如:

#[repr(C, align(16))]
pub struct BaseData
{
    x: i32,
    y: i32,
}
#[repr(C, align(16))]
pub struct Base
{
    vptr: &'static BaseVTable,
    data: BaseData,
}
#[repr(C, align(32))]
pub struct Derive1Data
{
    base: Base,
    z: i64,
}
#[repr(C, align(32))]
pub struct Derive1
{
    vptr: &'static Derive1VTable,
    data: Derive1Data
}

如上,基类和派生类分别指定了16字节对齐和32字节对齐的属性,为直观起见,我们用下图来表示两个类的内存模型,左为基类,右为派生类:
不同对齐属性的基类和派生类.jpg
我们看到,派生类和基类的内存模型是不兼容的,因而不能将派生类对象强制转换为基类,类机制能够正常运行的基础塌了。这大概是这么长时间以来我们遇到的最大的挫败了。

为此我们需要对对象模型进行重构,以保证在任何情况下,派生类和基类的内存模型都能够兼容。
回忆一下,我们期望的类定义如下:

#[class]
pub struct Base
{
    x: i32,
    y: i32,
    pub fn new(x: i32, y: i32) -> Self { Base{ x, y } }
    pub virtual fn func1(&self) -> i32 { self.x }
    pub virtual fn func2(&self, i: i32) -> i32 { self.y + i }
}
#[class]
pub struct Derive1 : Base
{
    z: i32,
    pub fn new(x: i32, y: i32, z: i32) -> Self { Derive1 { base: Base::new(x, y), z} }
    pub override fn func1(&self) -> i32 { self.z }
    pub virtual fn func3(&self) -> i32 { self.z }
}
#[class]
pub struct Derive2 : Derive1
{
    pub fn new(x: i32, y: i32, z: i32) -> Self { Derive2 { base: Derive1::new(x, y, z) } }
    pub override fn func2(&self, i: i32) -> i32 { Base::func2(self, i) + 200 }
    pub override fn func3(&self) -> i32 { Derive1::func3(self) + 200 }
}

这次,我们取消数据类,直接将数据成员放置于类的定义,基类重新展开如下:

#[repr(C)]
pub struct BaseVTable
{
    func1: fn(this: &Base) -> i32,
    func2: fn(this: &Base, i: i32) -> i32,
}
#[repr(C)]
pub struct Base
{
    vptr: &'static BaseVTable,
    x: i32,
    y: i32,
}
impl Base
{
    const TYPEINFO: TypeInfo = TypeInfo
    {
        base_class: None,
    };
    const VTABLE: BaseVTable = BaseVTable
    {
        _type_info: &Self::TYPEINFO,
        func1: Self::func1_impl0,
        func2: Self::func2_impl0,
    };
    pub fn new(x: i32, y: i32) -> Self
    {
        Base { vptr: &Self::VTABLE, x, y }
    }
    fn func1_impl1(&self) -> i32 { self.x }
    fn func1_impl0(this: &Base) -> i32 { this.func1_impl1() }
    pub fn func1(&self) -> i32 { (self.vptr.func1)(self) }
    fn func2_impl1(&self, i: i32) -> i32 { self.y + i }
    fn func2_impl0(this: &Base, i: i32) -> i32 { this.func2_impl2(i) }
    pub fn func2(&self, i: i32) -> i32 { (self.vptr.func2)(self, i) }
}

我们将虚函数拆分成了三个函数:

  1. 一个函数以 _impl1 为后缀,为函数的原始定义;
  2. 一个函数以 _impl0 为后缀,将 &self 参数更改为 this: &Base,并在内部转调 _impl1 后缀的函数,是虚表的需要;
  3. 一个函数名不加后缀,用来产生虚表调用。

我们没有将 _impl0 和 _impl1 合并为一个函数,是为了避免在函数体内进行将 self 替换为 this 的操作。而且虽然我们没有将函数合并,但是编译器会帮我们做,依然是 0 运行时开销。
接下来 Derive1 类应该展开为如下的形式:

#[repr(C)]
struct Derive1VTable
{
    func1: fn(this: &Base) -> i32,
    func2: fn(this: &Base, i: i32) -> i32,
    func3: fn(this: &Derive1) -> i32,
}
#[repr(C)]
pub struct Derive1
{
    base: Base,
    z: i32,
}
impl Derive1
{
    const TYPEINFO: TypeInfo = TypeInfo
    {
        base_class: Some(&Base::TYPEINFO),
    };
    const VTABLE: Derive1VTable = Derive1VTable
    {
        _type_info: &Self::TYPEINFO,
        func1: Self::func1_impl0,
        func2: Base::VTABLE.func2,
        func3: Self::func3_impl0,
    };
    fn new(x: i32, y: i32, z: i32) -> Self { ... }
    fn func1_impl1(&self) -> i32 { self.z }
    fn func1_impl0(this: &Base) -> i32
    {
        let this: &Self = unsafe { reinterpret_cast(this) };
        this.func1_impl1()
    }
    fn func3_impl1(&self) -> i32 { self.z }
    fn func3_impl0(this: &Derive1) -> i32 { this.func3_impl1() }
    pub fn func3(&self) -> i32 { ... }
}

但是问题来了,在新的模型中,派生类不能直接访问虚指针,因此我们无法初始化虚指针,也无法通过虚指针来调用方法。
不过我们都知道类的第一个元素就是虚指针,那么事情就好办了。

fn _vptr(&self) -> &DeriveVTable
{
    unsafe
    {
        let vptr = self as *const Self as *const *const Derive1VTable;
        &**vptr
    }
}
fn _init_vptr(&mut self)
{
    unsafe
    {
        let vptr = self as *mut Self as *mut *const Derive1VTable;
        *vptr = &Self::VTABLE;
    }
}

有了这两个方法,我们就能够在派生类中初始化虚表,及产生虚表调用了。

fn new(x: i32, y: i32, z: i32) -> Self
{
    let mut ret = Derive1 { base: Base::new(x, y), z };
    ret._init_vptr();
    ret
}
pub fn func3(&self) -> i32 { (self._vptr().func3)(self) }

接下来,我们展开 Derive2 类,如下:

type Derive2VTable = Derive1VTable;
#[repr(C)]
pub struct Derive2
{
    base: Derive1,
}
impl Derive2
{
    const TYPEINFO: TypeInfo = TypeInfo
    {
        base_class: Some(&Derive1::TYPEINFO),
    };
    const VTABLE: Derive2VTable = Derive2VTable
    {
        _type_info: &Self::TYPEINFO,
        func1: Derive1::VTABLE.func1,
        func2: Self::func2_impl,
        func3: Self::func3_impl,
    };
    fn _vptr(&self) -> &DeriveVTable
    {
        unsafe
        {
            let vptr = self as *const Self as *const *const Derive2VTable;
            &**vptr
        }
    }
    fn _init_vptr(&mut self)
    {
        unsafe
        {
            let vptr = self as *mut Self as *mut *const Derive2VTable;
            *vptr = &Self::VTABLE;
        }
    }
    pub new(x: i32, y: i32, z: i32) -> Self
    {
        let mut ret = Derive2 { base: Derive1(x, y, z) };
        ret._init_vptr();
        ret
    }
    fn func2_impl1(&self, i: i32) -> i32 { (Base::VTABLE.func2)(self, i) + 200 }
    fn func3_impl1(&self) -> i32 { (Derive1::VTABLE.func3)(self) + 200 }
    fn func2_impl0(this: &Base, i: i32) -> i32
    {
        let this: &Self = unsafe { reinterpret_cast(this) };
        this.func2_impl1(i)
    }
    fn func3_impl0(this: &Derive1) -> i32
    {
        let this: &Self = unsafe { reinterpret_cast(this) };
        this.func3_impl1()
    }
}

到这里新的模型以及可以工作了,但我们不能忘记重构的初衷,我们来验证下新模型是否解决了不同对齐属性的内存模型兼容问题,如下:

#[repr(C, align(16))]
pub struct Base
{
    vptr: &'static BaseVTable,
    x: i32,
    y: i32,
}
#[repr(C, align(32))]
pub struct Derive1
{
    base: Base,
    z: i64,
}

同上面的模型,此处的基类和派生类也分别指定了16字节对齐和32字节对齐的属性,新内存模型如下图,左为基类,右为派生类:
不同对齐属性的基类和派生类2.jpg
我们看到,对齐方式不会影响派生类和基类的兼容性,而且还紧凑了很多。理论上,在新模型中基类是整体作为派生类的第一个成员而存在的,因此派生类和基类的内存模型一定是兼容的。
至此新模型验证完毕,接下来我们将在新模型上继续工作。

标签: Rust 模拟 C++

已有 3 条评论

  1. 2025年10月新盘 做第一批吃螃蟹的人

  2. 2025年10月新盘 做第一批吃螃蟹的人coinsrore.com
    新车新盘 嘎嘎稳 嘎嘎靠谱coinsrore.com
    新车首发,新的一年,只带想赚米的人coinsrore.com
    新盘 上车集合 留下 我要发发 立马进裙coinsrore.com
    做了几十年的项目 我总结了最好的一个盘(纯干货)coinsrore.com
    新车上路,只带前10个人coinsrore.com
    新盘首开 新盘首开 征召客户!!!coinsrore.com
    新项目准备上线,寻找志同道合 的合作伙伴coinsrore.com
    新车即将上线 真正的项目,期待你的参与coinsrore.com
    新盘新项目,不再等待,现在就是最佳上车机会!coinsrore.com
    新盘新盘 这个月刚上新盘 新车第一个吃螃蟹!coinsrore.com

  3. hello

添加新评论