2023年6月

上一节我们讨论了派生类中调用基类方法的问题,最终的代码实现还没有完成,本节我们要实现构造函数,这两个功能都需要对函数体进行遍历,于是我们就把它们放在一起来实现。
Rust 并无构造函数的概念,也没有统一的命名规则,只要关联函数返回值是 Self 或者类名即可。但除此之外,也可能有方法返回 Option 或者 Result<Self,E>,我们也认为它是构造函数。
在 Rust 中构造函数并不是必须的,用户也可以通过结构体的方式构造类的对象。但对于我们的类来说,构造函数是一定要有的,因为它涉及到虚表指针的初始化,是马虎不得的,因为我们的虚函数机制完全依赖于虚表指针,如果虚表指针初始化的不正确。程序出现任何异常都是有可能的。这就要求我们的类必须定义构造函数。
用户知道如何初始化成员变量,而我们知道如何初始化虚表指针,所以我们参考 C++ 的做法,由用户初始化成员变量,我们来初始化虚表指针,用户定义构造函数如下:

// Base
pub fn new(x: i32, y: i32) -> Self { Base{ x, y } }

用户不需要初始化虚指针,甚至不需要知道虚指针的存在。而我们会将构造函数转换为下面的形式:

pub fn new(x: i32, y: i32) -> Self
{
    Base { vptr: &Self::VTABLE, x, y }
}

对于派生类,我们需要暴露一些实现细节,我们需要用户显示使用 base 字段来初始化基类的部分,这样做确实是因为我们没有更好的方法。

// Derive1
pub fn new(x: i32, y: i32, z: i32) -> Self
{
    Derive1 { base: Base::new(x, y), z}
}
// Derive2
pub fn new(x: i32, y: i32, z: i32) -> Self
{
    Derive2 { base: Derive1::new(x, y, z) }
}

而上面的代码会转换为下面的形式:

// Derive1
fn new(x: i32, y: i32, z: i32) -> Self
{
    let mut ret = Derive1 { base: Base::new(x, y), z };
    ret._init_vptr();
    ret
}
// Derive2
fn new(x: i32, y: i32, z: i32) -> Self
{
    let mut ret = Derive2 { base: Derive1::new(x, y, z) };
    ret._init_vptr();
    ret
}

因为派生类无法直接访问虚表指针,因此先用基类的虚表地址来初始化虚表指针,完成之后再修改正为正确的虚表地址。
理论我们已经清楚了,接下来就是实现了。然而在实现中,构造函数可能不见得都如上面那样简单,可能在构造对象的操作前后都有复杂的逻辑,甚至可能返回类型是 Option 或者 Result<Self, ...>,也可能是用户定义的模板而我们不认识的,我们如何去转换这样的构造函数呢?

pub struct Base
{
    ...
    fn new(x: i32, y: i32) -> Self { Self { x, y } }
    fn some_func() -> Option<Self> { if ... { Some(Base::new(0, 0)) } else { None } }
    fn ok_func() -> Result<Base> { ... let r = Self { x: 0, y: 0 }; ... Ok(r) }
    fn other_func() -> OtherType<Self, ...> { ... OtherType { other_mem: Base { x: 1, y: 2 }, ...} }
}

拨开层层迷雾,我们不管它返回的类型是什么,只要函数内部有以 Self 关键字或自身类名来构造结构体的语法,就是我们的转换目标,如上文的 Self { x, y }, Self { x: 0, y: 0 }, Base { x: 1, y: 2 } 等。
现在我们的目标明确了,但要遍历函数体也不是一件容易的事,函数体内可用的语法元素非常之多,差不多意味着我们要为 syn 半数以上的类型编写遍历代码,这可不是一个小的工作量。幸好 syn 考虑到了遍历的需求,为开发者提供了 visit 和 visit_mut 模块,visit 模块用来遍历语法树,而 visit_mut 模块在遍历的同时可以修改语法树。两个模块分别通过特性 visit 和 visit-mut 来启用,下面我们启用了 visit-mut 特性,因为我们需要对修改语法树:

[dependencies]
syn = { version = "1.0", features = ["full", "extra-traits", "visit-mut"] }

