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

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++ 宏的第二个不同之处。
到这里似乎又走到了死胡同,在下一节我们将走出这个死胡同。

标签: concat_idents, 在 rust 规则宏中拼接标识符, Rust 模拟 C++

添加新评论