标签 proc-macro2 下的文章

之前两节,对于 C++ 类的手工验证阶段已经结束,接下来就要用宏来自动化生成代码。
回顾一下最初的想法:

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

从上面的定义来看,我们需要实现属性宏,三件套 proc_macro2, syn, quote 必不可少,都要添加到 Cargo.toml 的依赖列表:

[package]
name = "class_macro"
version = "0.1.0"
edition = "2021"

[lib]
proc-macro = true

[dependencies]
proc-macro2 = "1.0"
syn = { version = "1.0", features = ["full"] }
quote = "1.0"

其中,syn 需要指定 features 为 full,否则缺少一些特性,下面实现属性宏 class:

extern crate proc_macro;
use crate::proc_macro::TokenStream;
use quote::quote;
use syn;
mod class_def;

#[proc_macro_attribute]
pub fn class(_attr: TokenStream, input: TokenStream) -> TokenStream
{
    let class_def = syn::parse_macro_input!(input as class_def::ClassDef);
    let gen = quote! { #class_def };
    gen.into()
}

初次接触 syn 会觉得毫无头绪,我建议仔细学习 syn 的源码,syn 源码是一个大宝库,里面实现了 Rust 语言完整的语法定义及解析代码,可供开发者重用,而且还能够学习到一些文档和教科书上不曾提及的语法细节。
我们的类定义是在一个结构体的基础上,添加了基类,将方法写入结构体内部,并且增加了两个关键字 virtual 和 override。为了描述我们的类定义,我们参考 syn::ItemStruct 定义了 class_def::ClassDef。如下:

pub enum Virtuals
{
    Virtual,
    Override,
    Inherited,
}
pub struct VirtualFn
{
    virs: Virtuals,
    itemfn: ImplItemMethod,
}
pub struct ClassDef
{
    attrs: Vec<Attribute>,
    vis: Visibility,
    struct_token: Token![struct],
    ident: Ident,
    generics: Generics,
    base_class: Option<Ident>,
    base_generics: Option<Generics>,
    fields: FieldsNamed,
    vfns: Vec<VirtualFn>,
    semi_token: Option<Token![;]>,
}

为了能够将 TokenStream 解析为 ClassDef,syn 会调用要求 ClassDef 实现 Parse trait 的 parse(...) 方法, 方法实现如下,鉴于篇幅的原因这里就不全部展开了:

impl Parse for ClassDef
{
    fn parse(input: ParseStream) -> Result<Self>
    {
        let attrs = input.call(Attribute::parse_outer)?;
        let vis = input.parse()?;
        let struct_token = input.parse()?;
        let ident: Ident = input.parse()?;
        let generics = input.parse()?;
        let mut base_class: Option<Ident> = None;
        let mut base_generics: Option<Generics> = None;
        if let Ok(_) = input.parse::<Token![:]>()
        {
            base_class = Some(input.parse()?);
            base_generics = Some(input.parse()?);
        }
        let where_clause = Self::parse_where_clause(&input)?;
        let (fields, vfns) = Self::parse_fields_vfns(&input, ident.to_string().as_str())?;

        let generics = Generics { where_clause, .. generics };
        Ok(ClassDef {attrs, vis, struct_token, ident, generics, base_class, base_generics, fields, vfns})
    }
}

到这里我们已经将输入的 TokenStream 解析为我们的 ClassDef,接下来就要自动化生成类代码了。由于所需生成的代码过于复杂,无法在 quote!() 宏描述,故我将 #class_def 作为唯一的输入,并为 ClassDef 实现 ToTokens trait 的 to_tokens 方法,大致如下:

impl ToTokens for ClassDef
{
    fn to_tokens(&self, tokens: &mut TokenStream)
    {
        let helper = ...
        self.class_vtable_to_tokens(tokens, &helper);
        self.class_data_to_tokens(tokens, &helper);
        self.class_def_to_tokens(tokens, &helper);
        self.class_data_impl_to_tokens(tokens, &helper);
        self.class_impl_to_tokens(tokens, &helper);
    }
}

鉴于篇幅,具体的代码就不展开了。
我们生成基类代码的时候,一切都很顺利,但当我们生成派生类代码时,问题来了,基类的虚表定义如下:

pub struct BaseVTable
{
    func1: fn(this: &Base) -> i32,
    func2: fn(this: &Base, i: i32) -> i32,
}

这里没有问题,因为基类知道它所需要的所有虚函数的信息,生成虚表并不难,但是派生类并不知道所有的虚函数信息,如下,Derive1 类重写了方法 func2 并增加了新的虚函数 func3,但 Derive1 并不知道 func1 的存在:

struct Derive1VTable
{
    func1: fn(this: &Base) -> i32,
    func2: fn(this: &Base, i: i32) -> i32,
    func3: fn(this: &Derive1) -> i32,
}

我们只能够拿到当前类的定义,而无法拿到基类的定义,所以我们不知道基类的虚表长什么样子,因而也无法将基类虚表的定义嵌入到派生类的虚表中。

  • 之前也考虑另一种方案,就是直接将基类虚表作为派生类虚表的一个数据成员,从内存布局上来说,下面的定义和上面的定义是相同的。

    struct Derive1VTable
    {
        base: BaseVTable,
        func3: fn(this: &Derive1) -> i32,
    }
  • 但问题是,当类的派生层次增加,发生函数重写时,初始化虚表的实现将变得复杂,且丑,以 Derive2 为例:

    struct Derive2VTable
    {
        base: Derive1VTable,
        ...
    }
    const VTABLE: Derive2VTable = Derive2VTable
    {
        base: Derive1VTable
        {
            base: BaseVTable
            {
                func1: Derive1VTable::VTABLE.base.func1,
                func2: Self::func2_impl,
            },
            func3: Self::func3_impl,
        },
        ...
    }
  • 而且因为我们不知道基类的定义,我们也无法得知每个方法的具体路径,而这要求我们知道所有基类的定义,这个方案不仅没有解决问题,反而将问题复杂化了。

相比之下,将基类虚表复制到派生类的方法,只需要知道直接继承的基类虚表就好了。那么如何才能知道直接基类的虚表呢?我们下一节来解决这个问题。