接下来我们可以开始编写遍历类,如下:

pub struct InitVPTR<'a>
{
    class_name: &'a str,
    is_base_class: bool,
    blocked: bool,
}
impl<'a> VisitMut for InitVPTR<'a>
{
    fn visit_expr_mut(&mut self, expr: &mut Expr)
    {
        if !self.blocked
        {
            if let Expr::Struct(expr_stru) = expr
            {
                if let Some(seg) = expr_stru.path.segments.last()
                {
                    let name = seg.ident.to_string()
                    if self.class_name == name || "Self" == name
                    {
                        if self.is_base_class
                        {
                            self.base_class_init_vptr(expr_stru);
                        }
                        else
                        {
                            *expr = self.derive_class_init_vptr(expr_stru);
                        }
                    }
                }
            }
        }
        syn::visit_mut::visit_expr_mut(self, expr);
        self.blocked = false;
    }
}

由于 syn 提供了遍历的框架,因此我们只需要为我们需要的部分编写代码即可,在上文中我们通过遍历表达式得到了以类名或者 Self 关键字来构造结构体的表达式,接下来我们来改造结构体:

fn base_class_init_vptr(&mut self, expr_stru: &mut ExprStruct)
{
    expr_stru.fields.push(parse_quote!(vptr: &Self::VTABLE));
}
fn derive_class_init_vptr(&mut self, expr_stru: &mut ExprStruct) -> Expr
{
    self.blocked = true;
    Expr::Block(parse_quote!({ let mut ret = #expr_stru; ret._init_vptr(); ret }))
}

syn 的类型往往都比较复杂,手动构造不仅费时费力,而且还可能面临因为个别数据成员的类型定义在私有模块,而无法构造的困境,因此 syn 推荐使用 parse_quote! 宏,省时省力,可读性还好。
基类结构体的改造相对简单一些,只需添加 "vptr: &Self::VTABLE" 即可。
而派生类则是基于当前结构体构造了一个由大括号包裹的代码块,因为代码块中包含当前结构体,我们通过标记 self.blocked 标志,防止对结构体的重复循环遍历。
我们可以用 InitVPTR 类来改造构造函数了,用法也非常简单,如下:

let mut vis = visit_stmt::InitVPTR::new(class_name, true);
vis.visit_block_mut(&mut self.itemfn.block);

基类方法调用

构造函数的实现完成了,接下来我们实现在派生类虚函数中调用基类方法的问题。其实当我们处理完了构造函数,基类方法调用的问题也就迎刃而解了,两个问题的解决方法是一样的,都需要对函数体进行遍历,如下:

pub struct BaseFuncCall<'a>
{
    base_macro_name: &'a Ident,
}
impl<'a> VisitMut for BaseFuncCall<'a>
{ 
    fn visit_expr_mut(&mut self, expr: &mut Expr)
    {
        if let Expr::Call(call) = expr
        {
            if let Expr::Path(path) = &*call.func
            {
                let mut path = path.path.clone();
                if let Some(Pair::End(func)) = path.segments.pop()
                {
                    if let Some(Pair::Punctuated(class, _)) = path.segments.pop()
                    {
                        let macro_name = self.base_macro_name;
                        let args = call.args; 
                        let mut expr_macro = parse_quote!(#macro_name!(, call_super_func, #path #class #func #args));
                        expr_macro.attrs = call.attrs.clone();
                        *expr = Expr::Macro(expr_macro);
                    }
                }
            }
        }
        syn::visit_mut::visit_expr_mut(self, expr);
    }
}

首先我们找到通过 :: 进行的函数调用,并解析出了类名和函数名,然后转换为宏调用,因为属性不支持在 parse_quote! 宏中用 # 号来引用,因此是单独处理的。
BaseFuncCall 类的用法和 InitVPTR 相同:

let mut vis = visit_stmt::BaseFuncCall::new(class_name, true);
vis.visit_block_mut(&mut self.itemfn.block);

至此函数体的解析工作完成了,无论是构造函数还是基类方法调用我们都按照我们预想的工作了。本来我们预想中的很高很高的大山,在 syn::visit_mut 模块的帮助之下轻松的飞越了。终于,离我们的目标又近了一步。