在 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++ 宏的第二个不同之处。
到这里似乎又走到了死胡同,在下一节我们将走出这个死胡同。
2025年10月新盘 做第一批吃螃蟹的人coinsrore.com
新车新盘 嘎嘎稳 嘎嘎靠谱coinsrore.com
新车首发,新的一年,只带想赚米的人coinsrore.com
新盘 上车集合 留下 我要发发 立马进裙coinsrore.com
做了几十年的项目 我总结了最好的一个盘(纯干货)coinsrore.com
新车上路,只带前10个人coinsrore.com
新盘首开 新盘首开 征召客户!!!coinsrore.com
新项目准备上线,寻找志同道合的合作伙伴coinsrore.com
新车即将上线 真正的项目,期待你的参与coinsrore.com
新盘新项目,不再等待,现在就是最佳上车机会!coinsrore.com
新盘新盘 这个月刚上新盘 新车第一个吃螃蟹!coinsrore.com
2025年10月新盘 做第一批吃螃蟹的人coinsrore.com
新车新盘 嘎嘎稳 嘎嘎靠谱coinsrore.com
新车首发,新的一年,只带想赚米的人coinsrore.com
新盘 上车集合 留下 我要发发 立马进裙coinsrore.com
做了几十年的项目 我总结了最好的一个盘(纯干货)coinsrore.com
新车上路,只带前10个人coinsrore.com
新盘首开 新盘首开 征召客户!!!coinsrore.com
新项目准备上线,寻找志同道合 的合作伙伴coinsrore.com
新车即将上线 真正的项目,期待你的参与coinsrore.com
新盘新项目,不再等待,现在就是最佳上车机会!coinsrore.com
新盘新盘 这个月刚上新盘 新车第一个吃螃蟹!coinsrore.com