在 Rust 中模拟 C++ 类的功能 在规则宏中拼接标识符第四
在上一节,我们遇到了点问题,在生成派生类代码时,我们拿不到基类的定义,也就无法为派生类生成虚表。现在我们来解决它。如果我们能将基类虚表的信息存储于一个变量中,那么就可以在派生类虚表中使用它,那么怎么定义这个变量好呢?为了不增加运行时负担,我们可以用宏来做这件事,具体来说是规则宏。
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++ 宏的第二个不同之处。
到这里似乎又走到了死胡同,在下一节我们将走出这个死胡同。