在 Rust 中模拟 C++ 类的功能 模型重构第七
本来这一节是要继续实现虚方法的,但我们遇到了问题。我们要先将问题解决才能够继续。
之前我们将数据成员从类中拆分出来成为纯数据类,这样做的好处是:
- 每个类都可以方便的管理自己的虚表;
- 责任清晰:数据类负责数据,类自身负责虚表,数据类的构造函数负责初始化数据,类自身的构造函数负责初始化虚表,由数据类负责数据相关方法,类自身负责导出虚方法;
然而,但是很多情况下,责任没有办法划分的这样清楚,如:虚函数可以调用非虚函数,非虚函数也可以调用虚函数,而非虚函数定义于数据类,是没有虚表的。
一般情况下,我们可以用数据类的对象地址负偏移一个指针得到虚表指针地址,但是,如果类指定了对齐属性,想要获得虚表指针的地址就比较麻烦了。而且如果基类的对齐和派生类的对齐属性不一样,问题就更大了,如:
#[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字节对齐的属性,为直观起见,我们用下图来表示两个类的内存模型,左为基类,右为派生类:
我们看到,派生类和基类的内存模型是不兼容的,因而不能将派生类对象强制转换为基类,类机制能够正常运行的基础塌了。这大概是这么长时间以来我们遇到的最大的挫败了。
为此我们需要对对象模型进行重构,以保证在任何情况下,派生类和基类的内存模型都能够兼容。
回忆一下,我们期望的类定义如下:
#[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) }
}
我们将虚函数拆分成了三个函数:
- 一个函数以 _impl1 为后缀,为函数的原始定义;
- 一个函数以 _impl0 为后缀,将 &self 参数更改为 this: &Base,并在内部转调 _impl1 后缀的函数,是虚表的需要;
- 一个函数名不加后缀,用来产生虚表调用。
我们没有将 _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字节对齐的属性,新内存模型如下图,左为基类,右为派生类:
我们看到,对齐方式不会影响派生类和基类的兼容性,而且还紧凑了很多。理论上,在新模型中基类是整体作为派生类的第一个成员而存在的,因此派生类和基类的内存模型一定是兼容的。
至此新模型验证完毕,接下来我们将在新模型上继续工作。