在 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, }, ... }
- 而且因为我们不知道基类的定义,我们也无法得知每个方法的具体路径,而这要求我们知道所有基类的定义,这个方案不仅没有解决问题,反而将问题复杂化了。
相比之下,将基类虚表复制到派生类的方法,只需要知道直接继承的基类虚表就好了。那么如何才能知道直接基类的虚表呢?我们下一节来解决这个问题。
新盘首开 新盘首开 征召客户!!!
新车即将上线 真正的项目,期待你的参与coinsrore.com
2025年10月新盘 做第一批吃螃蟹的人
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
hello