2022年12月

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

  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
我们看到,对齐方式不会影响派生类和基类的兼容性,而且还紧凑了很多。理论上,在新模型中基类是整体作为派生类的第一个成员而存在的,因此派生类和基类的内存模型一定是兼容的。
至此新模型验证完毕,接下来我们将在新模型上继续工作。