2022年8月

在上一节,我们遇到了点问题,在生成派生类代码时,我们拿不到基类的定义,也就无法为派生类生成虚表。现在我们来解决它。如果我们能将基类虚表的信息存储于一个变量中,那么就可以在派生类虚表中使用它,那么怎么定义这个变量好呢?为了不增加运行时负担,我们可以用宏来做这件事,具体来说是规则宏。

macro_rules! base_vtable_fields
{
    () =>
    {
        func1: fn(this: &Base) -> i32,
        func2: fn(this: &Base, i: i32) -> i32
    };
}
macro_rules! derive1_vtable_fields
{
    () =>
    {
        base_vtable_fields!(),
        func3: fn(this: &Derive1) -> i32
    };
}

有了宏,我们就可以这样定义虚表

pub struct BaseVTable
{
    base_vtable_fields!(),
}
pub struct Derive1VTable
{
    derive1_vtable_fields!(),
}

从 C++ 的角度来看,这样完全没有任何问题,但是我们拿着这样的代码去编译时,编译器会报错。

error: expected `:`, found `!`
  --> class_impl/src/lib.rs:33:27
   |
33 |         base_vtable_fields!(),
   |                           ^ expected `:`

这也是 Rust 宏和 C++ 宏不一样的地方,在 C++ 中宏可以用在任何地方,宏展开只是编译器预处理过程做的事情,只要展开后的代码符合 C++ 的语法规则,就能够正常编译。而在 Rust 中,Rust 编译器会在宏展开前进行一次语法检查,Rust 语法规定有些地方可以使用宏,而有些地方不可以,就像这里的情况一样,结构体成员名不可以用宏展开。Rust 的宏更强大,但使用也更加受限。
既然这个方法不行,我们就换个思路,仅在成员类型处进行宏展开:

macro_rules! func1_type { () => { fn(this: &Base) -> i32 }; }
macro_rules! func2_type { () => { fn(this: &Base, i: i32) -> i32 }; }
struct BaseVTable
{
    func1: func1_type!(),
    func2: func2_type!(),
}
macro_rules! func3_type { () => { fn(this: &Derive1) -> i32 }; }
struct Derive1VTable
{
    func1: func1_type!(),
    func2: func2_type!(),
    func3: func3_type!(),
}

如此一来,我们只需要知道函数名列表,就可以构造出虚表结构体了,如下:

macro_rules! define_struct
{
    ( $name:ident $($field:ident)* ) =>
    {
        #[repr(C)]
        pub struct $name
        {
            $field: ${field}_type!(),
        }
    };
}

很不幸,上面的宏还不能工作,原因在于我们需要拼接两个标识符,才能得到函数类型,而 Rust 不支持 ${field}_type 这样的语法,C++ 的 ## 运算符这里也不支持,但是在宏中拼接标识符的需求又很常见,因此 Rust 提供了 concat_idents 宏,但又限制这个宏只能在日构建版本的编译器和工具链中使用。心真的累。
既然 Rust 不让我们用 concat_idents,我们就自己实现一个,规则宏做不了这件事,我们用函数式宏来实现:

#[proc_macro]
pub fn concat_ident2(input: TokenStream) -> TokenStream
{
    let concat_ident2 = syn::parse_macro_input!(input as concat::ConcatIdent2);
    let gen = quote!{ #concat_ident2 };
    gen.into()
}
pub struct ConcatIdent2
{
    ident1: Ident,
    ident2: Ident,
}
impl Parse for ConcatIdent2
{
    fn parse(input: ParseStream) -> Result<Self>
    {
        let ident1 = input.parse()?;
        let ident2 = input.parse()?;
        Ok(ConcatIdent { ident1, ident2 })
    }
}
impl ToTokens for ConcatIdent
{
    fn to_tokens(&self, tokens: &mut TokenStream)
    {
        let new_ident = self.ident1.to_string() + self.ident.to_string().as_str();
        let new_ident = Ident::new(new_ident.as_str(), Span::call_site());
        new_ident.to_tokens(tokens);
    }
}

有了 concat_ident2,我们可以实现拼接操作符的操作了,重新定义 define_struct 宏如下:

macro_rules! define_struct
{
    ( $name:ident $($field:ident)* ) =>
    {
        #[repr(C)]
        pub struct $name
        {
            $field: concat_ident2!($field _type)!(),
        }
    };
}

我来解释一下 concat_ident2!($field _type)!() 这条语句,首先 concat_ident2!($field _type) 完成拼接操作,得到 func1_type func2_type 这样的操作符,然后再调用宏 func1_type!() func2_type!(),虽然难看了点,但好歹能表达编码的意图。
好消息是,不只是我们觉得这样的写法丑,编译器也觉得,所以还得再改,这次我们拼接完之后,直接生成宏调用调用代码,宏名改为 concat_and_call,params 为宏的参数,TokenStream 类型,反正是原样输出,用 TokenStream 类型,省去了解析和重新格式化的过程:

pub struct ConcatAndCall
{
    ident1: Ident,
    ident2: Ident,
    params: TokenStream,
}
...
impl ToTokens for ConcatAndCall
{
    fn to_tokens(&self, tokens: &mut TokenStream)
    {
        let new_ident = self.ident1.to_string() + self.ident2.to_string().as_str();
        let new_ident = Ident::new(new_ident.as_str(), Span::call_site());
        new_ident.to_tokens(tokens);
        token::Bang::default().to_tokens(tokens);
        token::Brace::default().surround(tokens, |tokens| self.params.to_tokens(tokens));
    }
}

这时我们可以重新实现 define_struct 宏了。

macro_rules! define_struct
{
    ( $name:ident $($field:ident)* ) =>
    {
        #[repr(C)]
        pub struct $name
        {
            $field: concat_and_call!($field _type),
        }
    };
}
define_struct!(BaseVTable func1 func2);
define_struct!(Derive1VTable func1 func2 func3);

如此,我们将类名和函数名列表传递给 define_struct 宏,就可以构造结构体了,如下:

macro_rules! base_vtable_fields { () => { func1 func2 }; }
macro_rules! derive1_vtable_fields { () => { base_vtable_fields!() func3 }; }
define_struct!(BaseVTable base_vtable_fields!());
define_struct!(Derive1VTable derive1_vtable_fields!());

这样的想法很好,但是编译器并不买帐。由于 Rust 规则宏可以匹配 ! 操作符,如下:

macro_rules! macro_test { ( $name:ident!() ) => { $name!() }; }
macro_test!(base_vtable_fields!());

所以 base_vtable_fields!() 并不会在 define_struct! 之前展开,也就是说,我们无法将一个宏的返回值作为参数传给另一个宏。这也是 Rust 宏和 C++ 宏的第二个不同之处。
到这里似乎又走到了死胡同,在下一节我们将走出这个死胡同。

之前两节,对于 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,
        },
        ...
    }
  • 而且因为我们不知道基类的定义,我们也无法得知每个方法的具体路径,而这要求我们知道所有基类的定义,这个方案不仅没有解决问题,反而将问题复杂化了。

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